Add files via upload

This commit is contained in:
公明
2026-05-01 01:01:30 +08:00
committed by GitHub
parent 0e35506ae1
commit 39926007fe
6 changed files with 1713 additions and 854 deletions
+400 -175
View File
@@ -6172,9 +6172,12 @@ header {
max-height: 90vh;
display: flex;
flex-direction: column;
border-radius: 20px;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.25), 0 0 0 1px rgba(0, 0, 0, 0.05);
box-shadow:
0 32px 96px rgba(15, 23, 42, 0.28),
0 8px 24px rgba(15, 23, 42, 0.1),
0 0 0 1px rgba(15, 23, 42, 0.06);
background: #ffffff;
}
@@ -6184,7 +6187,10 @@ header {
flex: 1;
overflow: hidden;
padding: 0;
background: linear-gradient(135deg, #fafbfc 0%, #f5f7fa 100%);
background:
radial-gradient(1200px 600px at 10% -10%, rgba(99, 102, 241, 0.06) 0%, transparent 60%),
radial-gradient(1000px 500px at 100% 110%, rgba(14, 165, 233, 0.05) 0%, transparent 60%),
linear-gradient(180deg, #f8fafc 0%, #eef2f7 100%);
}
.attack-chain-main-layout {
@@ -6205,10 +6211,13 @@ header {
}
.attack-chain-sidebar {
width: 280px;
width: 300px;
flex-shrink: 0;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
border-left: 1px solid rgba(0, 0, 0, 0.06);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(248, 250, 252, 0.9) 100%);
backdrop-filter: saturate(1.1) blur(6px);
-webkit-backdrop-filter: saturate(1.1) blur(6px);
border-left: 1px solid rgba(15, 23, 42, 0.07);
display: flex;
flex-direction: column;
overflow: hidden;
@@ -6224,43 +6233,67 @@ header {
-webkit-overflow-scrolling: touch;
}
.attack-chain-sidebar-content::-webkit-scrollbar {
width: 6px;
}
.attack-chain-sidebar-content::-webkit-scrollbar-track {
background: transparent;
}
.attack-chain-sidebar-content::-webkit-scrollbar-thumb {
background: rgba(15, 23, 42, 0.15);
border-radius: 3px;
}
.attack-chain-sidebar-content::-webkit-scrollbar-thumb:hover {
background: rgba(15, 23, 42, 0.3);
}
.attack-chain-toolbar {
padding: 12px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
padding: 14px 18px;
border-bottom: 1px solid rgba(15, 23, 42, 0.07);
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.96) 0%, rgba(248, 250, 252, 0.92) 100%);
backdrop-filter: saturate(1.1) blur(6px);
-webkit-backdrop-filter: saturate(1.1) blur(6px);
display: flex;
align-items: center;
gap: 16px;
gap: 14px;
flex-shrink: 0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.02);
box-shadow: 0 1px 0 rgba(15, 23, 42, 0.04);
}
.attack-chain-info {
font-size: 0.8125rem;
color: var(--text-primary);
color: #1e293b;
font-weight: 600;
padding: 6px 12px;
background: linear-gradient(135deg, rgba(0, 102, 255, 0.1) 0%, rgba(0, 102, 255, 0.06) 100%);
border: 1px solid rgba(0, 102, 255, 0.2);
border-radius: 6px;
box-shadow: 0 1px 3px rgba(0, 102, 255, 0.08);
padding: 7px 14px;
background: linear-gradient(135deg, rgba(59, 130, 246, 0.12) 0%, rgba(99, 102, 241, 0.08) 100%);
border: 1px solid rgba(59, 130, 246, 0.22);
border-radius: 999px;
box-shadow: 0 1px 2px rgba(59, 130, 246, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.6);
display: inline-flex;
align-items: center;
gap: 6px;
gap: 7px;
white-space: nowrap;
flex-shrink: 0;
letter-spacing: 0.2px;
}
.attack-chain-info::before {
content: '📊';
font-size: 0.8125rem;
content: '';
width: 14px;
height: 14px;
border-radius: 4px;
background:
linear-gradient(135deg, #3b82f6 0%, #6366f1 100%);
box-shadow: 0 1px 3px rgba(59, 130, 246, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.35);
flex-shrink: 0;
}
.attack-chain-legend {
display: flex;
flex-direction: column;
gap: 20px;
padding: 16px;
gap: 18px;
padding: 18px 16px 20px;
background: transparent;
flex-shrink: 0;
}
@@ -6271,58 +6304,89 @@ header {
gap: 10px;
font-size: 0.8125rem;
font-weight: 500;
color: var(--text-primary);
padding: 4px 0;
transition: all 0.2s ease;
color: #334155;
padding: 5px 8px;
border-radius: 8px;
transition: background 0.2s ease, transform 0.2s ease;
}
.legend-item:hover {
background: rgba(99, 102, 241, 0.06);
transform: translateX(2px);
}
.legend-color {
width: 20px;
height: 20px;
border-radius: 6px;
border: 1.5px solid rgba(255, 255, 255, 0.9);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.12), inset 0 1px 2px rgba(255, 255, 255, 0.3);
transition: all 0.2s ease;
width: 22px;
height: 22px;
border-radius: 7px;
border: 1.5px solid rgba(255, 255, 255, 0.95);
box-shadow:
0 2px 6px rgba(15, 23, 42, 0.18),
inset 0 1px 2px rgba(255, 255, 255, 0.5);
transition: transform 0.2s ease, box-shadow 0.2s ease;
flex-shrink: 0;
position: relative;
}
.legend-color::after {
content: '';
position: absolute;
inset: 0;
border-radius: inherit;
background: linear-gradient(180deg, rgba(255,255,255,0.35) 0%, transparent 60%);
pointer-events: none;
}
.legend-item:hover .legend-color {
transform: scale(1.15);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2), inset 0 1px 2px rgba(255, 255, 255, 0.3);
transform: scale(1.12);
box-shadow: 0 6px 14px rgba(15, 23, 42, 0.25), inset 0 1px 2px rgba(255, 255, 255, 0.4);
}
.legend-section {
display: flex;
flex-direction: column;
gap: 8px;
gap: 4px;
width: 100%;
padding: 12px 12px 14px;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(15, 23, 42, 0.06);
border-radius: 12px;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.03);
}
.legend-title {
font-size: 0.75rem;
font-size: 0.7rem;
font-weight: 700;
color: var(--text-primary);
color: #0f172a;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 6px;
padding-bottom: 6px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
letter-spacing: 0.8px;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px dashed rgba(15, 23, 42, 0.08);
display: flex;
align-items: center;
gap: 8px;
}
.legend-title::before {
content: '';
display: inline-block;
width: 4px;
height: 14px;
border-radius: 2px;
background: linear-gradient(180deg, #3b82f6 0%, #6366f1 100%);
}
.legend-line {
display: inline-block;
width: 32px;
width: 34px;
height: 0;
margin-right: 0;
vertical-align: middle;
border-radius: 2px;
flex-shrink: 0;
transition: all 0.2s ease;
transition: transform 0.2s ease;
}
.legend-item:hover .legend-line {
@@ -6332,28 +6396,44 @@ header {
.attack-chain-container {
flex: 1;
min-height: 0;
background: #ffffff;
border: none;
background:
linear-gradient(180deg, #fbfcfe 0%, #f6f8fc 100%);
border: 1px solid rgba(15, 23, 42, 0.06);
position: relative;
overflow: hidden;
margin: 12px;
border-radius: 12px;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.06);
margin: 14px 14px 14px 14px;
border-radius: 14px;
box-shadow:
0 1px 2px rgba(15, 23, 42, 0.03),
inset 0 1px 0 rgba(255, 255, 255, 0.75);
}
.attack-chain-container::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background:
radial-gradient(circle at 20% 30%, rgba(0, 102, 255, 0.02) 0%, transparent 50%),
radial-gradient(circle at 80% 70%, rgba(0, 102, 255, 0.02) 0%, transparent 50%);
inset: 0;
background-image:
radial-gradient(circle at center, rgba(15, 23, 42, 0.07) 1px, transparent 1.5px);
background-size: 22px 22px;
background-position: 0 0;
opacity: 0.55;
pointer-events: none;
z-index: 0;
border-radius: 12px;
border-radius: 14px;
mask-image: radial-gradient(ellipse at center, black 60%, transparent 100%);
-webkit-mask-image: radial-gradient(ellipse at center, black 60%, transparent 100%);
}
.attack-chain-container::after {
content: '';
position: absolute;
inset: 0;
background:
radial-gradient(800px 400px at 15% 12%, rgba(59, 130, 246, 0.06) 0%, transparent 70%),
radial-gradient(700px 360px at 85% 88%, rgba(168, 85, 247, 0.05) 0%, transparent 70%);
pointer-events: none;
z-index: 0;
border-radius: 14px;
}
/* 攻击链筛选器样式 */
@@ -6367,128 +6447,202 @@ header {
}
.attack-chain-filters input[type="text"] {
padding: 6px 12px;
border: 1.5px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 7px 14px 7px 34px;
border: 1.5px solid rgba(15, 23, 42, 0.1);
border-radius: 10px;
font-size: 0.8125rem;
min-width: 180px;
min-width: 200px;
flex: 1;
max-width: 300px;
background: #ffffff;
color: var(--text-primary);
transition: all 0.25s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
max-width: 320px;
background:
#ffffff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='m21 21-4.3-4.3'/%3E%3C/svg%3E") no-repeat 10px center;
color: #0f172a;
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
}
.attack-chain-filters input[type="text"]:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 4px rgba(0, 102, 255, 0.12), 0 2px 6px rgba(0, 102, 255, 0.1);
transform: translateY(-1px);
border-color: #6366f1;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15), 0 2px 6px rgba(99, 102, 241, 0.12);
}
.attack-chain-filters input[type="text"]:hover {
border-color: rgba(0, 102, 255, 0.3);
border-color: rgba(99, 102, 241, 0.4);
}
.attack-chain-filters input[type="text"]::placeholder {
color: var(--text-muted);
color: #94a3b8;
}
.attack-chain-filters select {
padding: 6px 12px;
padding-right: 32px;
border: 1.5px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
padding: 7px 34px 7px 14px;
border: 1.5px solid rgba(15, 23, 42, 0.1);
border-radius: 10px;
font-size: 0.8125rem;
background: #ffffff;
color: var(--text-primary);
color: #0f172a;
cursor: pointer;
transition: all 0.25s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
appearance: none;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23333' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%2364748b' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 10px center;
font-weight: 500;
min-width: 120px;
min-width: 130px;
}
.attack-chain-filters select:focus {
outline: none;
border-color: var(--accent-color);
box-shadow: 0 0 0 4px rgba(0, 102, 255, 0.12), 0 2px 6px rgba(0, 102, 255, 0.1);
transform: translateY(-1px);
border-color: #6366f1;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.15), 0 2px 6px rgba(99, 102, 241, 0.12);
}
.attack-chain-filters select:hover {
border-color: rgba(0, 102, 255, 0.3);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
border-color: rgba(99, 102, 241, 0.4);
box-shadow: 0 2px 4px rgba(15, 23, 42, 0.08);
}
.attack-chain-filters button.btn-secondary {
padding: 6px 14px;
padding: 7px 16px;
font-size: 0.8125rem;
font-weight: 600;
border: 1.5px solid rgba(0, 0, 0, 0.1);
border-radius: 6px;
border: 1.5px solid rgba(15, 23, 42, 0.1);
border-radius: 10px;
background: #ffffff;
color: var(--text-primary);
color: #0f172a;
cursor: pointer;
transition: all 0.25s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease;
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
white-space: nowrap;
}
.attack-chain-filters button.btn-secondary:hover {
background: linear-gradient(135deg, rgba(0, 102, 255, 0.08) 0%, rgba(0, 102, 255, 0.04) 100%);
border-color: var(--accent-color);
color: var(--accent-color);
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 102, 255, 0.15);
background: linear-gradient(135deg, rgba(99, 102, 241, 0.1) 0%, rgba(59, 130, 246, 0.06) 100%);
border-color: #6366f1;
color: #4338ca;
transform: translateY(-1px);
box-shadow: 0 6px 14px rgba(99, 102, 241, 0.18);
}
.attack-chain-action-btn {
padding: 10px 18px !important;
font-size: 0.875rem !important;
padding: 9px 16px !important;
font-size: 0.8125rem !important;
font-weight: 600 !important;
border-radius: 10px !important;
transition: all 0.25s ease !important;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08) !important;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease !important;
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.08) !important;
letter-spacing: 0.2px;
}
.attack-chain-action-btn:hover {
transform: translateY(-2px) !important;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important;
transform: translateY(-1px) !important;
box-shadow: 0 8px 18px rgba(15, 23, 42, 0.14) !important;
}
.modal-header {
padding: 16px 20px !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.06) !important;
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important;
padding: 16px 22px !important;
border-bottom: 1px solid rgba(15, 23, 42, 0.07) !important;
background:
linear-gradient(180deg, #ffffff 0%, #f8fafc 100%) !important;
flex-shrink: 0;
position: relative;
}
.modal-header::after {
content: '';
position: absolute;
left: 22px;
right: 22px;
bottom: -1px;
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(99, 102, 241, 0.25) 50%, transparent 100%);
pointer-events: none;
}
.modal-header h2 {
font-size: 1.25rem !important;
font-size: 1.2rem !important;
font-weight: 700 !important;
letter-spacing: -0.3px !important;
background: linear-gradient(135deg, #1a1a1a 0%, #4a4a4a 100%);
background: linear-gradient(135deg, #0f172a 0%, #334155 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
display: inline-flex;
align-items: center;
gap: 10px;
}
.modal-header h2::before {
content: '';
display: inline-block;
width: 6px;
height: 22px;
border-radius: 3px;
background: linear-gradient(180deg, #3b82f6 0%, #6366f1 60%, #a855f7 100%);
box-shadow: 0 2px 6px rgba(99, 102, 241, 0.4);
-webkit-text-fill-color: initial;
}
.attack-chain-details {
display: flex !important;
flex-direction: column;
gap: 12px;
margin-top: 8px;
padding-top: 20px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
margin: 4px 16px 16px;
padding: 16px;
background: linear-gradient(180deg, #ffffff 0%, #fafbff 100%);
border: 1px solid rgba(99, 102, 241, 0.15);
border-radius: 14px;
box-shadow:
0 4px 14px rgba(15, 23, 42, 0.06),
0 1px 2px rgba(15, 23, 42, 0.03);
transition: opacity 0.2s ease;
overflow: hidden;
will-change: opacity;
position: relative;
}
/* ========== 节点详情独占态:点击节点后详情占满 sidebar,图例隐藏 ========== */
/* 激活态:隐藏图例的两个辅助 section(风险等级、连线含义),保留详情 */
.attack-chain-sidebar.details-active
.attack-chain-legend > .legend-section:not(.attack-chain-details) {
display: none !important;
}
/* 激活态:让 legend 容器成为可撑满的 flex 列,详情占满剩余空间 */
.attack-chain-sidebar.details-active .attack-chain-legend {
flex: 1 1 auto;
min-height: 0;
padding: 0;
gap: 0;
display: flex;
}
/* 激活态:详情面板填满 sidebar 全高 */
.attack-chain-sidebar.details-active .attack-chain-details {
flex: 1 1 auto;
min-height: 0;
margin: 14px 14px 16px;
border-radius: 14px;
}
/* 激活态:详情内容区占满剩余空间可滚动 */
.attack-chain-sidebar.details-active .attack-chain-details-content {
flex: 1 1 auto;
min-height: 0;
max-height: none;
}
.attack-chain-details::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, #3b82f6 0%, #6366f1 50%, #a855f7 100%);
border-radius: 14px 14px 0 0;
}
.attack-chain-details[style*="display: none"] {
@@ -6515,8 +6669,8 @@ header {
justify-content: space-between;
align-items: center;
margin-bottom: 0 !important;
padding-bottom: 8px !important;
border-bottom: 1px solid rgba(0, 0, 0, 0.08) !important;
padding-bottom: 10px !important;
border-bottom: 1px dashed rgba(15, 23, 42, 0.1) !important;
}
.attack-chain-details-title span {
@@ -6524,32 +6678,33 @@ header {
}
.attack-chain-details-close {
width: 24px;
height: 24px;
width: 26px;
height: 26px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
border-radius: 8px;
cursor: pointer;
color: var(--text-secondary);
color: #64748b;
border: 1px solid transparent;
background: transparent;
background: rgba(15, 23, 42, 0.04);
transition: all 0.2s ease;
padding: 0;
flex-shrink: 0;
opacity: 0.6;
opacity: 0.75;
}
.attack-chain-details-close:hover {
background: rgba(0, 0, 0, 0.06);
color: var(--text-primary);
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
opacity: 1;
transform: rotate(90deg);
}
.attack-chain-details-content {
display: flex;
flex-direction: column;
gap: 12px;
gap: 10px;
max-height: 600px;
overflow-y: auto;
overflow-x: hidden;
@@ -6563,69 +6718,73 @@ header {
}
.attack-chain-details-content::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.02);
background: transparent;
border-radius: 3px;
}
.attack-chain-details-content::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.15);
background: rgba(15, 23, 42, 0.15);
border-radius: 3px;
}
.attack-chain-details-content::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.25);
background: rgba(15, 23, 42, 0.3);
}
.node-detail-item {
margin-bottom: 0;
font-size: 0.8125rem;
line-height: 1.6;
padding: 10px 12px;
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.05);
transition: all 0.2s ease;
line-height: 1.65;
padding: 10px 14px;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border-radius: 10px;
border: 1px solid rgba(15, 23, 42, 0.06);
transition: border-color 0.2s ease, box-shadow 0.2s ease, transform 0.2s ease;
color: #1e293b;
}
.node-detail-item:hover {
background: rgba(0, 0, 0, 0.04);
border-color: rgba(0, 102, 255, 0.2);
border-color: rgba(99, 102, 241, 0.3);
box-shadow: 0 3px 10px rgba(99, 102, 241, 0.08);
transform: translateY(-1px);
}
.node-detail-item strong {
display: block;
margin-bottom: 6px;
color: var(--text-primary);
font-weight: 600;
font-size: 0.75rem;
color: #64748b;
font-weight: 700;
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-secondary);
letter-spacing: 0.8px;
}
.node-detail-item code {
background: rgba(0, 0, 0, 0.06);
background: rgba(99, 102, 241, 0.08);
color: #4338ca;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.8125rem;
border-radius: 5px;
font-size: 0.8rem;
word-break: break-all;
display: inline-block;
max-width: 100%;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
color: var(--text-primary);
font-family: 'SFMono-Regular', 'Monaco', 'Menlo', 'Consolas', monospace;
border: 1px solid rgba(99, 102, 241, 0.15);
}
.node-detail-item pre {
background: rgba(0, 0, 0, 0.06);
padding: 10px;
border-radius: 6px;
font-size: 0.75rem;
background: #0f172a;
color: #e2e8f0;
padding: 12px;
border-radius: 8px;
font-size: 0.78rem;
overflow-x: auto;
margin: 8px 0 0 0;
line-height: 1.5;
line-height: 1.6;
max-width: 100%;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
border: 1px solid rgba(0, 0, 0, 0.08);
font-family: 'SFMono-Regular', 'Monaco', 'Menlo', 'Consolas', monospace;
border: 1px solid rgba(15, 23, 42, 0.2);
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.2);
}
.node-detail-item ul {
@@ -6683,7 +6842,7 @@ header {
.modal-header-actions {
display: flex;
gap: 12px;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
@@ -6692,46 +6851,86 @@ header {
padding: 8px 16px !important;
font-size: 0.8125rem !important;
font-weight: 600 !important;
border-radius: 8px !important;
transition: all 0.25s ease !important;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08) !important;
border: 2px solid transparent !important;
border-radius: 10px !important;
transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease, transform 0.2s ease, box-shadow 0.2s ease !important;
box-shadow: 0 1px 3px rgba(15, 23, 42, 0.06) !important;
border: 1.5px solid transparent !important;
display: inline-flex !important;
align-items: center !important;
gap: 6px !important;
gap: 7px !important;
letter-spacing: 0.2px;
position: relative;
overflow: hidden;
}
.attack-chain-action-btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(180deg, rgba(255,255,255,0.15) 0%, transparent 100%);
pointer-events: none;
}
.attack-chain-action-btn:hover {
transform: translateY(-2px) !important;
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important;
transform: translateY(-1px) !important;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.12) !important;
}
.attack-chain-action-btn:active {
transform: translateY(0) !important;
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.1) !important;
}
.attack-chain-action-btn.btn-primary {
background: linear-gradient(135deg, #0066ff 0%, #0052cc 100%) !important;
color: white !important;
border-color: #0066ff !important;
background: linear-gradient(135deg, #3b82f6 0%, #6366f1 50%, #8b5cf6 100%) !important;
color: #ffffff !important;
border-color: transparent !important;
box-shadow:
0 4px 14px rgba(99, 102, 241, 0.35),
inset 0 1px 0 rgba(255, 255, 255, 0.25) !important;
}
.attack-chain-action-btn.btn-primary:hover {
background: linear-gradient(135deg, #0052cc 0%, #0040a3 100%) !important;
box-shadow: 0 6px 20px rgba(0, 102, 255, 0.3) !important;
background: linear-gradient(135deg, #2563eb 0%, #4f46e5 50%, #7c3aed 100%) !important;
box-shadow:
0 10px 22px rgba(99, 102, 241, 0.42),
inset 0 1px 0 rgba(255, 255, 255, 0.3) !important;
}
.attack-chain-action-btn.btn-secondary {
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%) !important;
color: var(--text-primary) !important;
border-color: rgba(0, 0, 0, 0.1) !important;
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%) !important;
color: #334155 !important;
border-color: rgba(15, 23, 42, 0.1) !important;
}
.attack-chain-action-btn.btn-secondary:hover {
background: linear-gradient(135deg, #f8f9fa 0%, #f1f3f5 100%) !important;
border-color: var(--accent-color) !important;
color: var(--accent-color) !important;
background: linear-gradient(180deg, #eef2ff 0%, #e0e7ff 100%) !important;
border-color: #6366f1 !important;
color: #4338ca !important;
}
.attack-chain-action-btn.btn-primary:hover {
background: linear-gradient(135deg, #0052cc 0%, #0040a3 100%);
border-color: #0052cc;
.modal-header-actions .modal-close {
width: 32px;
height: 32px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
background: rgba(15, 23, 42, 0.04);
color: #64748b;
cursor: pointer;
transition: all 0.2s ease;
font-size: 20px;
line-height: 1;
user-select: none;
border: 1.5px solid transparent;
}
.modal-header-actions .modal-close:hover {
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.2);
transform: rotate(90deg);
}
.loading-spinner {
@@ -6739,8 +6938,28 @@ header {
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
font-size: 1rem;
color: #64748b;
font-size: 0.9375rem;
font-weight: 500;
position: relative;
z-index: 2;
}
.attack-chain-container .loading-spinner::before {
content: '';
display: inline-block;
width: 18px;
height: 18px;
margin-right: 10px;
border-radius: 50%;
border: 2.5px solid rgba(99, 102, 241, 0.2);
border-top-color: #6366f1;
animation: ac-spin 0.9s linear infinite;
}
@keyframes ac-spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.empty-message {
@@ -6748,8 +6967,11 @@ header {
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-secondary);
font-size: 1rem;
color: #64748b;
font-size: 0.9375rem;
font-weight: 500;
position: relative;
z-index: 2;
}
.error-message {
@@ -6757,9 +6979,12 @@ header {
align-items: center;
justify-content: center;
height: 100%;
color: var(--error-color);
font-size: 1rem;
color: #ef4444;
font-size: 0.9375rem;
font-weight: 500;
padding: 20px;
position: relative;
z-index: 2;
}
/* ==================== 知识管理样式 ==================== */
+11
View File
@@ -546,6 +546,17 @@
"typeCustom": "Custom",
"cmdParam": "Command parameter name",
"cmdParamPlaceholder": "Leave empty for cmd; e.g. xxx for xxx=command",
"encoding": "Response encoding",
"encodingAuto": "Auto detect",
"encodingUtf8": "UTF-8",
"encodingGbk": "GBK (Simplified Chinese Windows)",
"encodingGb18030": "GB18030",
"encodingHint": "Switch to GBK or GB18030 if the Simplified Chinese Windows target shows garbled output.",
"os": "Target OS",
"osAuto": "Auto (infer from Shell type)",
"osLinux": "Linux / Unix",
"osWindows": "Windows",
"osHint": "Determines whether file manager / uploads use Linux or Windows commands. Choose Windows for PHP/JSP hosted on Windows.",
"remark": "Remark",
"remarkPlaceholder": "Friendly name for this connection",
"deleteConfirm": "Delete this connection?",
+11
View File
@@ -535,6 +535,17 @@
"typeCustom": "自定义",
"cmdParam": "命令参数名",
"cmdParamPlaceholder": "不填默认为 cmd,如填 xxx 则请求为 xxx=命令",
"encoding": "响应编码",
"encodingAuto": "自动检测",
"encodingUtf8": "UTF-8",
"encodingGbk": "GBK(中文 Windows",
"encodingGb18030": "GB18030",
"encodingHint": "中文 Windows 目标若出现乱码,请切换为 GBK 或 GB18030",
"os": "目标系统",
"osAuto": "自动(按 Shell 类型推断)",
"osLinux": "Linux / Unix",
"osWindows": "Windows",
"osHint": "决定文件管理/上传使用 Linux 还是 Windows 命令;PHP/JSP 跑在 Windows 上请选 Windows",
"remark": "备注",
"remarkPlaceholder": "便于识别的备注名",
"deleteConfirm": "确定要删除该连接吗?",
+1123 -651
View File
File diff suppressed because it is too large Load Diff
+149 -28
View File
@@ -39,6 +39,100 @@ let webshellStreamingTypingId = 0;
let webshellProbeStatusById = {};
let webshellBatchProbeRunning = false;
/** 允许的响应编码,与后端 normalizeWebshellEncoding 对齐 */
const WEBSHELL_ALLOWED_ENCODINGS = ['auto', 'utf-8', 'gbk', 'gb18030'];
/** 归一化连接的 encoding 字段,返回 'auto' | 'utf-8' | 'gbk' | 'gb18030'(空/未知 → auto */
function normalizeWebshellEncoding(v) {
var s = (v == null ? '' : String(v)).trim().toLowerCase();
if (s === 'utf8') s = 'utf-8';
if (!s) return 'auto';
return WEBSHELL_ALLOWED_ENCODINGS.indexOf(s) >= 0 ? s : 'auto';
}
/** 从连接对象取编码,便于透传到 /api/webshell/exec 与 /api/webshell/file */
function webshellConnEncoding(conn) {
return normalizeWebshellEncoding(conn && conn.encoding);
}
/** 允许的目标 OS,与后端 normalizeWebshellOS 对齐 */
const WEBSHELL_ALLOWED_OS = ['auto', 'linux', 'windows'];
/** 归一化连接的 os 字段,返回 'auto' | 'linux' | 'windows'(空/未知 → auto */
function normalizeWebshellOS(v) {
var s = (v == null ? '' : String(v)).trim().toLowerCase();
if (!s) return 'auto';
return WEBSHELL_ALLOWED_OS.indexOf(s) >= 0 ? s : 'auto';
}
/** 从连接对象取目标 OS,便于透传到 /api/webshell/exec 与 /api/webshell/file */
function webshellConnOS(conn) {
return normalizeWebshellOS(conn && conn.os);
}
/**
* 组装 /api/webshell/file 的公共请求体。
* 所有文件管理调用点都应走此函数,避免遗漏字段(如 connection_id)。
* @param {Object} conn 连接对象
* @param {Object} extra 额外字段(action / path / content / target_path / chunk_index ...
* @returns {string} JSON 字符串
*/
function webshellFileRequestBody(conn, extra) {
const base = {
url: conn.url,
password: conn.password || '',
type: conn.type || 'php',
method: (conn.method || 'post').toLowerCase(),
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
connection_id: conn.id || ''
};
const merged = Object.assign(base, extra || {});
return JSON.stringify(merged);
}
/**
* 当服务端探活命中目标系统(仅 auto 连接首次列目录时出现)时,
* 把结果同步到本地 webshellConnections 缓存 + 持久化到数据库。
* 后续刷新不再探活,AI 也能直接看到正确的 OS 上下文。
*/
function applyWebshellDetectedOS(conn, data) {
if (!conn || !data || !data.detected_os) return;
const detected = normalizeWebshellOS(data.detected_os);
if (detected !== 'linux' && detected !== 'windows') return;
if (webshellConnOS(conn) !== 'auto') return; // 用户已显式配置,尊重之
conn.os = detected;
if (Array.isArray(webshellConnections)) {
for (var i = 0; i < webshellConnections.length; i++) {
if (webshellConnections[i] && webshellConnections[i].id === conn.id) {
webshellConnections[i].os = detected;
break;
}
}
}
if (typeof renderWebshellList === 'function') {
try { renderWebshellList(); } catch (e) {}
}
// 服务端已经回写了 DB;但极少数情况下调用方未带 connection_id,这里再兜底 PUT 一次
if (conn.id && typeof apiFetch === 'function') {
apiFetch('/api/webshell/connections/' + encodeURIComponent(conn.id), {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: conn.url,
password: conn.password || '',
type: conn.type || 'php',
method: conn.method || 'post',
cmd_param: conn.cmdParam || '',
remark: conn.remark || '',
encoding: conn.encoding || 'auto',
os: detected
})
}).catch(function () {});
}
}
/** 与主对话页一致:Eino 模式走 /api/multi-agent/streambody 带 orchestration */
function resolveWebshellAiStreamRequest() {
if (typeof apiFetch === 'undefined') {
@@ -335,6 +429,17 @@ function wsT(key) {
'webshell.addConnection': '添加连接',
'webshell.cmdParam': '命令参数名',
'webshell.cmdParamPlaceholder': '不填默认为 cmd,如填 xxx 则请求为 xxx=命令',
'webshell.encoding': '响应编码',
'webshell.encodingAuto': '自动检测',
'webshell.encodingUtf8': 'UTF-8',
'webshell.encodingGbk': 'GBK(中文 Windows',
'webshell.encodingGb18030': 'GB18030',
'webshell.encodingHint': '中文 Windows 目标若出现乱码,请切换为 GBK 或 GB18030',
'webshell.os': '目标系统',
'webshell.osAuto': '自动(按 Shell 类型推断)',
'webshell.osLinux': 'Linux / Unix',
'webshell.osWindows': 'Windows',
'webshell.osHint': '决定文件管理/上传使用 Linux 还是 Windows 命令;PHP/JSP 跑在 Windows 上请选 Windows',
'webshell.connections': '连接列表',
'webshell.noConnections': '暂无连接,请点击「添加连接」',
'webshell.selectOrAdd': '请从左侧选择连接,或添加新的 WebShell 连接',
@@ -661,9 +766,20 @@ function renderWebshellList() {
} else if (probe && probe.state === 'fail') {
probeHtml = '<span class="webshell-probe-badge fail" title="' + escapeHtml(probe.message || '') + '">' + (wsT('webshell.probeOffline') || '离线') + '</span>';
}
var encNorm = normalizeWebshellEncoding(conn.encoding);
var encHtml = '';
if (encNorm && encNorm !== 'auto') {
encHtml = '<span class="webshell-probe-badge" title="' + escapeHtml(wsT('webshell.encoding') || '响应编码') + '">' + escapeHtml(encNorm.toUpperCase()) + '</span>';
}
var osNorm = normalizeWebshellOS(conn.os);
var osHtml = '';
if (osNorm && osNorm !== 'auto') {
var osLabel = osNorm === 'windows' ? 'WIN' : 'LINUX';
osHtml = '<span class="webshell-probe-badge" title="' + escapeHtml(wsT('webshell.os') || '目标系统') + '">' + osLabel + '</span>';
}
return (
'<div class="webshell-item' + active + '" data-id="' + safeId + '">' +
'<div class="webshell-item-remark-row"><div class="webshell-item-remark" title="' + urlTitle + '">' + remark + '</div>' + probeHtml + '</div>' +
'<div class="webshell-item-remark-row"><div class="webshell-item-remark" title="' + urlTitle + '">' + remark + '</div>' + probeHtml + osHtml + encHtml + '</div>' +
'<div class="webshell-item-url" title="' + urlTitle + '">' + url + '</div>' +
'<div class="webshell-item-actions">' +
'<details class="webshell-conn-actions"><summary class="btn-ghost btn-sm webshell-conn-actions-btn" title="' + actionsLabel + '">' + actionsLabel + '</summary>' +
@@ -709,6 +825,8 @@ function probeWebshellConnection(conn) {
type: conn.type || 'php',
method: ((conn.method || 'post').toLowerCase() === 'get') ? 'get' : 'post',
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
command: 'echo 1'
})
})
@@ -3365,6 +3483,8 @@ function execWebshellCommand(conn, command) {
type: conn.type || 'php',
method: (conn.method || 'post').toLowerCase(),
cmd_param: conn.cmdParam || '',
encoding: webshellConnEncoding(conn),
os: webshellConnOS(conn),
command: command
})
}).then(function (r) { return r.json(); })
@@ -3391,17 +3511,10 @@ function webshellFileListDir(conn, path) {
apiFetch('/api/webshell/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: conn.url,
password: conn.password || '',
type: conn.type || 'php',
method: (conn.method || 'post').toLowerCase(),
cmd_param: conn.cmdParam || '',
action: 'list',
path: path
})
body: webshellFileRequestBody(conn, { action: 'list', path: path })
}).then(function (r) { return r.json(); })
.then(function (data) {
applyWebshellDetectedOS(conn, data);
if (!data.ok && data.error) {
listEl.innerHTML = '<div class="webshell-file-error">' + escapeHtml(data.error) + '</div><pre class="webshell-file-raw">' + escapeHtml(data.output || '') + '</pre>';
return;
@@ -3497,16 +3610,9 @@ function fetchWebshellDirectoryItems(conn, path) {
return apiFetch('/api/webshell/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: conn.url,
password: conn.password || '',
type: conn.type || 'php',
method: (conn.method || 'post').toLowerCase(),
cmd_param: conn.cmdParam || '',
action: 'list',
path: path
})
body: webshellFileRequestBody(conn, { action: 'list', path: path })
}).then(function (r) { return r.json(); }).then(function (data) {
applyWebshellDetectedOS(conn, data);
if (!data || data.error || !data.ok) return [];
return parseWebshellListItems(data.output || '');
}).catch(function () {
@@ -3801,7 +3907,7 @@ function webshellFileMkdir(conn, pathInput) {
var name = prompt(wsT('webshell.newDir') || '新建目录', 'newdir');
if (name == null || !name.trim()) return;
var path = base === '.' ? name.trim() : base + '/' + name.trim();
apiFetch('/api/webshell/file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'mkdir', path: path }) })
apiFetch('/api/webshell/file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: webshellFileRequestBody(conn, { action: 'mkdir', path: path }) })
.then(function (r) { return r.json(); })
.then(function () { webshellFileListDir(conn, base); })
.catch(function () { webshellFileListDir(conn, base); });
@@ -3848,7 +3954,7 @@ function webshellFileUpload(conn, pathInput) {
webshellFileListDir(conn, base);
return;
}
apiFetch('/api/webshell/file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'upload_chunk', path: path, content: base64Chunks[idx], chunk_index: idx }) })
apiFetch('/api/webshell/file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: webshellFileRequestBody(conn, { action: 'upload_chunk', path: path, content: base64Chunks[idx], chunk_index: idx }) })
.then(function (r) { return r.json(); })
.then(function () { idx++; sendNext(); })
.catch(function () { idx++; sendNext(); });
@@ -3867,7 +3973,7 @@ function webshellFileRename(conn, oldPath, oldName, listEl) {
var parts = oldPath.split('/');
var dir = parts.length > 1 ? parts.slice(0, -1).join('/') + '/' : '';
var newPath = dir + newName.trim();
apiFetch('/api/webshell/file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'rename', path: oldPath, target_path: newPath }) })
apiFetch('/api/webshell/file', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: webshellFileRequestBody(conn, { action: 'rename', path: oldPath, target_path: newPath }) })
.then(function (r) { return r.json(); })
.then(function () { webshellFileListDir(conn, document.getElementById('webshell-file-path').value.trim() || '.'); })
.catch(function () { webshellFileListDir(conn, document.getElementById('webshell-file-path').value.trim() || '.'); });
@@ -3906,7 +4012,7 @@ function webshellFileDownload(conn, path) {
apiFetch('/api/webshell/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'read', path: path })
body: webshellFileRequestBody(conn, { action: 'read', path: path })
}).then(function (r) { return r.json(); })
.then(function (data) {
var content = (data && data.output) != null ? data.output : (data.error || '');
@@ -3927,7 +4033,7 @@ function webshellFileRead(conn, path, listEl, browsePath) {
apiFetch('/api/webshell/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'read', path: path })
body: webshellFileRequestBody(conn, { action: 'read', path: path })
}).then(function (r) { return r.json(); })
.then(function (data) {
const out = (data && data.output) ? data.output : (data.error || '');
@@ -3956,7 +4062,7 @@ function webshellFileEdit(conn, path, listEl) {
apiFetch('/api/webshell/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'read', path: path })
body: webshellFileRequestBody(conn, { action: 'read', path: path })
}).then(function (r) { return r.json(); })
.then(function (data) {
const content = (data && data.output) ? data.output : (data.error || '');
@@ -3992,7 +4098,7 @@ function webshellFileWrite(conn, path, content, onDone, listEl) {
apiFetch('/api/webshell/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'write', path: path, content: content })
body: webshellFileRequestBody(conn, { action: 'write', path: path, content: content })
}).then(function (r) { return r.json(); })
.then(function (data) {
if (data && !data.ok && data.error && listEl) {
@@ -4011,7 +4117,7 @@ function webshellFileDelete(conn, path, onDone) {
apiFetch('/api/webshell/file', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: conn.url, password: conn.password || '', type: conn.type || 'php', method: (conn.method || 'post').toLowerCase(), cmd_param: conn.cmdParam || '', action: 'delete', path: path })
body: webshellFileRequestBody(conn, { action: 'delete', path: path })
}).then(function (r) { return r.json(); })
.then(function () { if (onDone) onDone(); })
.catch(function () { if (onDone) onDone(); });
@@ -4063,6 +4169,10 @@ function showAddWebshellModal() {
document.getElementById('webshell-type').value = 'php';
document.getElementById('webshell-method').value = 'post';
document.getElementById('webshell-cmd-param').value = '';
var osSelEl = document.getElementById('webshell-os');
if (osSelEl) osSelEl.value = 'auto';
var encSelEl = document.getElementById('webshell-encoding');
if (encSelEl) encSelEl.value = 'auto';
document.getElementById('webshell-remark').value = '';
var titleEl = document.getElementById('webshell-modal-title');
if (titleEl) titleEl.textContent = wsT('webshell.addConnection');
@@ -4081,6 +4191,10 @@ function showEditWebshellModal(connId) {
document.getElementById('webshell-type').value = conn.type || 'php';
document.getElementById('webshell-method').value = (conn.method || 'post').toLowerCase();
document.getElementById('webshell-cmd-param').value = conn.cmdParam || '';
var osEditEl = document.getElementById('webshell-os');
if (osEditEl) osEditEl.value = normalizeWebshellOS(conn.os);
var encEditEl = document.getElementById('webshell-encoding');
if (encEditEl) encEditEl.value = normalizeWebshellEncoding(conn.encoding);
document.getElementById('webshell-remark').value = conn.remark || '';
var titleEl = document.getElementById('webshell-modal-title');
if (titleEl) titleEl.textContent = wsT('webshell.editConnectionTitle');
@@ -4308,6 +4422,8 @@ function testWebshellConnection() {
var method = ((document.getElementById('webshell-method') || {}).value || 'post').toLowerCase();
var cmdParam = (document.getElementById('webshell-cmd-param') || {}).value;
if (cmdParam && typeof cmdParam.trim === 'function') cmdParam = cmdParam.trim(); else cmdParam = '';
var osTag = normalizeWebshellOS((document.getElementById('webshell-os') || {}).value);
var encoding = normalizeWebshellEncoding((document.getElementById('webshell-encoding') || {}).value);
var btn = document.getElementById('webshell-test-btn');
if (btn) { btn.disabled = true; btn.textContent = (typeof wsT === 'function' ? wsT('common.refresh') : '刷新') + '...'; }
if (typeof apiFetch === 'undefined') {
@@ -4315,6 +4431,7 @@ function testWebshellConnection() {
alert(wsT('webshell.testFailed') || '连通性测试失败');
return;
}
// 连通性使用 Windows/Linux 都识别的最小内建命令作为探测(echo 1 在 cmd 和 sh 下行为等价)
apiFetch('/api/webshell/exec', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -4324,6 +4441,8 @@ function testWebshellConnection() {
type: type,
method: method === 'get' ? 'get' : 'post',
cmd_param: cmdParam || '',
encoding: encoding,
os: osTag,
command: 'echo 1'
})
})
@@ -4369,12 +4488,14 @@ function saveWebshellConnection() {
var method = ((document.getElementById('webshell-method') || {}).value || 'post').toLowerCase();
var cmdParam = (document.getElementById('webshell-cmd-param') || {}).value;
if (cmdParam && typeof cmdParam.trim === 'function') cmdParam = cmdParam.trim(); else cmdParam = '';
var osTag = normalizeWebshellOS((document.getElementById('webshell-os') || {}).value);
var encoding = normalizeWebshellEncoding((document.getElementById('webshell-encoding') || {}).value);
var remark = (document.getElementById('webshell-remark') || {}).value;
if (remark && typeof remark.trim === 'function') remark = remark.trim(); else remark = '';
var editIdEl = document.getElementById('webshell-edit-id');
var editId = editIdEl ? editIdEl.value.trim() : '';
var body = { url: url, password: password, type: type, method: method === 'get' ? 'get' : 'post', cmd_param: cmdParam, remark: remark || url };
var body = { url: url, password: password, type: type, method: method === 'get' ? 'get' : 'post', cmd_param: cmdParam, encoding: encoding, os: osTag, remark: remark || url };
if (typeof apiFetch === 'undefined') return;
var reqUrl = editId ? ('/api/webshell/connections/' + encodeURIComponent(editId)) : '/api/webshell/connections';
+19
View File
@@ -2925,6 +2925,25 @@
<label for="webshell-cmd-param" data-i18n="webshell.cmdParam">命令参数名</label>
<input type="text" id="webshell-cmd-param" data-i18n="webshell.cmdParamPlaceholder" data-i18n-attr="placeholder" placeholder="不填默认为 cmd,如 xxx 则请求为 xxx=命令" />
</div>
<div class="form-group">
<label for="webshell-os" data-i18n="webshell.os">目标系统</label>
<select id="webshell-os">
<option value="auto" data-i18n="webshell.osAuto">自动(按 Shell 类型推断)</option>
<option value="linux" data-i18n="webshell.osLinux">Linux / Unix</option>
<option value="windows" data-i18n="webshell.osWindows">Windows</option>
</select>
<small class="form-hint" data-i18n="webshell.osHint">决定文件管理/上传使用 Linux 还是 Windows 命令;PHP/JSP 跑在 Windows 上请选 Windows</small>
</div>
<div class="form-group">
<label for="webshell-encoding" data-i18n="webshell.encoding">响应编码</label>
<select id="webshell-encoding">
<option value="auto" data-i18n="webshell.encodingAuto">自动检测</option>
<option value="utf-8" data-i18n="webshell.encodingUtf8">UTF-8</option>
<option value="gbk" data-i18n="webshell.encodingGbk">GBK(中文 Windows</option>
<option value="gb18030" data-i18n="webshell.encodingGb18030">GB18030</option>
</select>
<small class="form-hint" data-i18n="webshell.encodingHint">中文 Windows 目标若出现乱码,请切换为 GBK 或 GB18030</small>
</div>
<div class="form-group">
<label for="webshell-remark" data-i18n="webshell.remark">备注</label>
<input type="text" id="webshell-remark" data-i18n="webshell.remarkPlaceholder" data-i18n-attr="placeholder" placeholder="便于识别的备注名" />