${rbacEscape(rbacShortId(log.resourceId || log.resource_id))}` : ''}
- ${rbacEscape(formatTime(log.createdAt || log.created_at))}
- diff --git a/web/static/css/style.css b/web/static/css/style.css index 02364ca3..9bd69023 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -3531,7 +3531,6 @@ html[data-theme="dark"] .user-menu-dropdown { flex-shrink: 0; } -.mcp-call-buttons, .mcp-call-toolbar { display: flex; flex-wrap: wrap; @@ -10291,7 +10290,7 @@ html[data-theme="dark"] .robot-binding-one-time-badge { .rbac-assignment-toolbar-main { display: grid; - grid-template-columns: minmax(140px, 180px) minmax(220px, 1fr); + grid-template-columns: minmax(140px, 180px) minmax(280px, 1fr); align-items: end; gap: 10px; } @@ -10301,51 +10300,6 @@ html[data-theme="dark"] .robot-binding-one-time-badge { min-width: 0; } -.rbac-advanced-assignment--inline { - margin: 0; - border: 0; - border-top: 1px dashed color-mix(in srgb, var(--border-color) 88%, transparent); - border-radius: 0; - background: transparent; -} - -.rbac-advanced-assignment--inline summary { - padding: 2px 0 0; - color: var(--text-secondary); - font-size: 0.75rem; - cursor: pointer; - list-style: none; -} - -.rbac-advanced-assignment--inline summary::-webkit-details-marker { - display: none; -} - -.rbac-advanced-assignment--inline summary::before { - content: '▸'; - display: inline-block; - width: 14px; - color: var(--text-muted); - transition: transform 0.15s ease; -} - -.rbac-advanced-assignment--inline[open] summary::before { - transform: rotate(90deg); -} - -.rbac-advanced-assignment-body { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(180px, 280px); - align-items: center; - gap: 10px; - padding-top: 8px; -} - -.rbac-advanced-assignment-body span { - color: var(--text-secondary); - font-size: 0.6875rem; - line-height: 1.35; -} .rbac-assignment-controls { flex: 0 0 auto; @@ -10354,10 +10308,6 @@ html[data-theme="dark"] .robot-binding-one-time-badge { gap: 8px; } -.rbac-advanced-assignment { - flex: 0 0 auto; - margin-bottom: 0; -} .rbac-assignment-help { flex: 0 0 auto; @@ -10373,12 +10323,12 @@ html[data-theme="dark"] .robot-binding-one-time-badge { align-items: stretch; gap: 12px; flex: 1 1 auto; - min-height: clamp(320px, 46vh, 680px); + min-height: 0; } .rbac-resource-section { - display: grid; - grid-template-rows: auto minmax(clamp(220px, 34vh, 560px), 1fr) auto; + display: flex; + flex-direction: column; min-width: 0; min-height: 0; height: 100%; @@ -10390,10 +10340,11 @@ html[data-theme="dark"] .robot-binding-one-time-badge { .rbac-resource-section-head { display: flex; - flex-direction: column; - justify-content: center; + flex-direction: row; + align-items: center; + justify-content: space-between; gap: 8px; - min-height: auto; + min-height: 54px; padding: 10px 12px; border-bottom: 1px solid var(--border-color); background: color-mix(in srgb, var(--bg-secondary) 70%, var(--bg-primary)); @@ -10404,6 +10355,7 @@ html[data-theme="dark"] .robot-binding-one-time-badge { align-items: baseline; justify-content: space-between; gap: 10px; + min-width: 0; } .rbac-resource-section-head strong { @@ -10421,6 +10373,28 @@ html[data-theme="dark"] .robot-binding-one-time-badge { grid-template-columns: minmax(112px, 0.7fr) minmax(150px, 1.3fr); gap: 8px; margin-top: 0; + width: 100%; +} + +.rbac-assignment-filterbar { + flex: 0 0 auto; + padding: 8px 12px; + border-bottom: 1px solid var(--border-color); + background: color-mix(in srgb, var(--bg-secondary) 35%, var(--bg-primary)); +} + +.rbac-resource-filterbar { + display: grid; + grid-template-columns: minmax(110px, 0.65fr) minmax(150px, 1fr) minmax(190px, 1.25fr); + gap: 8px; +} + +.rbac-resource-filterbar :is(.form-select, .form-input), +.rbac-resource-filterbar .settings-custom-select-trigger { + width: 100%; + min-width: 0; + min-height: 40px; + height: 40px; } .rbac-assignment-filters :is(.form-select, .form-input) { @@ -10492,7 +10466,8 @@ html[data-theme="dark"] .robot-binding-one-time-badge { display: flex; flex-direction: column; gap: 6px; - min-height: clamp(220px, 34vh, 560px); + min-height: 0; + max-height: 300px; margin: 0; padding: 8px; overflow-y: auto; @@ -10578,10 +10553,13 @@ html[data-theme="dark"] .robot-binding-one-time-badge { .rbac-pending-bar { display: flex; - flex-direction: column; - gap: 10px; + flex: 0 0 auto; + flex-direction: row; + align-items: center; + gap: 12px; + min-height: 0; margin: 0 0 12px; - padding: 12px; + padding: 9px 10px; border: 1px solid rgba(37, 99, 235, 0.24); border-radius: 8px; background: rgba(37, 99, 235, 0.05); @@ -10593,22 +10571,32 @@ html[data-theme="dark"] .robot-binding-one-time-badge { .rbac-pending-bar-head { display: flex; + flex: 0 0 auto; align-items: center; - gap: 8px; + gap: 6px; } .rbac-pending-list { display: flex; + flex: 1 1 auto; flex-wrap: wrap; - gap: 8px; + align-content: flex-start; + gap: 6px; + min-width: 0; + max-height: 72px; + padding-right: 2px; + overflow-y: auto; + overscroll-behavior: contain; + scrollbar-gutter: stable; } .rbac-pending-item { display: flex; align-items: center; - gap: 8px; + gap: 6px; min-width: 0; - padding: 6px 10px; + max-width: 260px; + padding: 4px 5px 4px 9px; border: 1px solid var(--border-color); border-radius: 999px; background: var(--bg-primary); @@ -10616,11 +10604,16 @@ html[data-theme="dark"] .robot-binding-one-time-badge { .rbac-pending-item-main { display: flex; - flex-direction: column; - gap: 1px; + align-items: baseline; + gap: 5px; min-width: 0; } +.rbac-pending-item-main strong { + font-size: 0.75rem; + font-weight: 600; +} + .rbac-pending-item-main strong, .rbac-pending-item-main small { overflow: hidden; @@ -10635,10 +10628,48 @@ html[data-theme="dark"] .robot-binding-one-time-badge { .rbac-pending-bar-actions { display: flex; + flex: 0 0 auto; justify-content: flex-end; gap: 8px; } +.rbac-pending-remove { + display: inline-flex; + flex: 0 0 auto; + align-items: center; + justify-content: center; + width: 22px; + height: 22px; + padding: 0; + border: 0; + border-radius: 50%; + background: transparent; + color: var(--text-muted); + font-size: 1rem; + line-height: 1; + cursor: pointer; +} + +.rbac-pending-remove:hover { + background: rgba(239, 68, 68, 0.1); + color: var(--danger-color, #dc2626); +} + +@container rbac-page (max-width: 760px) { + .rbac-pending-bar { + align-items: stretch; + flex-direction: column; + } + + .rbac-pending-bar-actions { + justify-content: flex-end; + } + + .rbac-pending-list { + max-height: 108px; + } +} + .rbac-resource-selection-bar { display: flex; align-items: center; @@ -10692,7 +10723,8 @@ html[data-theme="dark"] .robot-binding-one-time-badge { display: flex; flex-direction: column; gap: 6px; - min-height: clamp(220px, 34vh, 560px); + min-height: 0; + max-height: 300px; padding: 8px; overflow-y: auto; overscroll-behavior: contain; @@ -10702,12 +10734,12 @@ html[data-theme="dark"] .robot-binding-one-time-badge { .rbac-assignments-list .empty-state, .rbac-resource-picker .rbac-picker-status, .rbac-resource-picker .rbac-empty { - flex: 1 1 auto; + flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; justify-content: center; - min-height: 120px; + min-height: 140px; margin: 0; padding: 20px 16px; border: 1px dashed var(--border-color); @@ -10716,8 +10748,20 @@ html[data-theme="dark"] .robot-binding-one-time-badge { } .rbac-assignment-row { - min-height: 45px; - padding: 8px 10px; + min-height: 52px; + padding: 9px 10px; + border: 0; + border-bottom: 1px solid color-mix(in srgb, var(--border-color) 78%, transparent); + border-radius: 0; + background: transparent; +} + +.rbac-assignment-row:last-child { + border-bottom: 0; +} + +.rbac-assignment-row:hover { + background: color-mix(in srgb, var(--accent-color) 3%, var(--bg-primary)); } .rbac-pagination { @@ -10730,6 +10774,7 @@ html[data-theme="dark"] .robot-binding-one-time-badge { padding: 8px; border-top: 1px solid var(--border-color); background: color-mix(in srgb, var(--bg-secondary) 68%, var(--bg-primary)); + margin-top: auto; } .rbac-pagination[hidden] { @@ -10776,9 +10821,7 @@ html[data-theme="dark"] .robot-binding-one-time-badge { max-height: min(420px, 60vh); } - .rbac-assignment-columns { - min-height: clamp(360px, 52vh, 720px); - } + .rbac-assignment-columns { min-height: 0; } .rbac-panel-detail #rbac-tab-assignments.rbac-tab-panel:not([hidden]) { overflow-y: visible; @@ -26839,6 +26882,165 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda } } +/* Skills 管理:复用全局页面与列表规范,移除该页独有的渐变/悬浮视觉。 */ +.skills-management-page .page-content { + padding: 16px 20px 0; + gap: 12px; +} + +.skills-management-page .skills-overview { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.skills-management-page .skills-overview-card, +.skills-management-page .skills-overview-card-primary { + min-height: 0; + padding: 12px 14px; + gap: 6px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-primary); + box-shadow: var(--shadow-xs); +} + +.skills-management-page .skills-overview-card strong { + font-size: 1.25rem; + font-weight: 600; +} + +.skills-management-page .skills-search-box::before { + display: none; +} + +.skills-management-page .skills-search-box input { + height: auto; + padding: 8px 40px 8px 16px; + border-radius: 8px; + background: var(--bg-primary); + box-shadow: none; +} + +.skills-management-page .skills-grid { + gap: 12px; + padding: 0 0 16px; +} + +.skills-management-page .skill-card { + min-height: 76px; + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 16px; + padding: 16px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: var(--bg-primary); + box-shadow: none; + overflow: visible; +} + +.skills-management-page .skill-card::after { + display: none; +} + +.skills-management-page .skill-card:hover { + transform: none; + border-color: var(--accent-color); + box-shadow: var(--shadow-sm); +} + +.skills-management-page .skill-card-body { + flex: 1; + min-width: 0; + display: flex; + align-items: center; + gap: 16px; +} + +.skills-management-page .skill-card-header { + gap: 6px; +} + +.skills-management-page .skill-card-title-row { + flex-wrap: wrap; + gap: 8px 12px; +} + +.skills-management-page .skill-card-meta { + gap: 8px; +} + +.skills-management-page .skill-card-meta span { + padding: 0; + border: 0; + border-radius: 0; + background: transparent; + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 400; +} + +.skills-management-page .skill-card-description { + display: block; + min-height: 0; + overflow: visible; + line-height: 1.45; +} + +.skills-management-page .skill-card-actions { + display: flex; + grid-column: auto; + grid-row: auto; + margin: 0; + align-items: center; + justify-content: flex-end; +} + +.skills-management-page .skill-card-actions .btn-secondary { + padding: 6px 12px; + border-radius: 6px; + background: var(--bg-primary); +} + +html[data-theme="dark"] .skills-management-page .skills-overview-card, +html[data-theme="dark"] .skills-management-page .skills-overview-card-primary, +html[data-theme="dark"] .skills-management-page .skill-card, +html[data-theme="dark"] .skills-management-page .skills-search-box input, +html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-secondary { + background: var(--bg-primary); + border-color: var(--border-color); + box-shadow: none; +} + +@media (max-width: 768px) { + .skills-management-page .page-content { + padding: 14px 14px 0; + } + + .skills-management-page .skills-overview { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .skills-management-page .skill-card-body { + width: 100%; + flex-direction: column; + align-items: flex-start; + } + + .skills-management-page .skill-card-actions { + width: 100%; + justify-content: flex-start; + flex-wrap: wrap; + } +} + +@media (max-width: 520px) { + .skills-management-page .skills-overview { + grid-template-columns: 1fr; + } +} + .skill-item { background: var(--bg-primary); border: 1px solid var(--border-color); @@ -26996,146 +27198,37 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda color: var(--text-secondary); } -/* Skills 状态监控:专属视觉增强 */ -.skills-monitor-page { - --skills-blue: #2563eb; - --skills-cyan: #06b6d4; - --skills-success: #16a34a; - --skills-danger: #e11d48; - --skills-card-shadow: 0 10px 28px rgba(15, 23, 42, 0.08); -} - -.skills-monitor-page .page-content { - background: - linear-gradient(180deg, rgba(37, 99, 235, 0.06) 0%, rgba(248, 250, 252, 0) 260px), - #f5f7fb; - padding: 20px 24px 28px; -} - -.skills-monitor-page .page-header { - padding: 16px 24px; - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(12px); -} - -.skills-monitor-page .page-header h2 { - font-weight: 750; -} - -.skills-monitor-page .monitor-sections { - gap: 16px; -} - -.skills-monitor-page .monitor-section { - border-color: rgba(37, 99, 235, 0.1); - border-radius: 14px; - box-shadow: var(--skills-card-shadow); -} - -.skills-monitor-page .monitor-section.monitor-overview { - background: transparent; - border: 0; - box-shadow: none; -} - -.skills-monitor-page .monitor-section .section-header h3 { - display: inline-flex; - align-items: center; - gap: 10px; - font-size: 1.05rem; -} - -.skills-monitor-page .monitor-section .section-header { - background: transparent; - border: 0; - box-shadow: none; - padding: 0; -} - -.skills-monitor-page .monitor-section .section-header h3::before { - content: ""; - width: 9px; - height: 9px; - border-radius: 999px; - background: linear-gradient(135deg, var(--skills-blue), var(--skills-cyan)); - box-shadow: 0 0 0 5px rgba(37, 99, 235, 0.1); -} - -.skills-monitor-page .monitor-stats-grid { - grid-template-columns: repeat(5, minmax(150px, 1fr)); - gap: 18px; -} - -.skills-monitor-page .skills-stat-card { - position: relative; - isolation: isolate; - min-height: 124px; - padding: 20px; - border: 1px solid rgba(37, 99, 235, 0.12); - border-radius: 16px; - background: - linear-gradient(145deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.92)), - radial-gradient(circle at 100% 0%, rgba(37, 99, 235, 0.14), transparent 34%); - box-shadow: var(--skills-card-shadow); - overflow: hidden; - transition: transform 0.18s ease, border-color 0.18s ease, box-shadow 0.18s ease; -} - -.skills-monitor-page .skills-stat-card:hover { - transform: translateY(-2px); - border-color: rgba(37, 99, 235, 0.24); - box-shadow: 0 14px 34px rgba(15, 23, 42, 0.12); +/* Skills 状态监控:只保留数据展示所需规则,视觉复用通用页面组件。 */ +.skills-monitor-page .monitor-overview { + gap: 12px; } .skills-monitor-page .monitor-stat-label { - color: #64748b; - font-size: 0.86rem; - font-weight: 650; + color: var(--text-secondary); + font-size: 0.875rem; + font-weight: 500; } -.skills-monitor-page .monitor-stat-value { - margin-top: 6px; - font-size: 2.15rem; - font-weight: 780; - line-height: 1.05; - letter-spacing: 0; - color: #111827; +.skills-monitor-page .monitor-stat-value, +.skills-monitor-page .skills-table-pill, +.skills-monitor-page .skills-table-number, +.skills-monitor-page .skills-rate-cell, +.skills-monitor-page .skills-last-call { font-variant-numeric: tabular-nums; } .skills-monitor-page .monitor-stat-value.is-success, .skills-monitor-page .skills-table-number.is-success { - color: var(--skills-success); + color: var(--success-color); } .skills-monitor-page .monitor-stat-value.is-danger, .skills-monitor-page .skills-table-number.is-danger { - color: var(--skills-danger); -} - -.skills-monitor-page #skills-monitor-list { - border-color: rgba(15, 23, 42, 0.08); - border-radius: 14px; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7); + color: var(--error-color); } .skills-monitor-page .skills-monitor-table { min-width: 900px; - font-size: 0.875rem; -} - -.skills-monitor-page .skills-monitor-table thead { - background: linear-gradient(180deg, #f8fafc 0%, #f1f5f9 100%); -} - -.skills-monitor-page .skills-monitor-table th { - position: sticky; - top: 0; - z-index: 1; - padding: 14px 16px; - color: #64748b; - font-size: 0.78rem; - letter-spacing: 0; } .skills-monitor-page .skills-monitor-table th:first-child, @@ -27143,174 +27236,32 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda text-align: left; } -.skills-monitor-page .skills-monitor-table th:not(:first-child), -.skills-monitor-page .skills-monitor-table td:not(:first-child) { - text-align: center; -} - -.skills-monitor-page .skills-monitor-table th:last-child, -.skills-monitor-page .skills-monitor-table td:last-child { - text-align: left; -} - -.skills-monitor-page .skills-monitor-table td { - padding: 14px 16px; - border-color: rgba(15, 23, 42, 0.06); - white-space: nowrap; -} - -.skills-monitor-page .skills-monitor-table td:first-child { - color: #1f2937; -} - -.skills-monitor-page .skills-monitor-table tbody tr { - transition: background 0.15s ease, box-shadow 0.15s ease; -} - -.skills-monitor-page .skills-monitor-table tbody tr:hover { - background: rgba(37, 99, 235, 0.055); - box-shadow: inset 3px 0 0 rgba(37, 99, 235, 0.56); -} - -.skills-monitor-page .skills-table-pill, -.skills-monitor-page .skills-table-number { +.skills-monitor-page .skills-rate-cell { display: inline-flex; align-items: center; - justify-content: center; - min-width: 34px; - height: 26px; - padding: 0 10px; - border-radius: 999px; - background: rgba(15, 23, 42, 0.05); - font-weight: 700; - font-variant-numeric: tabular-nums; -} - -.skills-monitor-page .skills-table-number.is-success { - background: rgba(22, 163, 74, 0.1); -} - -.skills-monitor-page .skills-table-number.is-danger { - background: rgba(225, 29, 72, 0.1); -} - -.skills-monitor-page .skills-rate-cell { - display: inline-grid; - grid-template-columns: 52px 76px; - align-items: center; gap: 10px; - color: var(--text-primary); - font-weight: 650; - font-variant-numeric: tabular-nums; + font-weight: 500; } .skills-monitor-page .skills-rate-track { display: block; - height: 7px; + width: 76px; + height: 6px; border-radius: 999px; - background: rgba(15, 23, 42, 0.08); + background: var(--border-color); overflow: hidden; } .skills-monitor-page .skills-rate-track span { display: block; height: 100%; - min-width: 3px; + min-width: 2px; border-radius: inherit; - background: linear-gradient(90deg, var(--skills-blue), var(--skills-success)); + background: var(--success-color); } .skills-monitor-page .skills-last-call { color: var(--text-secondary); - font-variant-numeric: tabular-nums; -} - -.skills-monitor-page .monitor-empty { - border: 1px dashed rgba(37, 99, 235, 0.18); - border-radius: 14px; - background: rgba(37, 99, 235, 0.035); -} - -@media (max-width: 1280px) { - .skills-monitor-page .monitor-stats-grid { - grid-template-columns: repeat(3, minmax(180px, 1fr)); - } -} - -@media (max-width: 760px) { - .skills-monitor-page .page-content { - padding: 16px; - } - - .skills-monitor-page .monitor-stats-grid { - grid-template-columns: 1fr; - } -} - -html[data-theme="dark"] .skills-monitor-page { - --skills-blue: #60a5fa; - --skills-cyan: #22d3ee; - --skills-success: #34d399; - --skills-danger: #fb7185; - --skills-card-shadow: 0 14px 34px rgba(0, 0, 0, 0.34); -} - -html[data-theme="dark"] .skills-monitor-page .page-content { - background: - linear-gradient(180deg, rgba(37, 99, 235, 0.16) 0%, rgba(11, 17, 32, 0) 280px), - var(--bg-secondary); -} - -html[data-theme="dark"] .skills-monitor-page .page-header { - background: rgba(17, 24, 39, 0.88); -} - -html[data-theme="dark"] .skills-monitor-page .monitor-section { - border-color: rgba(96, 165, 250, 0.18); -} - -html[data-theme="dark"] .skills-monitor-page .monitor-section .section-header { - background: transparent; - border-color: transparent; -} - -html[data-theme="dark"] .skills-monitor-page .skills-stat-card { - border-color: rgba(96, 165, 250, 0.2); - background: - linear-gradient(145deg, rgba(17, 24, 39, 0.96), rgba(15, 23, 42, 0.94)), - radial-gradient(circle at 100% 0%, rgba(96, 165, 250, 0.2), transparent 34%); -} - -html[data-theme="dark"] .skills-monitor-page .monitor-stat-label, -html[data-theme="dark"] .skills-monitor-page .skills-monitor-table th { - color: #94a3b8; -} - -html[data-theme="dark"] .skills-monitor-page .monitor-stat-value, -html[data-theme="dark"] .skills-monitor-page .skills-monitor-table td:first-child { - color: var(--text-primary); -} - -html[data-theme="dark"] .skills-monitor-page .skills-monitor-table thead { - background: linear-gradient(180deg, #172033 0%, #111827 100%); -} - -html[data-theme="dark"] .skills-monitor-page #skills-monitor-list, -html[data-theme="dark"] .skills-monitor-page .skills-monitor-table td { - border-color: rgba(148, 163, 184, 0.16); -} - -html[data-theme="dark"] .skills-monitor-page .skills-table-pill { - background: rgba(148, 163, 184, 0.12); -} - -html[data-theme="dark"] .skills-monitor-page .skills-rate-track { - background: rgba(148, 163, 184, 0.16); -} - -html[data-theme="dark"] .skills-monitor-page .monitor-empty { - border-color: rgba(96, 165, 250, 0.22); - background: rgba(96, 165, 250, 0.08); } /* ==================== 信息收集(FOFA)页面 ==================== */ @@ -36624,34 +36575,6 @@ html[data-theme="dark"] .form-group select { font-size: 0.6875rem; } -.rbac-advanced-assignment { - margin-bottom: 14px; - border: 1px solid var(--border-color); - border-radius: 8px; - background: var(--bg-secondary); -} - -.rbac-advanced-assignment summary { - padding: 9px 11px; - color: var(--text-secondary); - font-size: 0.75rem; - cursor: pointer; -} - -.rbac-advanced-assignment > div { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 10px; - padding: 0 11px 11px; -} - -.rbac-advanced-assignment span { - max-width: 280px; - color: var(--text-secondary); - font-size: 0.6875rem; -} - .rbac-assignment-list-head { display: flex; align-items: center; @@ -36678,9 +36601,56 @@ html[data-theme="dark"] .form-group select { } .rbac-metric-context { - margin: -4px 0 10px; + display: inline-flex; + flex: 1 1 160px; + align-items: center; + gap: 7px; + min-width: 0; + max-width: min(25%, 360px); + min-height: 36px; + margin: 0; + padding: 5px 7px 5px 10px; + border: 1px solid var(--border-color); + border-radius: 999px; + background: var(--bg-primary); color: var(--text-secondary); font-size: 0.75rem; + white-space: nowrap; + overflow: hidden; +} + +.rbac-metric-context[hidden] { + display: none !important; +} + +.rbac-context-label { + flex: 0 0 auto; + color: var(--text-muted); +} + +.rbac-context-member { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; + color: var(--text-primary); + font-size: 0.8125rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rbac-context-stat { + display: inline-flex; + flex: 0 0 auto; + align-items: baseline; + gap: 3px; + padding: 3px 7px; + border-radius: 999px; + background: var(--bg-secondary); +} + +.rbac-context-stat b { + color: var(--accent-color); + font-size: 0.8125rem; } .rbac-metric-enabled[hidden] { @@ -36825,31 +36795,246 @@ html[data-theme="dark"] .form-group select { .rbac-audit-list { display: flex; + flex: 1 1 auto; flex-direction: column; + gap: 0; + min-height: 0; + overflow: hidden; + overscroll-behavior: contain; +} + +.rbac-panel-detail #rbac-tab-audit.rbac-tab-panel:not([hidden]) { + container-name: rbac-audit; + container-type: inline-size; + display: flex; + flex-direction: column; + min-height: 0; + overflow: hidden; +} + +#rbac-tab-audit .rbac-audit-toolbar, +#rbac-tab-audit .rbac-pagination { + flex: 0 0 auto; +} + +.rbac-audit-table { + display: flex; + flex: 1 1 auto; + flex-direction: column; + width: 100%; + min-width: 0; + min-height: 0; +} + +.rbac-audit-table-body { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + overflow-x: hidden; + overscroll-behavior: contain; + scrollbar-width: thin; + scrollbar-color: color-mix(in srgb, var(--text-muted) 60%, transparent) transparent; +} + +.rbac-audit-table-body::-webkit-scrollbar { + width: 6px; +} + +.rbac-audit-table-body::-webkit-scrollbar-thumb { + border-radius: 999px; + background: color-mix(in srgb, var(--text-muted) 52%, transparent); +} + +.rbac-audit-toolbar { + display: grid; + grid-template-columns: minmax(220px, 1fr) auto; + align-items: end; + gap: 20px; + margin-bottom: 12px; + padding: 10px 12px; + border: 1px solid var(--border-color); + border-radius: 8px; + background: color-mix(in srgb, var(--bg-secondary) 72%, var(--bg-primary)); +} + +.rbac-audit-heading { + min-width: 0; + padding-bottom: 2px; +} + +.rbac-audit-heading strong { + display: block; +} + +.rbac-audit-heading .rbac-muted { + margin-top: 4px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rbac-audit-filterbar { + display: grid; + grid-template-columns: 200px 220px; + justify-content: end; + gap: 10px; + min-width: 0; +} + +.rbac-audit-filter { + display: flex; + flex-direction: column; + gap: 6px; + min-width: 0; +} + +.rbac-audit-filter > span { + color: var(--text-secondary); + font-size: 0.71875rem; + font-weight: 600; +} + +.rbac-audit-filter .settings-custom-select, +.rbac-audit-filter .form-select, +.rbac-audit-filter .form-input { + width: 100%; + min-width: 0; +} + +.rbac-audit-filter .settings-custom-select-trigger, +.rbac-audit-filter .form-select, +.rbac-audit-filter .form-input { + min-height: 38px; + height: 38px; +} + +.rbac-audit-pagination { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + min-height: 54px; + margin: 10px 0 0; + padding: 8px 12px; + border: 1px solid var(--border-color); + border-radius: 10px; +} + +.rbac-audit-pagination-meta, +.rbac-audit-pagination-nav, +.rbac-audit-page-size-control { + display: flex; + align-items: center; +} + +.rbac-audit-pagination-meta { + gap: 16px; + min-width: 0; + color: var(--text-secondary); + font-size: 0.75rem; +} + +.rbac-audit-page-size-control { gap: 8px; - min-height: 220px; + white-space: nowrap; +} + +.rbac-audit-page-size-control .settings-custom-select, +.rbac-audit-page-size-control .form-select { + width: 82px; + min-width: 82px; +} + +.rbac-audit-page-size-control .settings-custom-select-trigger, +.rbac-audit-page-size-control .form-select { + min-height: 34px; + height: 34px; +} + +.rbac-audit-page-size-control .settings-custom-select-menu { + top: auto; + bottom: calc(100% + 6px); +} + +.rbac-audit-pagination-nav { + flex: 0 0 auto; + justify-content: flex-end; + gap: 8px; +} + +.rbac-audit-pagination-nav > span { + min-width: 76px; + color: var(--text-secondary); + font-size: 0.75rem; + text-align: center; + white-space: nowrap; +} + +@container rbac-page (max-width: 900px) { + .rbac-audit-pagination { + align-items: stretch; + flex-direction: column; + } + + .rbac-audit-pagination-meta, + .rbac-audit-pagination-nav { + justify-content: space-between; + } +} + +.rbac-audit-table-head, +.rbac-audit-row { + display: grid; + grid-template-columns: + minmax(130px, 0.85fr) + minmax(90px, 0.55fr) + minmax(80px, 0.5fr) + minmax(320px, 2.1fr) + minmax(180px, max-content); + align-items: center; + gap: 12px; + width: 100%; + min-width: 0; + box-sizing: border-box; +} + +.rbac-audit-table-head { + padding: 9px 12px; + border: 1px solid var(--border-color); + border-bottom: 0; + border-radius: 8px 8px 0 0; + background: var(--bg-secondary); + color: var(--text-secondary); + font-size: 0.75rem; + font-weight: 600; } .rbac-audit-row { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; padding: 8px 12px; border: 1px solid var(--border-color); - border-radius: 8px; + border-bottom: 0; + border-radius: 0; background: var(--bg-primary); font-size: 0.8125rem; line-height: 1.4; } +.rbac-audit-row:last-child { + border-bottom: 1px solid var(--border-color); + border-radius: 0 0 8px 8px; +} + .rbac-audit-action { flex: 0 0 auto; font-weight: 600; } .rbac-audit-actor { - flex: 0 0 auto; + justify-self: start; +} + +.rbac-audit-cell-value { + min-width: 0; } .rbac-audit-detail { @@ -36862,14 +37047,133 @@ html[data-theme="dark"] .form-group select { font-size: 0.75rem; } +.rbac-audit-resource-id { + overflow: visible; + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + text-overflow: clip; +} + .rbac-audit-time { - flex: 0 0 auto; - margin-left: auto; color: var(--text-secondary); font-size: 0.75rem; white-space: nowrap; } +@container rbac-audit (max-width: 1000px) { + .rbac-audit-table-head, + .rbac-audit-row { + grid-template-columns: + minmax(110px, 0.82fr) + minmax(72px, 0.5fr) + minmax(62px, 0.42fr) + minmax(230px, 1.9fr) + minmax(145px, 0.95fr); + gap: 8px; + padding-right: 10px; + padding-left: 10px; + } + + .rbac-audit-resource-id { + overflow-wrap: anywhere; + white-space: normal; + } +} + +@container rbac-audit (max-width: 740px) { + .rbac-audit-table-head { + display: none; + } + + .rbac-audit-table-body { + display: flex; + flex-direction: column; + gap: 8px; + } + + .rbac-audit-row, + .rbac-audit-row:last-child { + flex: 0 0 auto; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px 16px; + padding: 12px; + border: 1px solid var(--border-color); + border-radius: 8px; + } + + .rbac-audit-row > [role="cell"] { + display: grid; + grid-template-columns: 68px minmax(0, 1fr); + align-items: center; + gap: 8px; + min-width: 0; + overflow: visible; + white-space: normal; + } + + .rbac-audit-row > [role="cell"]::before { + content: attr(data-label); + color: var(--text-muted); + font-family: inherit; + font-size: 0.6875rem; + font-weight: 500; + line-height: 1.2; + } + + .rbac-audit-row .rbac-audit-actor { + justify-self: stretch; + } + + .rbac-audit-row .rbac-audit-resource-id { + grid-column: 1 / -1; + overflow-wrap: anywhere; + } + + .rbac-audit-pagination { + align-items: stretch; + flex-direction: column; + } + + .rbac-audit-pagination-meta, + .rbac-audit-pagination-nav { + justify-content: space-between; + } +} + +@container rbac-audit (max-width: 620px) { + .rbac-audit-toolbar { + grid-template-columns: minmax(0, 1fr); + align-items: stretch; + gap: 10px; + } + + .rbac-audit-filterbar { + grid-template-columns: repeat(2, minmax(0, 1fr)); + justify-content: stretch; + width: 100%; + } +} + +@container rbac-audit (max-width: 520px) { + .rbac-audit-filterbar { + grid-template-columns: minmax(0, 1fr); + } + + .rbac-audit-row, + .rbac-audit-row:last-child { + grid-template-columns: minmax(0, 1fr); + } + + .rbac-audit-row .rbac-audit-resource-id { + grid-column: auto; + } + + .rbac-audit-pagination-meta, + .rbac-audit-pagination-nav { + flex-wrap: wrap; + gap: 8px; + } +} + .rbac-user-roles-readonly { display: flex; flex-direction: column; @@ -37270,22 +37574,34 @@ html[data-theme="dark"] .form-group select { .rbac-panel-detail .rbac-detail-head h3 { display: flex; + min-width: 0; align-items: baseline; - flex-wrap: wrap; + flex-wrap: nowrap; gap: 8px; margin: 0; + overflow: hidden; font-size: 0.9375rem; line-height: 1.3; } .rbac-detail-name { + flex: 0 1 auto; + min-width: 0; + overflow: hidden; font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; } .rbac-detail-meta { + flex: 1 1 auto; + min-width: 0; + overflow: hidden; color: var(--text-secondary); font-size: 0.8125rem; font-weight: 500; + text-overflow: ellipsis; + white-space: nowrap; } .rbac-panel-detail .rbac-detail-head .rbac-muted[hidden] { @@ -37305,7 +37621,9 @@ html[data-theme="dark"] .form-group select { .rbac-detail-head > div:first-child, .rbac-panel-head > div:first-child { + flex: 1 1 auto; min-width: 0; + overflow: hidden; } .rbac-detail-actions { @@ -37472,7 +37790,7 @@ html[data-theme="dark"] .form-group select { .rbac-assignment-toolbar-main { display: grid; - grid-template-columns: minmax(140px, 180px) minmax(220px, 1fr); + grid-template-columns: minmax(140px, 180px) minmax(280px, 1fr); align-items: end; gap: 10px; } @@ -37513,13 +37831,180 @@ html[data-theme="dark"] .form-group select { .rbac-resource-picker { display: flex; + flex: 1 1 auto; flex-direction: column; - gap: 6px; + gap: 0; min-height: 0; + max-height: none; + margin: 0; + padding: 0 12px; overflow-y: auto; overscroll-behavior: contain; } +.rbac-resource-picker .rbac-resource-option { + display: grid; + grid-template-columns: auto minmax(120px, 1fr) minmax(90px, 0.75fr) minmax(60px, 0.55fr) max-content; + gap: 10px; + height: 58px; + min-height: 58px; + padding: 8px 2px; + box-sizing: border-box; + border: 0; + border-bottom: 1px solid color-mix(in srgb, var(--border-color) 78%, transparent); + border-radius: 0; + background: transparent; +} + +.rbac-resource-picker .rbac-resource-option:last-child { + border-bottom: 0; +} + +.rbac-resource-picker .rbac-resource-option:hover:not(.is-assigned) { + background: color-mix(in srgb, var(--accent-color) 3%, var(--bg-primary)); +} + +.rbac-resource-picker .rbac-resource-option:has(input:checked) { + background: color-mix(in srgb, var(--accent-color) 6%, var(--bg-primary)); +} + +.rbac-resource-picker .rbac-resource-option.is-assigned { + opacity: 1; +} + +.rbac-resource-picker .rbac-resource-option.is-assigned .rbac-resource-option-main { + opacity: 0.72; +} + +.rbac-resource-option-id { + min-width: 0; + overflow: hidden; + color: var(--text-secondary); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.75rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rbac-resource-copy { + min-height: 28px; + padding: 3px 6px; + font-size: 0.75rem; +} + +.rbac-assignments-list { + flex: 1 1 auto; + gap: 0; + min-height: 0; + max-height: none; + padding: 0 12px; +} + +.rbac-assignments-list .rbac-assignment-row { + display: grid; + grid-template-columns: minmax(150px, 1.25fr) minmax(100px, 0.8fr) minmax(80px, 0.7fr) max-content; + align-items: center; + gap: 12px; + height: 58px; + min-height: 58px; + padding: 8px 10px; + box-sizing: border-box; + border: 0; + border-bottom: 1px solid color-mix(in srgb, var(--border-color) 78%, transparent); + border-radius: 0; + background: transparent; +} + +.rbac-assignments-list .rbac-assignment-row:last-child { + border-bottom: 0; +} + +.rbac-assignments-list .rbac-assignment-row:hover { + background: color-mix(in srgb, var(--accent-color) 3%, var(--bg-primary)); +} + +.rbac-assignment-identity { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; +} + +.rbac-assignment-identity strong { + overflow: hidden; + font-size: 0.8125rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rbac-assignments-list .rbac-assignment-id { + min-width: 0; + overflow: hidden; + color: var(--text-secondary); + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 0.75rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rbac-assignment-detail { + overflow: hidden; + color: var(--text-secondary); + font-size: 0.75rem; + text-overflow: ellipsis; + white-space: nowrap; +} + +.rbac-assignment-actions { + display: flex; + align-items: center; + gap: 4px; +} + +.rbac-assignment-actions .btn-link { + min-height: 28px; + padding: 3px 6px; + font-size: 0.75rem; +} + +.rbac-assignment-actions .btn-link.is-danger { + color: var(--danger-color, #dc2626); +} + +@container rbac-page (max-width: 720px) { + .rbac-resource-picker .rbac-resource-option { + grid-template-columns: auto minmax(0, 1fr) max-content; + height: auto; + min-height: 72px; + } + + .rbac-resource-option-id, + .rbac-resource-option-detail { + grid-column: 2; + } + + .rbac-resource-copy { + grid-column: 3; + grid-row: 1 / span 3; + } + + .rbac-assignments-list .rbac-assignment-row { + grid-template-columns: minmax(0, 1fr) max-content; + height: auto; + min-height: 72px; + } + + .rbac-assignment-id, + .rbac-assignment-detail { + grid-column: 1; + } + + .rbac-assignment-actions { + grid-column: 2; + grid-row: 1 / span 3; + } +} + .rbac-picker-status { min-height: 100px; display: flex; @@ -37527,10 +38012,6 @@ html[data-theme="dark"] .form-group select { justify-content: center; } -.rbac-advanced-assignment > div { - grid-template-columns: minmax(0, 1fr) minmax(180px, 280px); -} - .rbac-assignment-list-head { padding-top: 3px; } @@ -37636,7 +38117,7 @@ html[data-theme="dark"] .form-group select { } .rbac-assignment-toolbar-main { - grid-template-columns: minmax(130px, 170px) minmax(180px, 1fr); + grid-template-columns: minmax(120px, 155px) minmax(220px, 1fr); } .rbac-assignment-toolbar-main .btn-primary { @@ -37655,6 +38136,10 @@ html[data-theme="dark"] .form-group select { width: min(100%, 540px); } + .rbac-resource-filterbar { + grid-template-columns: minmax(0, 1fr); + } + .rbac-overview { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -37685,6 +38170,11 @@ html[data-theme="dark"] .form-group select { width: 100%; grid-template-columns: minmax(0, 1fr) max-content; } + + .rbac-assignment-toolbar-main { + grid-template-columns: minmax(120px, 160px) minmax(0, 1fr); + } + } @container rbac-page (max-width: 560px) { @@ -37714,14 +38204,9 @@ html[data-theme="dark"] .form-group select { width: 100%; } - .rbac-advanced-assignment-body, #rbac-role-modal .rbac-permission-tools { grid-template-columns: minmax(0, 1fr); } - - .rbac-advanced-assignment span { - max-width: none; - } } @media (max-width: 720px) { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 2ffa8224..45108c9a 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -3714,6 +3714,28 @@ "auditHint": "Audit events related to this member's platform access.", "unknownActor": "Unknown actor", "metricUserContext": "Selected member: {{name}} · {{roles}} roles · {{grants}} grants", + "currentMemberLabel": "Selected member", + "auditPageSizeLabel": "Rows per page", + "auditPageSize20": "20 / page", + "auditPageSize50": "50 / page", + "auditPageSize100": "100 / page", + "auditFilters": { + "filterBarLabel": "Change history filters", + "actionLabel": "Filter by action", + "allActions": "All actions", + "actorPlaceholder": "Actor username", + "resourceTypeLabel": "Filter by resource type", + "allResourceTypes": "All resource types" + }, + "unknownTarget": "Unknown member", + "auditColumns": { + "action": "Action", + "actor": "Actor", + "target": "Target member", + "resourceType": "Resource type", + "resourceId": "Resource ID", + "time": "Time" + }, "deleteUserConfirmDetailed": "Delete member {{name}} (@{{username}})?\n\nThis will revoke active sessions and remove {{roles}} role bindings and {{grants}} resource grants.", "auditActions": { "create_user": "Create platform user", @@ -3730,13 +3752,19 @@ "resourceIdLabel": "ID", "resourceDetailProject": "Project {{id}}", "pagination": { + "first": "First", "previous": "Previous", "next": "Next", + "last": "Last", + "rowsPerPage": "Rows per page", + "recordRange": "Showing {{start}}-{{end}} of {{total}} records", + "pageIndicator": "Page {{page}} / {{pages}}", "resourcePage": "Page {{page}}", "pageSummary": "Page {{page}} / {{pages}} · {{total}} items", "emptySummary": "0 items" }, "resourceTypes": { + "user": "Platform user", "project": "Project", "conversation": "Conversation", "vulnerability": "Vulnerability", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 3aecbf16..c6b4d444 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -3702,6 +3702,28 @@ "auditHint": "展示该成员相关的平台权限操作审计。", "unknownActor": "未知操作者", "metricUserContext": "当前成员:{{name}} · {{roles}} 个角色 · {{grants}} 项授权", + "currentMemberLabel": "当前成员", + "auditPageSizeLabel": "每页显示条数", + "auditPageSize20": "20 条/页", + "auditPageSize50": "50 条/页", + "auditPageSize100": "100 条/页", + "auditFilters": { + "filterBarLabel": "变更记录筛选", + "actionLabel": "按操作筛选", + "allActions": "全部操作", + "actorPlaceholder": "操作人账号", + "resourceTypeLabel": "按资源类型筛选", + "allResourceTypes": "全部资源类型" + }, + "unknownTarget": "未知成员", + "auditColumns": { + "action": "操作", + "actor": "操作人", + "target": "操作对象", + "resourceType": "资源类型", + "resourceId": "资源 ID", + "time": "操作时间" + }, "deleteUserConfirmDetailed": "确认删除成员 {{name}}(@{{username}})?\n\n将立即吊销其会话,并清除 {{roles}} 个角色绑定与 {{grants}} 项资源授权。", "auditActions": { "create_user": "创建平台用户", @@ -3718,13 +3740,19 @@ "resourceIdLabel": "ID", "resourceDetailProject": "所属项目 {{id}}", "pagination": { + "first": "首页", "previous": "上一页", "next": "下一页", + "last": "末页", + "rowsPerPage": "每页显示", + "recordRange": "显示 {{start}}-{{end}} / 共 {{total}} 条记录", + "pageIndicator": "第 {{page}} / {{pages}} 页", "resourcePage": "第 {{page}} 页", "pageSummary": "第 {{page}} / {{pages}} 页 · 共 {{total}} 项", "emptySummary": "共 0 项" }, "resourceTypes": { + "user": "平台用户", "project": "项目", "conversation": "对话", "vulnerability": "漏洞", diff --git a/web/static/js/chat.js b/web/static/js/chat.js index f3b4301c..0ab00d67 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -2178,19 +2178,20 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr timeDiv.dataset.messageTime = messageTime.toISOString(); } catch (e) { /* ignore */ } contentWrapper.appendChild(timeDiv); + messageDiv.appendChild(contentWrapper); // 有 MCP 执行记录且非流式占位消息时展示调用按钮;带 progressId 的流式占位不挂此条(与进度卡片一致,结束时 integrate 再创建) if (role === 'assistant' && (mcpExecutionIds && Array.isArray(mcpExecutionIds) && mcpExecutionIds.length > 0) && !progressId) { if (options && options.deferMcpButtons) { try { - messageDiv.dataset.pendingMcpExecutionIds = JSON.stringify(mcpExecutionIds); + const ids = cacheMcpExecutionIds(messageDiv, mcpExecutionIds); + messageDiv.dataset.pendingMcpExecutionIds = JSON.stringify(ids); } catch (e) { /* ignore */ } } else { - appendMcpCallButtons(messageDiv, mcpExecutionIds); + setMcpCallExecutionIds(messageDiv, mcpExecutionIds); } } - messageDiv.appendChild(contentWrapper); // 标记「系统就绪」占位消息,便于切换语言后刷新文案 if (options && options.systemReadyMessage) { messageDiv.setAttribute('data-system-ready-message', '1'); @@ -2935,23 +2936,6 @@ window.addEventListener('beforeunload', () => { } }); -// 异步获取工具名称并更新按钮文本 -async function updateButtonWithToolName(button, executionId, index) { - try { - const response = await apiFetch(`/api/monitor/execution/${executionId}`); - if (response.ok) { - const exec = await response.json(); - const toolName = exec.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); - // 格式化工具名称(如果是 name::toolName 格式,只显示 toolName 部分) - const displayToolName = toolName.includes('::') ? toolName.split('::')[1] : toolName; - button.querySelector('span').textContent = `${displayToolName} #${index}`; - } - } catch (error) { - // 如果获取失败,保持原有文本不变 - console.error('获取工具名称失败:', error); - } -} - function getPendingMcpExecutionCount(messageElement) { if (!messageElement || !messageElement.dataset || !messageElement.dataset.pendingMcpExecutionIds) { return 0; @@ -2983,7 +2967,7 @@ function getMcpExecutionCount(messageElement) { if (pendingSummaries > 0) return pendingSummaries; const toolList = messageElement && messageElement.querySelector('.mcp-tool-list'); if (toolList) { - const rendered = toolList.querySelectorAll('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]').length; + const rendered = toolList.querySelectorAll('.mcp-detail-btn').length; if (rendered > 0) return rendered; } if (messageElement && messageElement.dataset && messageElement.dataset.mcpExecutionCount) { @@ -3013,16 +2997,43 @@ function collectMcpExecutionIdsFromProcessDetails(processDetails) { return ids; } +function normalizeMcpExecutionIds(executionIds) { + if (!Array.isArray(executionIds)) return []; + const seen = new Set(); + return executionIds.reduce((ids, value) => { + const id = value == null ? '' : String(value).trim(); + if (id && !seen.has(id)) { + seen.add(id); + ids.push(id); + } + return ids; + }, []); +} + +function cacheMcpExecutionIds(messageElement, executionIds) { + if (!messageElement || !messageElement.dataset) return []; + const ids = normalizeMcpExecutionIds(executionIds); + if (ids.length > 0) { + messageElement.dataset.mcpExecutionIds = JSON.stringify(ids); + } else { + delete messageElement.dataset.mcpExecutionIds; + } + return ids; +} + +function getCachedMcpExecutionIds(messageElement) { + if (!messageElement || !messageElement.dataset || !messageElement.dataset.mcpExecutionIds) return []; + try { + return normalizeMcpExecutionIds(JSON.parse(messageElement.dataset.mcpExecutionIds)); + } catch (e) { + delete messageElement.dataset.mcpExecutionIds; + return []; + } +} + function setPendingMcpExecutionIds(messageElement, executionIds) { if (!messageElement || !messageElement.dataset || !Array.isArray(executionIds)) return; - const seen = new Set(); - const ids = []; - executionIds.forEach((value) => { - const id = value == null ? '' : String(value).trim(); - if (!id || seen.has(id)) return; - seen.add(id); - ids.push(id); - }); + const ids = cacheMcpExecutionIds(messageElement, executionIds); if (ids.length > 0) { messageElement.dataset.pendingMcpExecutionIds = JSON.stringify(ids); } else { @@ -3051,6 +3062,8 @@ function cacheToolExecutionSummaries(messageElement, summaries) { .filter((item) => item.toolName || item.executionId || item.toolCallId); if (normalized.length > 0) { messageElement.dataset.toolExecutionSummaries = JSON.stringify(normalized); + } else { + delete messageElement.dataset.toolExecutionSummaries; } return normalized; } @@ -3075,8 +3088,8 @@ function setPendingToolExecutionSummaries(messageElement, summaries) { delete messageElement.dataset.pendingToolExecutionSummaries; } const renderedToolList = messageElement.querySelector('.mcp-tool-list'); - if (normalized.length > 0 && renderedToolList && renderedToolList.querySelector('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]')) { - appendMcpCallSummaryButtons(messageElement, normalized); + if (normalized.length > 0 && renderedToolList && renderedToolList.querySelector('.mcp-detail-btn')) { + setMcpCallSummaries(messageElement, normalized); delete messageElement.dataset.pendingToolExecutionSummaries; delete messageElement.dataset.pendingMcpExecutionIds; } @@ -3136,17 +3149,10 @@ function ensureMcpCallSectionChrome(messageElement, messageId) { } let toolbar = mcpSection.querySelector('.mcp-call-toolbar'); - const legacyButtons = mcpSection.querySelector('.mcp-call-buttons'); if (!toolbar) { toolbar = document.createElement('div'); toolbar.className = 'mcp-call-toolbar'; - if (legacyButtons) { - const processBtn = legacyButtons.querySelector('.process-detail-btn'); - if (processBtn) toolbar.appendChild(processBtn); - mcpSection.replaceChild(toolbar, legacyButtons); - } else { - mcpSection.appendChild(toolbar); - } + mcpSection.appendChild(toolbar); } let toolList = mcpSection.querySelector('.mcp-tool-list'); @@ -3161,11 +3167,6 @@ function ensureMcpCallSectionChrome(messageElement, messageId) { } } - if (legacyButtons && legacyButtons.parentNode === mcpSection) { - legacyButtons.querySelectorAll('.mcp-detail-btn[data-exec-id]').forEach((btn) => toolList.appendChild(btn)); - legacyButtons.remove(); - } - const clientId = messageId || messageElement.id; if (clientId && !toolbar.querySelector('.process-detail-btn')) { const processDetailBtn = document.createElement('button'); @@ -3310,7 +3311,7 @@ function toggleMcpToolList(assistantMessageId) { if ( !getPendingMcpExecutionCount(messageEl) && !getPendingToolExecutionSummaryCount(messageEl) && - !toolList.querySelector('.mcp-detail-btn[data-exec-id], .mcp-detail-btn[data-tool-summary]') + !toolList.querySelector('.mcp-detail-btn') ) { if (typeof toggleProcessDetails === 'function') { toggleProcessDetails(null, assistantMessageId); @@ -3319,7 +3320,7 @@ function toggleMcpToolList(assistantMessageId) { } const willExpand = !toolList.classList.contains('expanded'); if (willExpand) { - ensureMcpCallButtons(messageEl); + renderPendingMcpCallButtons(messageEl); toolList.classList.add('expanded'); } else { toolList.classList.remove('expanded'); @@ -3336,85 +3337,78 @@ window.setMcpExecutionSummaryCount = setMcpExecutionSummaryCount; window.setPendingMcpExecutionIds = setPendingMcpExecutionIds; window.setPendingToolExecutionSummaries = setPendingToolExecutionSummaries; -/** 将 MCP 工具按钮挂到独立工具列表,并批量解析工具名 */ -function appendMcpCallButtons(messageElement, executionIds) { - if (!messageElement || !Array.isArray(executionIds) || executionIds.length === 0) { - return; - } +/** + * 声明式渲染工具调用列表。 + * 过程摘要是展示与定位的唯一模型;executionIds 仅在摘要尚未到达时提供占位。 + * 每次更新整体替换列表,避免增量追加产生双重状态。 + */ +function renderMcpCallButtons(messageElement) { + if (!messageElement) return; const chrome = ensureMcpCallSectionChrome(messageElement, messageElement.id); if (!chrome) return; const toolList = chrome.toolList; + const executionIds = getCachedMcpExecutionIds(messageElement); + const summaries = getCachedToolExecutionSummaries(messageElement); + const items = summaries.length > 0 + ? summaries + : executionIds.map((executionId) => normalizeToolExecutionSummaryForButton({ executionId })); - executionIds.forEach((execId, index) => { - if (toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(execId)) + '"]')) { - return; - } - const detailBtn = document.createElement('button'); - detailBtn.className = 'mcp-detail-btn'; - detailBtn.dataset.execId = execId; - detailBtn.dataset.execIndex = String(index + 1); - detailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + ''; - detailBtn.onclick = async () => { - const summary = await resolveToolExecutionSummaryForFocus(messageElement, execId, index); - if (summary && (summary.processDetailId || summary.toolCallId)) { - await focusToolExecutionInProcessDetails(messageElement, summary, index); - return; - } - showMCPDetail(execId); - }; - toolList.appendChild(detailBtn); - }); - batchUpdateButtonToolNames(toolList, executionIds); - syncMcpToolsToggleButton(messageElement); -} - -function appendMcpCallSummaryButtons(messageElement, summaries) { - if (!messageElement || !Array.isArray(summaries) || summaries.length === 0) { - return; - } - const chrome = ensureMcpCallSectionChrome(messageElement, messageElement.id); - if (!chrome) return; - const toolList = chrome.toolList; - summaries.forEach((raw, index) => { - const item = normalizeToolExecutionSummaryForButton(raw); - const key = item.executionId || item.toolCallId || `${item.toolName || 'tool'}-${index + 1}`; - const selector = '.mcp-detail-btn[data-tool-summary="' + CSS.escape(String(key)) + '"]'; - const existingSummaryBtn = toolList.querySelector(selector); - const existingExecBtn = item.executionId - ? toolList.querySelector('.mcp-detail-btn[data-exec-id="' + CSS.escape(String(item.executionId)) + '"]') - : null; - if (existingSummaryBtn) { - return; - } - // 历史会话可能先按 executionId 渲染旧按钮,随后摘要才异步到达。 - // 复用并升级该按钮,避免它永久保留“只看执行详情、不定位上下文”的旧处理器。 - const btn = existingExecBtn || document.createElement('button'); + const renderVersion = String((parseInt(toolList.dataset.renderVersion, 10) || 0) + 1); + toolList.dataset.renderVersion = renderVersion; + const fragment = document.createDocumentFragment(); + items.forEach((item, index) => { + const btn = document.createElement('button'); + btn.type = 'button'; btn.className = 'mcp-detail-btn'; - btn.dataset.toolSummary = key; + btn.dataset.execIndex = String(index + 1); if (item.executionId) { btn.dataset.execId = item.executionId; } - if (item.processDetailId || item.toolCallId) { - btn.onclick = async () => { - await focusToolExecutionInProcessDetails(messageElement, item, index); - }; - } else if (item.executionId) { - btn.onclick = () => showMCPDetail(item.executionId); + if (item.toolCallId) { + btn.dataset.toolCallId = item.toolCallId; + } + btn.onclick = async () => { + let focusItem = item; + if (!focusItem.processDetailId && !focusItem.toolCallId && focusItem.executionId) { + focusItem = await resolveToolExecutionSummaryForFocus( + messageElement, + focusItem.executionId, + index + ) || focusItem; + } + await focusToolExecutionInProcessDetails(messageElement, focusItem, index); + }; + if (item.toolName) { + renderToolExecutionButtonContent(btn, item.toolName, String(index + 1), item.status); } else { - btn.onclick = async () => { - await focusToolExecutionInProcessDetails(messageElement, item, index); - }; - } - renderToolExecutionButtonContent(btn, item.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'), String(index + 1), item.status); - if (!existingExecBtn) { - toolList.appendChild(btn); + btn.innerHTML = '' + (typeof window.t === 'function' + ? window.t('chat.callNumber', { n: index + 1 }) + : '调用 #' + (index + 1)) + ''; } + fragment.appendChild(btn); }); + toolList.replaceChildren(fragment); + + if (summaries.length === 0 && executionIds.length > 0) { + batchUpdateButtonToolNames(toolList, executionIds, renderVersion); + } syncMcpToolsToggleButton(messageElement); } -/** 历史会话懒加载:用户展开工具列表时再渲染工具按钮 */ -function ensureMcpCallButtons(messageElement) { +function setMcpCallExecutionIds(messageElement, executionIds) { + if (!messageElement || !Array.isArray(executionIds)) return; + cacheMcpExecutionIds(messageElement, executionIds); + renderMcpCallButtons(messageElement); +} + +function setMcpCallSummaries(messageElement, summaries) { + if (!messageElement || !Array.isArray(summaries)) return; + cacheToolExecutionSummaries(messageElement, summaries); + renderMcpCallButtons(messageElement); +} + +/** 懒加载:用户展开工具列表时提交待渲染的数据模型。 */ +function renderPendingMcpCallButtons(messageElement) { if (!messageElement || !messageElement.dataset) { return; } @@ -3428,7 +3422,7 @@ function ensureMcpCallButtons(messageElement) { summaries = []; } if (Array.isArray(summaries) && summaries.length > 0) { - appendMcpCallSummaryButtons(messageElement, summaries); + setMcpCallSummaries(messageElement, summaries); renderedSummaryExecutions = true; } delete messageElement.dataset.pendingToolExecutionSummaries; @@ -3446,15 +3440,13 @@ function ensureMcpCallButtons(messageElement) { executionIds = []; } if (Array.isArray(executionIds) && executionIds.length > 0) { - appendMcpCallButtons(messageElement, executionIds); + setMcpCallExecutionIds(messageElement, executionIds); } delete messageElement.dataset.pendingMcpExecutionIds; } } -window.ensureMcpCallButtons = ensureMcpCallButtons; -window.appendMcpCallButtons = appendMcpCallButtons; -window.appendMcpCallSummaryButtons = appendMcpCallSummaryButtons; +window.setMcpCallExecutionIds = setMcpCallExecutionIds; function normalizeToolExecutionSummary(raw) { if (typeof raw === 'string') { @@ -3514,7 +3506,7 @@ function renderToolExecutionButtonContent(btn, displayToolName, index, status) { } // 批量获取工具摘要并更新按钮(消除 N 次单独 API 请求,合并为 1 次) -async function batchUpdateButtonToolNames(buttonsContainer, executionIds) { +async function batchUpdateButtonToolNames(buttonsContainer, executionIds, renderVersion) { if (!executionIds || executionIds.length === 0) return; try { const response = await apiFetch('/api/monitor/executions/names', { @@ -3524,6 +3516,8 @@ async function batchUpdateButtonToolNames(buttonsContainer, executionIds) { }); if (!response.ok) return; const nameMap = await response.json(); // { execId: toolName } 或 { execId: { toolName, status } } + // 等待请求期间如果摘要触发了新一轮渲染,旧响应不得覆盖新状态。 + if (renderVersion && buttonsContainer.dataset.renderVersion !== renderVersion) return; // 更新对应按钮的文本 const buttons = buttonsContainer.querySelectorAll('.mcp-detail-btn[data-exec-id]'); buttons.forEach(btn => { diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 0be9eb44..e4f60944 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -1314,8 +1314,8 @@ function integrateProgressToMCPSection(progressId, assistantMessageId, mcpExecut return; } - if (mcpIds.length > 0 && typeof window.appendMcpCallButtons === 'function') { - window.appendMcpCallButtons(assistantElement, mcpIds); + if (mcpIds.length > 0 && typeof window.setMcpCallExecutionIds === 'function') { + window.setMcpCallExecutionIds(assistantElement, mcpIds); const toolList = mcpSection.querySelector('.mcp-tool-list'); if (toolList) toolList.classList.remove('expanded'); } diff --git a/web/static/js/notifications.js b/web/static/js/notifications.js index 110b6a86..9fc18c66 100644 --- a/web/static/js/notifications.js +++ b/web/static/js/notifications.js @@ -140,7 +140,13 @@ return; } if ((item.type === 'task_completed' || item.type === 'long_running_tasks') && item.conversationId) { - window.location.hash = 'chat?conversation=' + encodeURIComponent(item.conversationId); + // 统一走路由提供的会话导航入口。直接修改 hash 会先触发 hashchange + // 切页,再由路由延迟加载会话,视觉上会多闪一次。 + if (typeof window.navigateToConversation === 'function') { + window.navigateToConversation(item.conversationId); + } else { + window.location.hash = 'chat?conversation=' + encodeURIComponent(item.conversationId); + } return; } if (item.type === 'task_failed' && item.executionId) { diff --git a/web/static/js/rbac.js b/web/static/js/rbac.js index 43b840ac..401e90fc 100644 --- a/web/static/js/rbac.js +++ b/web/static/js/rbac.js @@ -16,8 +16,9 @@ let rbacState = { resourceSearchTimer: null, resourceRequestSeq: 0, resourcePage: 0, - resourcePageSize: 12, + resourcePageSize: 8, resourceHasMore: false, + resourceTotal: 0, assignmentSearch: '', assignmentType: 'all', assignmentPage: 0, @@ -26,6 +27,11 @@ let rbacState = { showEffectivePermissions: false, auditLogs: [], auditLoading: false, + auditPage: 0, + auditPageSize: 20, + auditTotal: 0, + auditAction: '', + auditResourceType: '', pendingRoleUserId: '', pendingUserRoles: new Set(), editingRoleIsSystem: false, @@ -39,6 +45,7 @@ const rbacScopeMeta = { }; const rbacResourceLabels = { + user: '平台用户', project: '项目', conversation: '对话', vulnerability: '漏洞', webshell: 'WebShell 连接', batch_task: '批量任务', c2_listener: 'C2 监听器', }; @@ -202,12 +209,27 @@ function renderRbacPendingSelection() {
${rbacEscape(rbacShortId(log.resourceId || log.resource_id))}` : ''}
- ${rbacEscape(formatTime(log.createdAt || log.created_at))}
- 仅“指定资源”范围的角色需要单独授权。项目授权会覆盖该项目下的对话和漏洞。
-