mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-01 00:27:35 +02:00
Add files via upload
This commit is contained in:
+501
-38
@@ -3325,7 +3325,7 @@ html[data-theme="dark"] .new-chat-btn:focus-visible {
|
||||
|
||||
.message-avatar svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@@ -28735,63 +28735,195 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda
|
||||
|
||||
/* ==================== 信息收集(FOFA)页面 ==================== */
|
||||
.info-collect-panel {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(37, 99, 235, 0.08), rgba(20, 184, 166, 0.05) 42%, transparent 70%),
|
||||
var(--bg-primary);
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 72%, var(--accent-color));
|
||||
border-radius: 14px;
|
||||
padding: 18px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
|
||||
box-shadow: 0 16px 36px rgba(15, 23, 42, 0.08);
|
||||
}
|
||||
|
||||
/* 表单整体增加纵向留白,避免“挤在一起” */
|
||||
.info-collect-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 12px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 将每个表单块做成轻量卡片分组(只作用于 info-collect 顶层 form-group) */
|
||||
.info-collect-form > .form-group,
|
||||
.info-collect-form > .info-collect-form-row {
|
||||
padding: 14px 14px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #ffffff 0%, #fafbfc 100%);
|
||||
.info-collect-form-top {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 0.32fr) minmax(420px, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-collect-provider-card,
|
||||
.info-collect-query-card,
|
||||
.info-collect-options-card,
|
||||
.info-collect-fields-card {
|
||||
border: 1px solid rgba(15, 23, 42, 0.07);
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.82);
|
||||
box-shadow: 0 1px 0 rgba(255, 255, 255, 0.9) inset;
|
||||
}
|
||||
|
||||
.info-collect-form .form-group {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.info-collect-provider-card,
|
||||
.info-collect-query-card,
|
||||
.info-collect-fields-card {
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
.info-collect-query-card {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(248, 250, 252, 0.88)),
|
||||
var(--bg-primary);
|
||||
}
|
||||
|
||||
.info-collect-options-card {
|
||||
padding: 14px 16px;
|
||||
background:
|
||||
linear-gradient(135deg, rgba(248, 250, 252, 0.94), rgba(239, 246, 255, 0.68));
|
||||
}
|
||||
|
||||
.info-collect-step-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 650;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.info-collect-step-title span {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 22px;
|
||||
border-radius: 7px;
|
||||
background: rgba(37, 99, 235, 0.1);
|
||||
color: var(--accent-color);
|
||||
font-size: 0.75rem;
|
||||
font-weight: 700;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.info-collect-step-title label,
|
||||
.info-collect-step-title div {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.info-collect-query-mode-panel[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.info-collect-query-mode-panel + .info-collect-query-mode-panel {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
#info-collect-syntax-panel.is-generated-target {
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed color-mix(in srgb, var(--border-color) 76%, var(--text-secondary));
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #info-collect-syntax-panel.is-generated-target {
|
||||
border-top-color: color-mix(in srgb, var(--border-color) 58%, var(--accent-color));
|
||||
}
|
||||
|
||||
.info-collect-inner-label {
|
||||
display: block;
|
||||
margin: 0 0 8px;
|
||||
color: var(--text-primary);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.info-collect-provider-select .settings-custom-select-trigger {
|
||||
min-height: 46px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.info-collect-provider-select .settings-custom-select-menu {
|
||||
z-index: 3200;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.info-collect-provider-select .settings-custom-select-option {
|
||||
min-height: 36px;
|
||||
}
|
||||
|
||||
/* 覆盖全局 .form-group textarea(min-height:200px) 的默认大留白 */
|
||||
.form-group textarea.info-collect-query-input {
|
||||
min-height: 36px;
|
||||
min-height: 42px;
|
||||
max-height: 96px;
|
||||
overflow: hidden; /* 配合 JS 自动增高 */
|
||||
resize: none;
|
||||
padding: 10px 12px;
|
||||
padding: 11px 12px;
|
||||
}
|
||||
|
||||
.info-collect-nl-row {
|
||||
#page-info-collect input[type="text"],
|
||||
#page-info-collect input[type="number"],
|
||||
#page-info-collect select,
|
||||
#page-info-collect textarea {
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#page-info-collect input:focus,
|
||||
#page-info-collect textarea:focus {
|
||||
border-color: var(--accent-color);
|
||||
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
|
||||
}
|
||||
|
||||
.info-collect-nl-row,
|
||||
.info-collect-query-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-collect-nl-row .info-collect-query-input {
|
||||
.info-collect-nl-row .info-collect-query-input,
|
||||
.info-collect-query-row .info-collect-query-input {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0; /* 允许在 flex 中收缩,避免撑破布局 */
|
||||
}
|
||||
|
||||
.info-collect-nl-row button {
|
||||
.info-collect-nl-row button,
|
||||
.info-collect-query-row button {
|
||||
flex: 0 0 auto;
|
||||
white-space: nowrap;
|
||||
min-height: 42px;
|
||||
padding-inline: 18px;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.info-collect-panel {
|
||||
padding: 14px;
|
||||
}
|
||||
.info-collect-form-top {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.info-collect-form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.info-collect-nl-row {
|
||||
.info-collect-nl-row,
|
||||
.info-collect-query-row {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
.syntax-guide-header {
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.info-collect-col-actions {
|
||||
width: 140px;
|
||||
}
|
||||
@@ -28799,15 +28931,45 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda
|
||||
|
||||
.info-collect-form-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(180px, 1fr));
|
||||
gap: 14px;
|
||||
align-items: end;
|
||||
grid-template-columns: minmax(160px, 0.7fr) minmax(160px, 0.7fr) minmax(150px, 0.45fr) minmax(280px, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-collect-form-row .form-group {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-collect-form-row #info-collect-full-option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.info-collect-form-row #info-collect-full-option::before {
|
||||
content: '';
|
||||
display: block;
|
||||
height: 17px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.info-collect-form-row #info-collect-full-option .checkbox-label {
|
||||
min-height: 40px;
|
||||
margin-top: -1px;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.info-collect-row-hint {
|
||||
grid-column: auto;
|
||||
align-self: end;
|
||||
margin-bottom: 9px;
|
||||
}
|
||||
|
||||
.info-collect-results {
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
@@ -28953,6 +29115,110 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.info-collect-syntax-guide {
|
||||
margin-top: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 82%, var(--accent-color));
|
||||
border-radius: 8px;
|
||||
background: color-mix(in srgb, var(--bg-secondary) 86%, var(--accent-color) 5%);
|
||||
}
|
||||
|
||||
.info-collect-syntax-guide.is-expanded {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.syntax-guide-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.info-collect-syntax-guide.is-expanded .syntax-guide-header {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.syntax-guide-summary {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.syntax-guide-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.info-collect-doc-link {
|
||||
color: var(--accent-color);
|
||||
font-size: 0.8125rem;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dashed color-mix(in srgb, var(--accent-color) 52%, transparent);
|
||||
}
|
||||
|
||||
.info-collect-doc-link:hover {
|
||||
border-bottom-color: var(--accent-color);
|
||||
}
|
||||
|
||||
.syntax-guide-toggle {
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 70%, var(--accent-color));
|
||||
border-radius: 7px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 5px 9px;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.syntax-guide-toggle:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
background: color-mix(in srgb, var(--accent-color) 8%, var(--bg-primary));
|
||||
}
|
||||
|
||||
.syntax-guide-section + .syntax-guide-section {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.syntax-guide-title {
|
||||
margin-bottom: 6px;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.syntax-guide-examples {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.syntax-example-chip {
|
||||
max-width: 100%;
|
||||
border: 1px solid color-mix(in srgb, var(--border-color) 78%, var(--text-secondary));
|
||||
border-radius: 7px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
padding: 6px 8px;
|
||||
font-family: var(--font-mono, "SFMono-Regular", Consolas, "Liberation Mono", monospace);
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.25;
|
||||
text-align: left;
|
||||
overflow-wrap: anywhere;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background 0.15s ease, color 0.15s ease;
|
||||
}
|
||||
|
||||
.syntax-example-chip:hover {
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
background: color-mix(in srgb, var(--accent-color) 8%, var(--bg-primary));
|
||||
}
|
||||
|
||||
.info-collect-query-input {
|
||||
min-height: 40px;
|
||||
max-height: 110px;
|
||||
@@ -29088,21 +29354,21 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda
|
||||
.info-collect-cell-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.45);
|
||||
background: rgba(2, 6, 23, 0.62);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10000;
|
||||
padding: 24px;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-content {
|
||||
width: min(920px, 100%);
|
||||
max-height: 86vh;
|
||||
width: min(760px, 100%);
|
||||
max-height: min(78vh, 720px);
|
||||
background: var(--bg-primary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.20);
|
||||
box-shadow: 0 24px 70px rgba(0,0,0,0.35);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
@@ -29110,42 +29376,91 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda
|
||||
|
||||
.info-collect-cell-modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 12px 14px;
|
||||
gap: 16px;
|
||||
padding: 18px 20px 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 12px;
|
||||
font-size: 1rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-subtitle {
|
||||
margin-top: 4px;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-header .btn-icon {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-body {
|
||||
padding: 12px 14px;
|
||||
padding: 18px 20px;
|
||||
overflow: auto;
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-pre {
|
||||
margin: 0;
|
||||
min-height: 64px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text-primary);
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-footer {
|
||||
padding: 12px 14px;
|
||||
padding: 14px 20px 18px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.info-collect-cell-modal {
|
||||
align-items: flex-end;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-content {
|
||||
width: 100%;
|
||||
max-height: 82vh;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-header {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-body {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.info-collect-cell-modal-footer {
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.info-collect-parse-modal-body {
|
||||
@@ -29195,6 +29510,107 @@ html[data-theme="dark"] .skills-management-page .skill-card-actions .btn-seconda
|
||||
padding: 18px 28px;
|
||||
}
|
||||
|
||||
.info-collect-panel,
|
||||
.info-collect-provider-card,
|
||||
.info-collect-query-card,
|
||||
.info-collect-options-card,
|
||||
.info-collect-fields-card,
|
||||
.info-collect-step-title,
|
||||
.info-collect-step-title span,
|
||||
.info-collect-inner-label,
|
||||
.info-collect-provider-select .settings-custom-select-trigger,
|
||||
.info-collect-provider-select .settings-custom-select-menu,
|
||||
.info-collect-provider-select .settings-custom-select-option,
|
||||
.info-collect-form-row #info-collect-full-option .checkbox-label,
|
||||
.info-collect-results,
|
||||
.info-collect-results-header,
|
||||
.info-collect-selected,
|
||||
.info-collect-results-title,
|
||||
.info-collect-results-meta,
|
||||
.info-collect-results-table-wrap,
|
||||
.info-collect-table,
|
||||
.info-collect-table th,
|
||||
.info-collect-table td,
|
||||
.info-collect-table .muted,
|
||||
.info-collect-actions .btn-icon,
|
||||
.info-collect-query-input,
|
||||
.preset-chip,
|
||||
.info-collect-link,
|
||||
.info-collect-cell-text,
|
||||
.info-collect-columns-panel,
|
||||
.info-collect-columns-title,
|
||||
.info-collect-col-item,
|
||||
.info-collect-cell-modal,
|
||||
.info-collect-cell-modal-content,
|
||||
.info-collect-cell-modal-header,
|
||||
.info-collect-cell-modal-title,
|
||||
.info-collect-cell-modal-subtitle,
|
||||
.info-collect-cell-modal-pre,
|
||||
.info-collect-cell-modal-footer,
|
||||
.info-collect-parse-warnings,
|
||||
.info-collect-parse-explanation,
|
||||
#page-info-collect input[type="text"],
|
||||
#page-info-collect input[type="number"],
|
||||
#page-info-collect select,
|
||||
#page-info-collect textarea,
|
||||
#page-info-collect .form-hint {
|
||||
transition:
|
||||
background 0.22s ease,
|
||||
background-color 0.22s ease,
|
||||
border-color 0.22s ease,
|
||||
color 0.22s ease,
|
||||
box-shadow 0.22s ease;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.info-collect-panel,
|
||||
.info-collect-provider-card,
|
||||
.info-collect-query-card,
|
||||
.info-collect-options-card,
|
||||
.info-collect-fields-card,
|
||||
.info-collect-step-title,
|
||||
.info-collect-step-title span,
|
||||
.info-collect-inner-label,
|
||||
.info-collect-provider-select .settings-custom-select-trigger,
|
||||
.info-collect-provider-select .settings-custom-select-menu,
|
||||
.info-collect-provider-select .settings-custom-select-option,
|
||||
.info-collect-form-row #info-collect-full-option .checkbox-label,
|
||||
.info-collect-results,
|
||||
.info-collect-results-header,
|
||||
.info-collect-selected,
|
||||
.info-collect-results-title,
|
||||
.info-collect-results-meta,
|
||||
.info-collect-results-table-wrap,
|
||||
.info-collect-table,
|
||||
.info-collect-table th,
|
||||
.info-collect-table td,
|
||||
.info-collect-table .muted,
|
||||
.info-collect-actions .btn-icon,
|
||||
.info-collect-query-input,
|
||||
.preset-chip,
|
||||
.info-collect-link,
|
||||
.info-collect-cell-text,
|
||||
.info-collect-columns-panel,
|
||||
.info-collect-columns-title,
|
||||
.info-collect-col-item,
|
||||
.info-collect-cell-modal,
|
||||
.info-collect-cell-modal-content,
|
||||
.info-collect-cell-modal-header,
|
||||
.info-collect-cell-modal-title,
|
||||
.info-collect-cell-modal-subtitle,
|
||||
.info-collect-cell-modal-pre,
|
||||
.info-collect-cell-modal-footer,
|
||||
.info-collect-parse-warnings,
|
||||
.info-collect-parse-explanation,
|
||||
#page-info-collect input[type="text"],
|
||||
#page-info-collect input[type="number"],
|
||||
#page-info-collect select,
|
||||
#page-info-collect textarea,
|
||||
#page-info-collect .form-hint {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.info-collect-columns-list {
|
||||
grid-template-columns: repeat(2, minmax(140px, 1fr));
|
||||
@@ -34381,8 +34797,10 @@ html[data-theme="dark"] #webshell-db-profile-modal .modal-header,
|
||||
html[data-theme="dark"] #webshell-db-profile-modal .modal-footer,
|
||||
html[data-theme="dark"] .webshell-db-toolbar label,
|
||||
html[data-theme="dark"] .webshell-db-output-wrap,
|
||||
html[data-theme="dark"] .info-collect-form > .form-group,
|
||||
html[data-theme="dark"] .info-collect-form > .info-collect-form-row,
|
||||
html[data-theme="dark"] .info-collect-provider-card,
|
||||
html[data-theme="dark"] .info-collect-query-card,
|
||||
html[data-theme="dark"] .info-collect-options-card,
|
||||
html[data-theme="dark"] .info-collect-fields-card,
|
||||
html[data-theme="dark"] .info-collect-selected,
|
||||
html[data-theme="dark"] .info-collect-results-table-wrap,
|
||||
html[data-theme="dark"] .info-collect-presets,
|
||||
@@ -34394,6 +34812,39 @@ html[data-theme="dark"] .info-collect-col-item {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .info-collect-options-card {
|
||||
background: #0f172a !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .info-collect-step-title {
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .info-collect-step-title span {
|
||||
background: rgba(96, 165, 250, 0.16) !important;
|
||||
color: #93c5fd !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .info-collect-provider-select .settings-custom-select-trigger,
|
||||
html[data-theme="dark"] .info-collect-provider-select .settings-custom-select-menu {
|
||||
background: #0f172a !important;
|
||||
border-color: #263244 !important;
|
||||
color: var(--text-primary) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .info-collect-provider-select .settings-custom-select-menu {
|
||||
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.36) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .info-collect-provider-select .settings-custom-select-option:hover:not(:disabled) {
|
||||
background: rgba(30, 41, 59, 0.82) !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .info-collect-provider-select .settings-custom-select-option.is-selected {
|
||||
background: rgba(59, 130, 246, 0.18) !important;
|
||||
color: #93c5fd !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .hitl-reviewer-toggle,
|
||||
html[data-theme="dark"] .projects-tabs,
|
||||
html[data-theme="dark"] .projects-sidebar-head,
|
||||
@@ -34544,6 +34995,14 @@ html[data-theme="dark"] .info-collect-query-input {
|
||||
border-color: #2b374b !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] #page-info-collect input[type="text"],
|
||||
html[data-theme="dark"] #page-info-collect input[type="number"],
|
||||
html[data-theme="dark"] #page-info-collect .info-collect-form-row #info-collect-full-option .checkbox-label {
|
||||
background: #0f172a !important;
|
||||
color: var(--text-primary) !important;
|
||||
border-color: #2b374b !important;
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .hitl-config-select:focus,
|
||||
html[data-theme="dark"] .hitl-config-input:focus,
|
||||
html[data-theme="dark"] .hitl-config-textarea:focus,
|
||||
@@ -34604,6 +35063,10 @@ html[data-theme="dark"] #page-info-collect .preset-chip:hover {
|
||||
}
|
||||
|
||||
html[data-theme="dark"] .info-collect-panel {
|
||||
background:
|
||||
linear-gradient(135deg, rgba(96, 165, 250, 0.1), rgba(45, 212, 191, 0.06) 44%, transparent 70%),
|
||||
#0b1120 !important;
|
||||
border-color: #263244 !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -648,7 +648,17 @@
|
||||
"einoRunRetryTitle": "🔁 Transient error retry",
|
||||
"einoEmptyResponseContinueTitle": "🔁 Auto resume (no assistant text)",
|
||||
"einoEmptyResponseContinueMessage": "Session ended without captured assistant text; resuming from trace…",
|
||||
"einoRunRetryPlan": "Retry progress: attempt {{attempt}}/{{maxAttempts}}, waiting {{backoffSec}}s",
|
||||
"einoRunRetryReasonKind": "Reason type",
|
||||
"einoRunRetryErrorDetail": "Error detail",
|
||||
"einoRunRetryKind_rate_limit": "Rate limited / too many requests",
|
||||
"einoRunRetryKind_retryable_http": "Retryable HTTP error",
|
||||
"einoRunRetryKind_upstream_server": "Upstream server error",
|
||||
"einoRunRetryKind_http_error": "HTTP error",
|
||||
"einoRunRetryKind_upstream_busy": "Upstream busy",
|
||||
"einoRunRetryKind_network": "Network connection issue",
|
||||
"einoRunRetryKind_stream": "Streaming read issue",
|
||||
"einoRunRetryKind_transient": "Transient issue",
|
||||
"iterationLimitReachedTitle": "⛔ Iteration limit reached",
|
||||
"iterationLimitReachedMessage": "Maximum iteration count reached; automatic iteration has stopped.",
|
||||
"einoPendingOrphanedTitle": "🧹 Tool call reconciliation",
|
||||
@@ -993,6 +1003,7 @@
|
||||
"batchScanFailed": "Batch scan failed",
|
||||
"batchQueueCreated": "Batch scan queue created",
|
||||
"field": "Field",
|
||||
"cellValueLength": "{{count}} characters",
|
||||
"parsePending": "AI parsing...",
|
||||
"parsePendingClickCancel": "AI parsing... (click button to cancel)",
|
||||
"parseSlow": "AI parse is taking a while, still processing…",
|
||||
@@ -1013,6 +1024,7 @@
|
||||
"none": "None",
|
||||
"truncated": "truncated",
|
||||
"resultsMeta": "Total {{total}} · This page {{count}} · page={{page}} · size={{size}}",
|
||||
"providerReturnedFewer": "Provider returned {{count}}/{{expected}} rows",
|
||||
"parseModalCancel": "Cancel",
|
||||
"parseModalApply": "Fill into query",
|
||||
"parseModalApplyRun": "Fill and query"
|
||||
|
||||
@@ -636,7 +636,17 @@
|
||||
"einoRunRetryTitle": "🔁 临时错误重试",
|
||||
"einoEmptyResponseContinueTitle": "🔁 自动续跑(无助手正文)",
|
||||
"einoEmptyResponseContinueMessage": "会话已结束但未捕获到助手正文,正在基于轨迹自动续跑…",
|
||||
"einoRunRetryPlan": "重试进度:第 {{attempt}}/{{maxAttempts}} 次,等待 {{backoffSec}} 秒",
|
||||
"einoRunRetryReasonKind": "原因类型",
|
||||
"einoRunRetryErrorDetail": "具体报错",
|
||||
"einoRunRetryKind_rate_limit": "限流 / 请求过多",
|
||||
"einoRunRetryKind_retryable_http": "可重试 HTTP 错误",
|
||||
"einoRunRetryKind_upstream_server": "上游服务错误",
|
||||
"einoRunRetryKind_http_error": "HTTP 错误",
|
||||
"einoRunRetryKind_upstream_busy": "上游繁忙",
|
||||
"einoRunRetryKind_network": "网络连接异常",
|
||||
"einoRunRetryKind_stream": "流式读取异常",
|
||||
"einoRunRetryKind_transient": "临时异常",
|
||||
"iterationLimitReachedTitle": "⛔ 达到迭代上限",
|
||||
"iterationLimitReachedMessage": "已达到最大迭代次数,任务已停止继续自动迭代。",
|
||||
"einoPendingOrphanedTitle": "🧹 工具调用收尾补偿",
|
||||
@@ -981,6 +991,7 @@
|
||||
"batchScanFailed": "批量扫描失败",
|
||||
"batchQueueCreated": "已创建批量扫描队列",
|
||||
"field": "字段",
|
||||
"cellValueLength": "共 {{count}} 个字符",
|
||||
"parsePending": "AI 解析中...",
|
||||
"parsePendingClickCancel": "AI 解析中...(点击按钮可取消)",
|
||||
"parseSlow": "AI 解析耗时较长,仍在处理中…",
|
||||
@@ -1001,6 +1012,7 @@
|
||||
"none": "无",
|
||||
"truncated": "已截断",
|
||||
"resultsMeta": "共 {{total}} 条 · 本页 {{count}} 条 · page={{page}} · size={{size}}",
|
||||
"providerReturnedFewer": "上游实际返回 {{count}}/{{expected}} 条",
|
||||
"parseModalCancel": "取消",
|
||||
"parseModalApply": "填入查询框",
|
||||
"parseModalApplyRun": "填入并查询"
|
||||
|
||||
+17
-8
@@ -1578,18 +1578,26 @@ function fofaResultToAsset(row, fields) {
|
||||
const idx = Array.isArray(fields) ? fields.indexOf(name) : -1;
|
||||
return idx >= 0 && Array.isArray(row) && row[idx] != null ? String(row[idx]).trim() : '';
|
||||
};
|
||||
const firstValue = names => {
|
||||
for (const name of names) {
|
||||
const v = value(name);
|
||||
if (v) return v;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const port = Number.parseInt(value('port'), 10);
|
||||
const rawIP = value('ip');
|
||||
const rawIP = firstValue(['ip', 'ip_str']);
|
||||
const ip = assetEditorIsIPv4(rawIP) || assetEditorIsIPv6(rawIP) ? rawIP.toLowerCase() : '';
|
||||
const rawDomain = value('domain');
|
||||
const rawDomain = firstValue(['domain', 'hostname', 'hostnames', 'domains']);
|
||||
const domain = rawDomain && !assetEditorIsIPv4(rawDomain) && !assetEditorIsIPv6(rawDomain)
|
||||
? assetEditorNormalizeDomain(rawDomain)
|
||||
? assetEditorNormalizeDomain(rawDomain.split(',')[0].replace(/^\[|\]$/g, '').replace(/^"|"$/g, ''))
|
||||
: '';
|
||||
const rawProtocol = value('protocol').toLowerCase();
|
||||
const rawProtocol = firstValue(['protocol', 'service.name', 'transport']).toLowerCase();
|
||||
return {
|
||||
host: value('host'), ip, port: Number.isFinite(port) ? port : 0, domain,
|
||||
protocol: /^[a-z][a-z0-9+.-]{0,31}$/.test(rawProtocol) ? rawProtocol : '', title: value('title'), server: value('server'), country: value('country'),
|
||||
province: value('province'), city: value('city'), source: 'fofa', status: 'active'
|
||||
host: firstValue(['host', 'hostname']), ip, port: Number.isFinite(port) ? port : 0, domain,
|
||||
protocol: /^[a-z][a-z0-9+.-]{0,31}$/.test(rawProtocol) ? rawProtocol : '', title: firstValue(['title', 'service.http.title']), server: firstValue(['server', 'product']),
|
||||
country: firstValue(['country', 'location.country_cn', 'location.country_name']), province: firstValue(['province', 'location.province_cn']), city: firstValue(['city', 'location.city_cn', 'location.city']),
|
||||
source: (window.infoCollectState?.currentPayload?.provider || 'fofa'), status: 'active'
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1607,7 +1615,8 @@ async function importFofaAssetsByIndexes(indexes) {
|
||||
alert(assetT('assets.noValidImportTarget', '所选结果中没有可入库的有效资产目标'));
|
||||
return;
|
||||
}
|
||||
const response = await apiFetch('/api/assets/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assets, source: 'fofa', source_query: current.query || '' }) });
|
||||
const source = current.provider || 'fofa';
|
||||
const response = await apiFetch('/api/assets/import', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ assets, source, source_query: current.query || '' }) });
|
||||
if (!response.ok) {
|
||||
alert(assetT('assets.importFailed', '资产入库失败') + ': ' + await response.text());
|
||||
return;
|
||||
|
||||
+79
-13
@@ -2644,6 +2644,83 @@ function renderProcessDetails(messageId, processDetails, options) {
|
||||
return s ? ('[' + s + '] ') : '';
|
||||
}
|
||||
|
||||
function formatProcessDetailEinoRunRetryKind(kind) {
|
||||
if (typeof window.formatEinoRunRetryKind === 'function') {
|
||||
return window.formatEinoRunRetryKind(kind);
|
||||
}
|
||||
const key = String(kind || '').trim();
|
||||
if (!key) return '';
|
||||
const labels = {
|
||||
rate_limit: '限流 / 请求过多',
|
||||
retryable_http: '可重试 HTTP 错误',
|
||||
upstream_server: '上游服务错误',
|
||||
http_error: 'HTTP 错误',
|
||||
upstream_busy: '上游繁忙',
|
||||
network: '网络连接异常',
|
||||
stream: '流式读取异常',
|
||||
transient: '临时异常'
|
||||
};
|
||||
if (typeof window.t === 'function') {
|
||||
const translated = window.t('chat.einoRunRetryKind_' + key);
|
||||
if (translated && translated !== 'chat.einoRunRetryKind_' + key) return translated;
|
||||
}
|
||||
return labels[key] || key;
|
||||
}
|
||||
|
||||
function formatProcessDetailEinoRunRetryTitle(data) {
|
||||
if (typeof window.formatEinoRunRetryTitle === 'function') {
|
||||
return window.formatEinoRunRetryTitle(data);
|
||||
}
|
||||
const d = data && typeof data === 'object' ? data : {};
|
||||
const base = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryTitle')
|
||||
: '🔁 临时错误重试';
|
||||
const attempt = Number(d.attempt || 0);
|
||||
const maxAttempts = Number(d.maxAttempts || 0);
|
||||
if (Number.isFinite(attempt) && attempt > 0 && Number.isFinite(maxAttempts) && maxAttempts > 0) {
|
||||
return base + '(' + attempt + '/' + maxAttempts + ')';
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
function formatProcessDetailEinoRunRetryMessage(message, data) {
|
||||
if (typeof window.formatEinoRunRetryMessage === 'function') {
|
||||
return window.formatEinoRunRetryMessage(message, data);
|
||||
}
|
||||
const d = data && typeof data === 'object' ? data : {};
|
||||
const base = String(message || '').trim();
|
||||
const errRaw = d.errorSummary != null && String(d.errorSummary).trim() !== ''
|
||||
? String(d.errorSummary).trim()
|
||||
: (d.error != null ? String(d.error).trim() : '');
|
||||
const lines = [];
|
||||
if (base) lines.push(base);
|
||||
const attempt = Number(d.attempt || 0);
|
||||
const maxAttempts = Number(d.maxAttempts || 0);
|
||||
const backoffSec = Number(d.backoffSec || 0);
|
||||
const kind = formatProcessDetailEinoRunRetryKind(d.errorKind);
|
||||
if (Number.isFinite(attempt) && attempt > 0 && Number.isFinite(maxAttempts) && maxAttempts > 0) {
|
||||
const retryPlan = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryPlan', { attempt: attempt, maxAttempts: maxAttempts, backoffSec: Number.isFinite(backoffSec) && backoffSec > 0 ? backoffSec : '-' })
|
||||
: ('重试进度:第 ' + attempt + '/' + maxAttempts + ' 次,等待 ' + (Number.isFinite(backoffSec) && backoffSec > 0 ? backoffSec : '-') + ' 秒');
|
||||
if (!base || base.indexOf(String(attempt) + '/' + String(maxAttempts)) === -1) {
|
||||
lines.push(retryPlan);
|
||||
}
|
||||
}
|
||||
if (kind) {
|
||||
const kindLabel = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryReasonKind')
|
||||
: '原因类型';
|
||||
lines.push(kindLabel + ':' + kind);
|
||||
}
|
||||
if (errRaw && (!base || base.indexOf(errRaw) === -1)) {
|
||||
const detailLabel = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryErrorDetail')
|
||||
: '错误详情';
|
||||
lines.push(detailLabel + ':' + errRaw);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function renderOneProcessDetail(detail) {
|
||||
const eventType = detail.eventType || '';
|
||||
const title = detail.message || '';
|
||||
@@ -2745,19 +2822,8 @@ function renderProcessDetails(messageId, processDetails, options) {
|
||||
? window.t('chat.einoEmptyResponseContinueTitle')
|
||||
: '🔁 自动续跑(无助手正文)';
|
||||
} else if (eventType === 'eino_run_retry') {
|
||||
itemTitle = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryTitle')
|
||||
: '🔁 临时错误重试';
|
||||
const errRaw = data && data.error != null ? String(data.error).trim() : '';
|
||||
if (errRaw) {
|
||||
const detailLabel = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryErrorDetail')
|
||||
: '错误详情';
|
||||
if (!title || String(title).indexOf(errRaw) === -1) {
|
||||
const merged = title ? (String(title) + '\n' + detailLabel + ':' + errRaw) : (detailLabel + ':' + errRaw);
|
||||
detail.message = merged;
|
||||
}
|
||||
}
|
||||
itemTitle = formatProcessDetailEinoRunRetryTitle(data);
|
||||
detail.message = formatProcessDetailEinoRunRetryMessage(title, data);
|
||||
} else if (eventType === 'knowledge_retrieval') {
|
||||
itemTitle = '📚 ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrieval') : '知识检索');
|
||||
} else if (eventType === 'error') {
|
||||
|
||||
+622
-31
@@ -6,11 +6,149 @@ function _t(key, opts) {
|
||||
const FOFA_FORM_STORAGE_KEY = 'info-collect-fofa-form';
|
||||
const FOFA_HIDDEN_FIELDS_STORAGE_KEY = 'info-collect-fofa-hidden-fields';
|
||||
|
||||
const INFO_COLLECT_PROVIDERS = {
|
||||
fofa: {
|
||||
label: 'FOFA',
|
||||
placeholder: '例如:app="Apache" && country="CN"',
|
||||
nlPlaceholder: '例如:找美国 Missouri 的 Apache 站点,标题包含 Home',
|
||||
hint: '查询语法参考 FOFA 文档,支持 && / || / () 等。',
|
||||
parseHint: '解析后会弹窗展示 FOFA 语法(可编辑),确认无误后再填入查询框并执行查询。',
|
||||
maxSize: 10000,
|
||||
sizeHint: 'FOFA 返回数量上限与账号权限相关,前端最多允许 10000。',
|
||||
fullOption: {
|
||||
label: '完整模式',
|
||||
hint: '向 FOFA 传 full=true,返回更完整/更实时的数据,可能消耗更多额度。'
|
||||
},
|
||||
fields: 'host,ip,port,domain,title,protocol,country,province,city,server',
|
||||
presets: [
|
||||
['Apache + 中国', 'app="Apache" && country="CN"'],
|
||||
['登录页 + 中国', 'title="登录" && country="CN"'],
|
||||
['指定域名', 'domain="example.com"'],
|
||||
['指定 IP', 'ip="1.1.1.1"']
|
||||
],
|
||||
fieldPresets: [
|
||||
['最小字段', 'host,ip,port,domain'],
|
||||
['Web 常用', 'host,title,ip,port,domain,protocol,server,icp,country,province,city'],
|
||||
['情报增强', 'host,ip,port,domain,title,protocol,country,province,city,server,as_number,as_organization,icp,header,banner']
|
||||
],
|
||||
syntaxGuide: {
|
||||
summary: 'FOFA 使用 field="value" 精确匹配,支持 &&、||、! 和括号组合;字符串建议用双引号包裹。',
|
||||
docsUrl: 'https://en.fofa.info/api',
|
||||
sections: [
|
||||
['常用字段', ['app="Apache"', 'title="后台管理"', 'body="Powered by"', 'domain="example.com"', 'host="https://example.com"', 'ip="1.1.1.1"', 'port="443"', 'country="CN"', 'city="Hangzhou"', 'server="nginx"']],
|
||||
['组合写法', ['app="nginx" && country="CN"', 'title="login" || title="登录"', '(app="Apache" || app="nginx") && port="443"', 'domain="example.com" && !title="404"']],
|
||||
['场景示例', ['cert="example.com" && port="443"', 'header="JSESSIONID" && country="CN"', 'icon_hash="-247388890"', 'fid="sZyXkR9e" && domain="example.com"']]
|
||||
]
|
||||
}
|
||||
},
|
||||
zoomeye: {
|
||||
label: 'ZoomEye',
|
||||
placeholder: '例如:app="Apache" && country="CN"',
|
||||
nlPlaceholder: '例如:找中国的 SSH 服务,排除蜜罐',
|
||||
hint: 'ZoomEye 支持 app/title/domain/ip/port/country/city 等语法。',
|
||||
parseHint: '解析后会弹窗展示 ZoomEye 语法(可编辑),确认无误后再填入查询框并执行查询。',
|
||||
maxSize: 10000,
|
||||
sizeHint: 'ZoomEye pagesize 最高支持到 10000,实际额度以账号为准。',
|
||||
fullOption: null,
|
||||
fields: 'ip,port,domain,hostname,title,service,app,country,city',
|
||||
presets: [
|
||||
['Apache + 中国', 'app="Apache" && country="CN"'],
|
||||
['SSH 服务', 'service="ssh"'],
|
||||
['指定域名', 'domain="example.com"'],
|
||||
['指定 IP', 'ip="1.1.1.1"']
|
||||
],
|
||||
fieldPresets: [
|
||||
['最小字段', 'ip,port,domain,hostname'],
|
||||
['Web 常用', 'ip,port,domain,hostname,title,service,app,country,city'],
|
||||
['情报增强', 'ip,port,domain,hostname,title,service,app,country,city,org,isp,ssl']
|
||||
],
|
||||
syntaxGuide: {
|
||||
summary: 'ZoomEye 支持字段检索、引号短语、AND/OR/NOT 与括号组合;字段名以官方控制台实际支持为准。',
|
||||
docsUrl: 'https://www.zoomeye.ai/help',
|
||||
sections: [
|
||||
['常用字段', ['app="Apache"', 'service="ssh"', 'title="登录"', 'domain="example.com"', 'hostname="example.com"', 'ip="1.1.1.1"', 'port=443', 'country="CN"', 'city="Beijing"', 'org="Tencent"']],
|
||||
['组合写法', ['app="nginx" AND country="CN"', 'service="http" AND (title="login" OR title="登录")', 'domain="example.com" AND NOT app="cloudflare"', 'port=443 AND country="US"']],
|
||||
['场景示例', ['ssl.cert.fingerprint="SHA256值"', 'iconhash="-247388890"', 'service="rdp" AND country="CN"', 'app="Elasticsearch" AND port=9200']]
|
||||
]
|
||||
}
|
||||
},
|
||||
quake: {
|
||||
label: 'Quake',
|
||||
placeholder: '例如:service.name:"http" AND country_cn:"中国"',
|
||||
nlPlaceholder: '例如:找中国的 HTTP 服务,标题包含登录',
|
||||
hint: 'Quake 使用 DSL 语法,常见字段如 service.name、domain、ip、port、country_cn。',
|
||||
parseHint: '解析后会弹窗展示 Quake DSL(可编辑),确认无误后再填入查询框并执行查询。',
|
||||
maxSize: 10000,
|
||||
sizeHint: 'Quake size 会消耗积分,建议按需控制返回数量。',
|
||||
fullOption: {
|
||||
label: '最新数据',
|
||||
hint: '向 Quake 传 latest=true,优先查询最新数据。'
|
||||
},
|
||||
fields: 'ip,port,domain,service.name,service.http.title,location.country_cn,location.province_cn,location.city_cn',
|
||||
presets: [
|
||||
['HTTP + 中国', 'service.name:"http" AND country_cn:"中国"'],
|
||||
['443 端口', 'port:443'],
|
||||
['指定域名', 'domain:"example.com"'],
|
||||
['指定 IP', 'ip:"1.1.1.1"']
|
||||
],
|
||||
fieldPresets: [
|
||||
['最小字段', 'ip,port,domain'],
|
||||
['Web 常用', 'ip,port,domain,service.name,service.http.title,location.country_cn,location.city_cn'],
|
||||
['情报增强', 'ip,port,domain,service.name,service.http.title,service.http.server,location.country_cn,location.province_cn,location.city_cn,asn']
|
||||
],
|
||||
syntaxGuide: {
|
||||
summary: 'Quake 使用 Lucene/DSL 风格查询,常见形式是 field:"value",逻辑运算符通常使用 AND、OR、NOT。',
|
||||
docsUrl: 'https://quake.360.net/quake/#/help',
|
||||
sections: [
|
||||
['常用字段', ['service.name:"http"', 'service.http.title:"登录"', 'service.http.server:"nginx"', 'domain:"example.com"', 'ip:"1.1.1.1"', 'port:443', 'country_cn:"中国"', 'province_cn:"浙江"', 'city_cn:"杭州"']],
|
||||
['组合写法', ['service.name:"http" AND country_cn:"中国"', '(service.name:"http" OR service.name:"https") AND port:443', 'domain:"example.com" AND NOT service.http.title:"404"', 'service.http.title:"login" AND port:443']],
|
||||
['场景示例', ['service.http.favicon.hash:"-247388890"', 'service.http.response.header:"JSESSIONID"', 'service.name:"ssh" AND country_cn:"中国"', 'service.http.title:"Dashboard" AND NOT ip:"127.0.0.1"']]
|
||||
]
|
||||
}
|
||||
},
|
||||
shodan: {
|
||||
label: 'Shodan',
|
||||
placeholder: '例如:product:nginx country:CN',
|
||||
nlPlaceholder: '例如:找中国的 nginx 资产,端口 443',
|
||||
hint: 'Shodan 使用 filter:value 语法,常见字段如 product、port、country、org。',
|
||||
parseHint: '解析后会弹窗展示 Shodan filter 语法(可编辑),确认无误后再填入查询框并执行查询。',
|
||||
maxSize: 1000,
|
||||
sizeHint: 'Shodan 官方每页 100 条;后端会自动翻页聚合,单次最多 1000 条以控制额度消耗。',
|
||||
fullOption: null,
|
||||
fields: 'ip_str,port,hostnames,domains,org,isp,location.country_name,location.city,product,transport',
|
||||
presets: [
|
||||
['Nginx + 中国', 'product:nginx country:CN'],
|
||||
['SSH 服务', 'port:22'],
|
||||
['证书域名', 'ssl.cert.subject.cn:example.com'],
|
||||
['Amazon 443', 'org:"Amazon" port:443']
|
||||
],
|
||||
fieldPresets: [
|
||||
['最小字段', 'ip_str,port,hostnames,domains'],
|
||||
['Web 常用', 'ip_str,port,hostnames,domains,product,org,location.country_name,location.city'],
|
||||
['情报增强', 'ip_str,port,hostnames,domains,org,isp,asn,location.country_name,location.city,product,transport,ssl.cert.subject.cn']
|
||||
],
|
||||
syntaxGuide: {
|
||||
summary: 'Shodan 默认搜索 banner data;精确条件使用 filter:value,值含空格时用双引号,多个过滤器并列表示收窄结果。',
|
||||
docsUrl: 'https://help.shodan.io/the-basics/search-query-fundamentals',
|
||||
sections: [
|
||||
['常用过滤器', ['product:nginx', 'port:443', 'country:CN', 'city:Shanghai', 'org:"Amazon"', 'asn:AS15169', 'hostname:example.com', 'ssl.cert.subject.cn:example.com', 'http.title:"Dashboard"']],
|
||||
['组合写法', ['product:nginx country:CN', 'apache port:443 country:DE', 'org:"Amazon" port:443', 'ssl.cert.subject.cn:example.com port:443']],
|
||||
['场景示例', ['http.title:"login" country:CN', 'ssl:true port:443 hostname:example.com', 'vuln:CVE-2021-41773', 'has_screenshot:true product:nginx']]
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const infoCollectState = {
|
||||
currentPayload: null, // { fields, results, query, total, page, size }
|
||||
hiddenFields: new Set(),
|
||||
selectedRowIndexes: new Set(),
|
||||
tableBound: false
|
||||
tableBound: false,
|
||||
providerSelectBound: false,
|
||||
presetEventsBound: false,
|
||||
syntaxGuideExpanded: false,
|
||||
queryHeightFrame: null,
|
||||
queryHeightResizeBound: false
|
||||
};
|
||||
|
||||
// AI 解析(自然语言 -> FOFA)交互状态
|
||||
@@ -31,6 +169,7 @@ if (typeof escapeHtml === 'undefined') {
|
||||
function getFofaFormElements() {
|
||||
return {
|
||||
query: document.getElementById('fofa-query'),
|
||||
provider: document.getElementById('fofa-provider'),
|
||||
nl: document.getElementById('fofa-nl'),
|
||||
size: document.getElementById('fofa-size'),
|
||||
page: document.getElementById('fofa-page'),
|
||||
@@ -45,6 +184,25 @@ function getFofaFormElements() {
|
||||
};
|
||||
}
|
||||
|
||||
function getInfoCollectProvider() {
|
||||
const provider = (document.getElementById('fofa-provider')?.value || 'fofa').trim().toLowerCase();
|
||||
return INFO_COLLECT_PROVIDERS[provider] ? provider : 'fofa';
|
||||
}
|
||||
|
||||
function providerLabel(provider) {
|
||||
return (INFO_COLLECT_PROVIDERS[provider] || INFO_COLLECT_PROVIDERS.fofa).label;
|
||||
}
|
||||
|
||||
function getInfoCollectFullOption(provider) {
|
||||
const cfg = INFO_COLLECT_PROVIDERS[provider] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
return cfg.fullOption || null;
|
||||
}
|
||||
|
||||
function isInfoCollectFullEnabled(provider) {
|
||||
const els = getFofaFormElements();
|
||||
return !!(getInfoCollectFullOption(provider) && els.full && els.full.checked);
|
||||
}
|
||||
|
||||
function loadHiddenFieldsFromStorage() {
|
||||
try {
|
||||
const raw = localStorage.getItem(FOFA_HIDDEN_FIELDS_STORAGE_KEY);
|
||||
@@ -94,13 +252,24 @@ function initInfoCollectPage() {
|
||||
|
||||
// 恢复上次输入
|
||||
const saved = loadFofaFormFromStorage();
|
||||
let shouldResetProviderFields = false;
|
||||
if (saved) {
|
||||
if (typeof saved.provider === 'string' && els.provider && INFO_COLLECT_PROVIDERS[saved.provider]) els.provider.value = saved.provider;
|
||||
if (typeof saved.query === 'string') els.query.value = saved.query;
|
||||
if (typeof saved.size === 'number' || typeof saved.size === 'string') els.size.value = saved.size;
|
||||
if (typeof saved.page === 'number' || typeof saved.page === 'string') els.page.value = saved.page;
|
||||
if (typeof saved.fields === 'string') els.fields.value = saved.fields;
|
||||
if (typeof saved.full === 'boolean') els.full.checked = saved.full;
|
||||
const provider = getInfoCollectProvider();
|
||||
const savedFields = String(saved.fields || '').trim();
|
||||
shouldResetProviderFields = provider !== 'fofa' && (
|
||||
savedFields === INFO_COLLECT_PROVIDERS.fofa.fields ||
|
||||
savedFields === 'host,ip,port,domain'
|
||||
);
|
||||
}
|
||||
initInfoCollectProviderSelect();
|
||||
bindInfoCollectPresetEvents();
|
||||
refreshInfoCollectProviderUI(shouldResetProviderFields);
|
||||
|
||||
// 绑定 Enter 快捷查询(在 query 里用 Ctrl/Cmd+Enter)
|
||||
els.query.addEventListener('keydown', (e) => {
|
||||
@@ -139,23 +308,391 @@ function initInfoCollectPage() {
|
||||
autoGrowTextarea(els.query);
|
||||
autoGrowTextarea(els.nl);
|
||||
}, 0);
|
||||
setInfoCollectQueryMode('syntax', { focus: false });
|
||||
if (!infoCollectState.queryHeightResizeBound) {
|
||||
infoCollectState.queryHeightResizeBound = true;
|
||||
window.addEventListener('resize', scheduleInfoCollectQueryCardHeightStabilize);
|
||||
}
|
||||
|
||||
// 绑定表格事件(事件委托,只绑定一次)
|
||||
bindFofaTableEvents();
|
||||
updateSelectedMeta();
|
||||
}
|
||||
|
||||
function handleInfoCollectProviderChange() {
|
||||
infoCollectState.syntaxGuideExpanded = false;
|
||||
refreshInfoCollectProviderUI(true);
|
||||
}
|
||||
|
||||
function setInfoCollectQueryMode(mode, options) {
|
||||
const shouldFocus = options?.focus !== false;
|
||||
const syntaxPanel = document.getElementById('info-collect-syntax-panel');
|
||||
const naturalPanel = document.getElementById('info-collect-natural-panel');
|
||||
|
||||
if (syntaxPanel) {
|
||||
syntaxPanel.hidden = false;
|
||||
syntaxPanel.classList.add('is-active');
|
||||
syntaxPanel.classList.add('is-generated-target');
|
||||
}
|
||||
if (naturalPanel) {
|
||||
naturalPanel.hidden = false;
|
||||
naturalPanel.classList.add('is-active');
|
||||
}
|
||||
|
||||
const queryLabel = document.getElementById('info-collect-query-label');
|
||||
const cfg = INFO_COLLECT_PROVIDERS[getInfoCollectProvider()] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
if (queryLabel) {
|
||||
queryLabel.textContent = cfg.label + ' 查询语法(可编辑,可直接查询)';
|
||||
}
|
||||
const nlLabel = document.getElementById('info-collect-nl-label');
|
||||
if (nlLabel) {
|
||||
nlLabel.textContent = '自然语言(可选,AI 解析为 ' + cfg.label + ' 语法)';
|
||||
}
|
||||
|
||||
if (shouldFocus) {
|
||||
const focusTarget = mode === 'natural' ? document.getElementById('fofa-nl') : document.getElementById('fofa-query');
|
||||
try { focusTarget?.focus(); } catch (e) { /* ignore */ }
|
||||
}
|
||||
scheduleInfoCollectQueryCardHeightStabilize();
|
||||
}
|
||||
|
||||
function scheduleInfoCollectQueryCardHeightStabilize() {
|
||||
if (infoCollectState.queryHeightFrame) {
|
||||
cancelAnimationFrame(infoCollectState.queryHeightFrame);
|
||||
}
|
||||
infoCollectState.queryHeightFrame = requestAnimationFrame(() => {
|
||||
infoCollectState.queryHeightFrame = null;
|
||||
stabilizeInfoCollectQueryCardHeight();
|
||||
});
|
||||
}
|
||||
|
||||
function stabilizeInfoCollectQueryCardHeight() {
|
||||
const card = document.querySelector('.info-collect-query-card');
|
||||
if (!card) return;
|
||||
const rect = card.getBoundingClientRect();
|
||||
if (!rect.width) return;
|
||||
|
||||
const clone = card.cloneNode(true);
|
||||
clone.style.position = 'absolute';
|
||||
clone.style.visibility = 'hidden';
|
||||
clone.style.pointerEvents = 'none';
|
||||
clone.style.left = '-10000px';
|
||||
clone.style.top = '0';
|
||||
clone.style.width = rect.width + 'px';
|
||||
clone.style.height = 'auto';
|
||||
clone.style.minHeight = '0';
|
||||
clone.style.maxHeight = 'none';
|
||||
|
||||
const naturalPanel = clone.querySelector('#info-collect-natural-panel');
|
||||
if (naturalPanel) {
|
||||
naturalPanel.hidden = false;
|
||||
naturalPanel.classList.add('is-active');
|
||||
}
|
||||
const syntaxPanel = clone.querySelector('#info-collect-syntax-panel');
|
||||
if (syntaxPanel) {
|
||||
syntaxPanel.hidden = false;
|
||||
syntaxPanel.classList.add('is-active', 'is-generated-target');
|
||||
}
|
||||
const queryLabel = clone.querySelector('#info-collect-query-label');
|
||||
if (queryLabel) {
|
||||
const cfg = INFO_COLLECT_PROVIDERS[getInfoCollectProvider()] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
queryLabel.textContent = cfg.label + ' 查询语法(可编辑,可直接查询)';
|
||||
}
|
||||
|
||||
document.body.appendChild(clone);
|
||||
const stableHeight = Math.ceil(clone.getBoundingClientRect().height);
|
||||
clone.remove();
|
||||
if (stableHeight > 0) {
|
||||
card.style.minHeight = stableHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
function presetDataAttr(value) {
|
||||
return escapeHtml(String(value == null ? '' : value))
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function bindInfoCollectPresetEvents() {
|
||||
if (infoCollectState.presetEventsBound) return;
|
||||
infoCollectState.presetEventsBound = true;
|
||||
document.addEventListener('click', (event) => {
|
||||
const queryBtn = event.target.closest?.('[data-info-query-preset]');
|
||||
if (queryBtn) {
|
||||
event.preventDefault();
|
||||
applyFofaQueryPreset(queryBtn.getAttribute('data-info-query-preset') || '');
|
||||
return;
|
||||
}
|
||||
const fieldsBtn = event.target.closest?.('[data-info-fields-preset]');
|
||||
if (fieldsBtn) {
|
||||
event.preventDefault();
|
||||
applyFofaFieldsPreset(fieldsBtn.getAttribute('data-info-fields-preset') || '');
|
||||
return;
|
||||
}
|
||||
const guideToggle = event.target.closest?.('[data-info-syntax-guide-toggle]');
|
||||
if (guideToggle) {
|
||||
event.preventDefault();
|
||||
toggleInfoCollectSyntaxGuide();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function initInfoCollectProviderSelect() {
|
||||
const els = getFofaFormElements();
|
||||
const select = els.provider;
|
||||
if (!select || infoCollectState.providerSelectBound) {
|
||||
syncInfoCollectProviderSelect();
|
||||
return;
|
||||
}
|
||||
infoCollectState.providerSelectBound = true;
|
||||
select.classList.add('settings-native-select');
|
||||
select.tabIndex = -1;
|
||||
select.setAttribute('aria-hidden', 'true');
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'settings-custom-select info-collect-provider-select';
|
||||
|
||||
const trigger = document.createElement('button');
|
||||
trigger.type = 'button';
|
||||
trigger.className = 'settings-custom-select-trigger';
|
||||
trigger.setAttribute('aria-haspopup', 'listbox');
|
||||
trigger.setAttribute('aria-expanded', 'false');
|
||||
|
||||
const value = document.createElement('span');
|
||||
value.className = 'settings-custom-select-value';
|
||||
value.id = 'info-collect-provider-select-value';
|
||||
const caret = document.createElement('span');
|
||||
caret.className = 'settings-custom-select-caret';
|
||||
caret.setAttribute('aria-hidden', 'true');
|
||||
caret.textContent = '▾';
|
||||
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'settings-custom-select-menu';
|
||||
menu.id = 'info-collect-provider-select-menu';
|
||||
menu.setAttribute('role', 'listbox');
|
||||
|
||||
trigger.appendChild(value);
|
||||
trigger.appendChild(caret);
|
||||
select.parentNode.insertBefore(wrapper, select);
|
||||
wrapper.appendChild(trigger);
|
||||
wrapper.appendChild(menu);
|
||||
wrapper.appendChild(select);
|
||||
|
||||
trigger.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const willOpen = !wrapper.classList.contains('open');
|
||||
closeInfoCollectProviderSelect();
|
||||
wrapper.classList.toggle('open', willOpen);
|
||||
trigger.setAttribute('aria-expanded', willOpen ? 'true' : 'false');
|
||||
});
|
||||
|
||||
trigger.addEventListener('keydown', (event) => {
|
||||
const options = Array.prototype.filter.call(select.options, (option) => !option.disabled);
|
||||
if (!options.length) return;
|
||||
const current = Math.max(0, options.indexOf(select.options[select.selectedIndex]));
|
||||
let next = current;
|
||||
if (event.key === 'ArrowDown') next = Math.min(options.length - 1, current + 1);
|
||||
else if (event.key === 'ArrowUp') next = Math.max(0, current - 1);
|
||||
else if (event.key === 'Home') next = 0;
|
||||
else if (event.key === 'End') next = options.length - 1;
|
||||
else if (event.key === 'Escape') {
|
||||
closeInfoCollectProviderSelect();
|
||||
return;
|
||||
} else if (event.key === 'Enter' || event.key === ' ') {
|
||||
wrapper.classList.add('open');
|
||||
trigger.setAttribute('aria-expanded', 'true');
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const nextOption = options[next];
|
||||
if (nextOption && select.value !== nextOption.value) {
|
||||
select.value = nextOption.value;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
syncInfoCollectProviderSelect();
|
||||
});
|
||||
|
||||
menu.addEventListener('click', (event) => {
|
||||
const item = event.target.closest('.settings-custom-select-option');
|
||||
if (!item || item.disabled) return;
|
||||
event.stopPropagation();
|
||||
const option = select.options[Number(item.dataset.index)];
|
||||
if (option && !option.disabled && select.value !== option.value) {
|
||||
select.value = option.value;
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
}
|
||||
syncInfoCollectProviderSelect();
|
||||
closeInfoCollectProviderSelect();
|
||||
});
|
||||
|
||||
select.addEventListener('change', syncInfoCollectProviderSelect);
|
||||
document.addEventListener('click', closeInfoCollectProviderSelect);
|
||||
document.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Escape') closeInfoCollectProviderSelect();
|
||||
});
|
||||
syncInfoCollectProviderSelect();
|
||||
}
|
||||
|
||||
function closeInfoCollectProviderSelect() {
|
||||
const wrapper = document.querySelector('.info-collect-provider-select');
|
||||
const trigger = wrapper?.querySelector('.settings-custom-select-trigger');
|
||||
if (!wrapper) return;
|
||||
wrapper.classList.remove('open');
|
||||
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
||||
}
|
||||
|
||||
function syncInfoCollectProviderSelect() {
|
||||
const select = document.getElementById('fofa-provider');
|
||||
const wrapper = document.querySelector('.info-collect-provider-select');
|
||||
if (!select || !wrapper) return;
|
||||
const value = wrapper.querySelector('.settings-custom-select-value');
|
||||
const menu = wrapper.querySelector('.settings-custom-select-menu');
|
||||
const selected = select.options[select.selectedIndex];
|
||||
if (value) value.textContent = selected ? selected.textContent : '';
|
||||
if (!menu) return;
|
||||
menu.innerHTML = '';
|
||||
Array.prototype.forEach.call(select.options, (option, index) => {
|
||||
const item = document.createElement('button');
|
||||
item.type = 'button';
|
||||
item.className = 'settings-custom-select-option';
|
||||
item.setAttribute('role', 'option');
|
||||
item.setAttribute('data-index', String(index));
|
||||
item.setAttribute('aria-selected', option.selected ? 'true' : 'false');
|
||||
item.classList.toggle('is-selected', option.selected);
|
||||
item.disabled = !!option.disabled;
|
||||
const check = document.createElement('span');
|
||||
check.className = 'settings-custom-select-check';
|
||||
check.setAttribute('aria-hidden', 'true');
|
||||
check.textContent = '✓';
|
||||
const label = document.createElement('span');
|
||||
label.className = 'settings-custom-select-label';
|
||||
label.textContent = option.textContent;
|
||||
item.appendChild(check);
|
||||
item.appendChild(label);
|
||||
menu.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function renderInfoCollectSyntaxGuide(cfg) {
|
||||
const container = document.getElementById('info-collect-syntax-guide');
|
||||
if (!container) return;
|
||||
const guide = cfg.syntaxGuide;
|
||||
if (!guide) {
|
||||
container.innerHTML = '';
|
||||
container.hidden = true;
|
||||
return;
|
||||
}
|
||||
const docsLink = guide.docsUrl
|
||||
? `<a class="info-collect-doc-link" href="${presetDataAttr(guide.docsUrl)}" target="_blank" rel="noopener noreferrer">官方文档</a>`
|
||||
: '';
|
||||
const expanded = !!infoCollectState.syntaxGuideExpanded;
|
||||
const sections = (guide.sections || []).map(([title, examples]) => {
|
||||
const chips = (examples || []).map(example => {
|
||||
return `<button class="syntax-example-chip" type="button" data-info-query-preset="${presetDataAttr(example)}" title="填入查询框">${escapeHtml(example)}</button>`;
|
||||
}).join('');
|
||||
return `<div class="syntax-guide-section"><div class="syntax-guide-title">${escapeHtml(title)}</div><div class="syntax-guide-examples">${chips}</div></div>`;
|
||||
}).join('');
|
||||
container.hidden = false;
|
||||
container.classList.toggle('is-expanded', expanded);
|
||||
container.innerHTML = `
|
||||
<div class="syntax-guide-header">
|
||||
<div class="syntax-guide-summary">${escapeHtml(guide.summary || '')}</div>
|
||||
<div class="syntax-guide-actions">
|
||||
${docsLink}
|
||||
<button class="syntax-guide-toggle" type="button" data-info-syntax-guide-toggle aria-expanded="${expanded ? 'true' : 'false'}">${expanded ? '收起示例' : '展开示例'}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="syntax-guide-body"${expanded ? '' : ' hidden'}>${sections}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function toggleInfoCollectSyntaxGuide() {
|
||||
infoCollectState.syntaxGuideExpanded = !infoCollectState.syntaxGuideExpanded;
|
||||
const provider = getInfoCollectProvider();
|
||||
const cfg = INFO_COLLECT_PROVIDERS[provider] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
renderInfoCollectSyntaxGuide(cfg);
|
||||
scheduleInfoCollectQueryCardHeightStabilize();
|
||||
}
|
||||
|
||||
function refreshInfoCollectProviderUI(resetProviderFields) {
|
||||
const els = getFofaFormElements();
|
||||
const provider = getInfoCollectProvider();
|
||||
const cfg = INFO_COLLECT_PROVIDERS[provider] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
const queryLabel = document.getElementById('info-collect-query-label');
|
||||
const nlLabel = document.getElementById('info-collect-nl-label');
|
||||
const queryHint = document.getElementById('info-collect-query-hint');
|
||||
const parseHint = document.getElementById('info-collect-parse-hint');
|
||||
const sizeHint = document.getElementById('info-collect-size-hint');
|
||||
const parseBtn = document.getElementById('fofa-nl-parse-btn');
|
||||
const presets = document.getElementById('info-collect-query-presets');
|
||||
const fieldPresets = document.getElementById('info-collect-fields-presets');
|
||||
const fullOption = document.getElementById('info-collect-full-option');
|
||||
const fullText = fullOption ? fullOption.querySelector('.checkbox-text') : null;
|
||||
const fullConfig = getInfoCollectFullOption(provider);
|
||||
if (queryLabel) queryLabel.textContent = cfg.label + ' 查询语法';
|
||||
if (nlLabel) nlLabel.textContent = '自然语言(AI 解析为 ' + cfg.label + ' 语法)';
|
||||
if (queryHint) queryHint.textContent = cfg.hint;
|
||||
if (parseHint) parseHint.textContent = cfg.parseHint;
|
||||
if (sizeHint) sizeHint.textContent = cfg.sizeHint;
|
||||
if (parseBtn && parseBtn.dataset.loading !== '1') parseBtn.title = '将自然语言解析为 ' + cfg.label + ' 查询语法';
|
||||
if (els.query) els.query.placeholder = cfg.placeholder;
|
||||
if (els.nl) els.nl.placeholder = cfg.nlPlaceholder;
|
||||
if (els.size) {
|
||||
els.size.max = String(cfg.maxSize || 10000);
|
||||
const currentSize = parseInt(els.size.value, 10) || 100;
|
||||
if (cfg.maxSize && currentSize > cfg.maxSize) els.size.value = cfg.maxSize;
|
||||
}
|
||||
if (fullOption) {
|
||||
if (fullConfig) {
|
||||
fullOption.hidden = false;
|
||||
fullOption.title = fullConfig.hint || '';
|
||||
if (fullText) fullText.textContent = fullConfig.label || _t('infoCollectPage.fullLabel');
|
||||
} else {
|
||||
fullOption.hidden = true;
|
||||
fullOption.title = '';
|
||||
if (els.full) els.full.checked = false;
|
||||
}
|
||||
}
|
||||
if (els.fields && (resetProviderFields || !els.fields.value.trim())) els.fields.value = cfg.fields;
|
||||
if (presets) {
|
||||
presets.innerHTML = cfg.presets.map(([label, query]) => {
|
||||
return `<button class="preset-chip" type="button" data-info-query-preset="${presetDataAttr(query)}" title="填入示例">${escapeHtml(label)}</button>`;
|
||||
}).join('');
|
||||
}
|
||||
if (fieldPresets) {
|
||||
fieldPresets.innerHTML = cfg.fieldPresets.map(([label, fields]) => {
|
||||
return `<button class="preset-chip" type="button" data-info-fields-preset="${presetDataAttr(fields)}" title="填入字段模板">${escapeHtml(label)}</button>`;
|
||||
}).join('');
|
||||
}
|
||||
renderInfoCollectSyntaxGuide(cfg);
|
||||
saveFofaFormToStorage({
|
||||
provider,
|
||||
query: (els.query?.value || '').trim(),
|
||||
size: parseInt(els.size?.value, 10) || 100,
|
||||
page: parseInt(els.page?.value, 10) || 1,
|
||||
fields: els.fields?.value || '',
|
||||
full: isInfoCollectFullEnabled(provider)
|
||||
});
|
||||
setInfoCollectQueryMode('syntax', { focus: false });
|
||||
scheduleInfoCollectQueryCardHeightStabilize();
|
||||
}
|
||||
|
||||
function applyFofaQueryPreset(preset) {
|
||||
const els = getFofaFormElements();
|
||||
if (!els.query) return;
|
||||
setInfoCollectQueryMode('syntax');
|
||||
els.query.value = (preset || '').trim();
|
||||
els.query.focus();
|
||||
saveFofaFormToStorage({
|
||||
provider: getInfoCollectProvider(),
|
||||
query: els.query.value,
|
||||
size: parseInt(els.size?.value, 10) || 100,
|
||||
page: parseInt(els.page?.value, 10) || 1,
|
||||
fields: els.fields?.value || '',
|
||||
full: !!els.full?.checked
|
||||
full: isInfoCollectFullEnabled(getInfoCollectProvider())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -165,54 +702,68 @@ function applyFofaFieldsPreset(preset) {
|
||||
els.fields.value = (preset || '').trim();
|
||||
els.fields.focus();
|
||||
saveFofaFormToStorage({
|
||||
provider: getInfoCollectProvider(),
|
||||
query: (els.query?.value || '').trim(),
|
||||
size: parseInt(els.size?.value, 10) || 100,
|
||||
page: parseInt(els.page?.value, 10) || 1,
|
||||
fields: els.fields.value,
|
||||
full: !!els.full?.checked
|
||||
full: isInfoCollectFullEnabled(getInfoCollectProvider())
|
||||
});
|
||||
}
|
||||
|
||||
function resetFofaForm() {
|
||||
const els = getFofaFormElements();
|
||||
if (!els.query) return;
|
||||
const provider = getInfoCollectProvider();
|
||||
const cfg = INFO_COLLECT_PROVIDERS[provider] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
els.query.value = '';
|
||||
if (els.size) els.size.value = 100;
|
||||
if (els.page) els.page.value = 1;
|
||||
if (els.fields) els.fields.value = 'host,ip,port,domain,title,protocol,country,province,city,server';
|
||||
if (els.fields) els.fields.value = cfg.fields;
|
||||
if (els.full) els.full.checked = false;
|
||||
if (els.nl) els.nl.value = '';
|
||||
setInfoCollectQueryMode('syntax');
|
||||
saveFofaFormToStorage({
|
||||
provider,
|
||||
query: els.query.value,
|
||||
size: parseInt(els.size?.value, 10) || 100,
|
||||
page: parseInt(els.page?.value, 10) || 1,
|
||||
fields: els.fields?.value || '',
|
||||
full: !!els.full?.checked
|
||||
full: isInfoCollectFullEnabled(provider)
|
||||
});
|
||||
renderFofaResults({ query: '', fields: [], results: [], total: 0, page: 1, size: 0 });
|
||||
}
|
||||
|
||||
async function submitFofaSearch() {
|
||||
const els = getFofaFormElements();
|
||||
const provider = getInfoCollectProvider();
|
||||
const query = (els.query?.value || '').trim();
|
||||
const size = parseInt(els.size?.value, 10) || 100;
|
||||
const providerCfg = INFO_COLLECT_PROVIDERS[provider] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
const maxSize = providerCfg.maxSize || 10000;
|
||||
let size = parseInt(els.size?.value, 10) || 100;
|
||||
if (size > maxSize) {
|
||||
size = maxSize;
|
||||
if (els.size) els.size.value = String(maxSize);
|
||||
showInlineToast(providerCfg.label + ' 单次最多返回 ' + maxSize + ' 条,已自动调整。');
|
||||
}
|
||||
const page = parseInt(els.page?.value, 10) || 1;
|
||||
const fields = (els.fields?.value || '').trim();
|
||||
const full = !!els.full?.checked;
|
||||
const full = isInfoCollectFullEnabled(provider);
|
||||
|
||||
if (!query) {
|
||||
alert(_t('infoCollect.enterFofaQuery'));
|
||||
return;
|
||||
}
|
||||
|
||||
saveFofaFormToStorage({ query, size, page, fields, full });
|
||||
setFofaMeta(_t('infoCollect.querying'));
|
||||
saveFofaFormToStorage({ provider, query, size, page, fields, full });
|
||||
setFofaMeta(providerLabel(provider) + ' ' + _t('infoCollect.querying'));
|
||||
setFofaLoading(true);
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/api/fofa/search', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, size, page, fields, full })
|
||||
body: JSON.stringify({ provider, query, size, page, fields, full })
|
||||
});
|
||||
|
||||
const result = await response.json().catch(() => ({}));
|
||||
@@ -221,9 +772,9 @@ async function submitFofaSearch() {
|
||||
}
|
||||
renderFofaResults(result);
|
||||
} catch (e) {
|
||||
console.error('FOFA 查询失败:', e);
|
||||
console.error(providerLabel(provider) + ' 查询失败:', e);
|
||||
setFofaMeta(_t('infoCollect.queryFailed'));
|
||||
renderFofaResults({ query, fields: [], results: [], total: 0, page: 1, size: 0 });
|
||||
renderFofaResults({ provider, query, fields: [], results: [], total: 0, page: 1, size: 0 });
|
||||
alert(_t('infoCollect.queryFailed') + ': ' + (e && e.message ? e.message : String(e)));
|
||||
} finally {
|
||||
setFofaLoading(false);
|
||||
@@ -232,6 +783,7 @@ async function submitFofaSearch() {
|
||||
|
||||
async function parseFofaNaturalLanguage() {
|
||||
const els = getFofaFormElements();
|
||||
const provider = getInfoCollectProvider();
|
||||
const text = (els.nl?.value || '').trim();
|
||||
if (!text) {
|
||||
alert(_t('infoCollect.enterNaturalLanguage'));
|
||||
@@ -264,7 +816,7 @@ async function parseFofaNaturalLanguage() {
|
||||
const resp = await apiFetch('/api/fofa/parse', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ text }),
|
||||
body: JSON.stringify({ provider, text }),
|
||||
signal: fofaParseAbortController.signal
|
||||
});
|
||||
const result = await resp.json().catch(() => ({}));
|
||||
@@ -309,9 +861,11 @@ function setFofaParseLoading(loading, statusText) {
|
||||
btn.setAttribute('aria-busy', 'true');
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
const provider = getInfoCollectProvider();
|
||||
const cfg = INFO_COLLECT_PROVIDERS[provider] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
btn.classList.remove('btn-loading');
|
||||
btn.textContent = btn.dataset.originalText || _t('infoCollectPage.parseBtn');
|
||||
btn.title = _t('infoCollect.parseToFofa');
|
||||
btn.title = '将自然语言解析为 ' + cfg.label + ' 查询语法';
|
||||
btn.disabled = false;
|
||||
delete btn.dataset.loading;
|
||||
btn.removeAttribute('aria-busy');
|
||||
@@ -333,6 +887,8 @@ function showFofaParseModal(nlText, parsed) {
|
||||
const existing = document.getElementById('fofa-parse-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const provider = getInfoCollectProvider();
|
||||
const cfg = INFO_COLLECT_PROVIDERS[provider] || INFO_COLLECT_PROVIDERS.fofa;
|
||||
const safeNL = escapeHtml((nlText || '').trim());
|
||||
const warnings = Array.isArray(parsed?.warnings) ? parsed.warnings.filter(Boolean).map(x => String(x)) : [];
|
||||
const explanation = parsed?.explanation != null ? String(parsed.explanation) : '';
|
||||
@@ -360,8 +916,8 @@ function showFofaParseModal(nlText, parsed) {
|
||||
</div>
|
||||
|
||||
<div class="form-group info-collect-parse-form-group">
|
||||
<label for="fofa-parse-query">${_t('infoCollect.fofaQueryEditable')}</label>
|
||||
<textarea id="fofa-parse-query" class="info-collect-query-input" rows="2" placeholder="${_t('infoCollect.queryPlaceholder')}"></textarea>
|
||||
<label for="fofa-parse-query">${escapeHtml(cfg.label)} 查询语法(可编辑)</label>
|
||||
<textarea id="fofa-parse-query" class="info-collect-query-input" rows="2" placeholder="${escapeHtml(cfg.placeholder)}"></textarea>
|
||||
<small class="form-hint">${_t('infoCollect.confirmBeforeQuery')}</small>
|
||||
</div>
|
||||
|
||||
@@ -420,7 +976,7 @@ function showFofaParseModal(nlText, parsed) {
|
||||
size: parseInt(els.size?.value, 10) || 100,
|
||||
page: parseInt(els.page?.value, 10) || 1,
|
||||
fields: (els.fields?.value || '').trim(),
|
||||
full: !!els.full?.checked
|
||||
full: isInfoCollectFullEnabled(getInfoCollectProvider())
|
||||
});
|
||||
close();
|
||||
if (run) submitFofaSearch();
|
||||
@@ -447,6 +1003,22 @@ function setFofaMeta(text) {
|
||||
}
|
||||
}
|
||||
|
||||
function buildInfoCollectResultsMeta(provider, total, count, page, size, expectedCount, shortfall) {
|
||||
let text = providerLabel(provider) + ' · ' + _t('infoCollect.resultsMeta', { total, count, page, size });
|
||||
if (provider === 'shodan') {
|
||||
let expected = Number(expectedCount || 0);
|
||||
if (!Number.isFinite(expected) || expected <= 0) {
|
||||
const startOffset = Math.max(0, (Number(page) || 1) - 1) * 100;
|
||||
expected = Math.min(Number(size) || 0, Math.max(0, (Number(total) || 0) - startOffset));
|
||||
}
|
||||
const missing = Number(shortfall || 0);
|
||||
if (expected > 0 && (missing > 0 || count < expected)) {
|
||||
text += ' · ' + _t('infoCollect.providerReturnedFewer', { expected, count });
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function updateSelectedMeta() {
|
||||
const els = getFofaFormElements();
|
||||
if (els.selectedMeta) {
|
||||
@@ -473,6 +1045,7 @@ function renderFofaResults(payload) {
|
||||
|
||||
// 保存当前 payload 到 state
|
||||
infoCollectState.currentPayload = {
|
||||
provider: payload.provider || getInfoCollectProvider(),
|
||||
query: payload.query || '',
|
||||
total: typeof payload.total === 'number' ? payload.total : 0,
|
||||
page: typeof payload.page === 'number' ? payload.page : 1,
|
||||
@@ -496,7 +1069,15 @@ function renderFofaResults(payload) {
|
||||
const size = typeof payload.size === 'number' ? payload.size : 0;
|
||||
const page = typeof payload.page === 'number' ? payload.page : 1;
|
||||
|
||||
setFofaMeta(_t('infoCollect.resultsMeta', { total, count: results.length, page, size }));
|
||||
setFofaMeta(buildInfoCollectResultsMeta(
|
||||
infoCollectState.currentPayload.provider,
|
||||
total,
|
||||
results.length,
|
||||
page,
|
||||
size,
|
||||
typeof payload.expected_count === 'number' ? payload.expected_count : 0,
|
||||
typeof payload.shortfall === 'number' ? payload.shortfall : 0
|
||||
));
|
||||
|
||||
// 可见字段
|
||||
const visibleFields = fields.filter(f => !infoCollectState.hiddenFields.has(f));
|
||||
@@ -768,7 +1349,8 @@ function buildScanMessage(target, row, options) {
|
||||
const fields = Array.isArray(opts.fields) ? opts.fields : [];
|
||||
|
||||
const summary = formatFofaRowSummary(row || {}, fields);
|
||||
return `对以下目标做信息收集与基础扫描:\n${target}\n\n要求:\n1) 识别服务/框架与关键指纹\n2) 枚举开放端口与常见管理入口\n3) 用 httpx/指纹/目录探测等方式快速确认可访问面\n4) 输出可复现的命令与结论\n\n已知信息(来自 FOFA 该行全部字段):\n${summary}`.trim();
|
||||
const provider = providerLabel(infoCollectState.currentPayload?.provider || getInfoCollectProvider());
|
||||
return `对以下目标做信息收集与基础扫描:\n${target}\n\n要求:\n1) 识别服务/框架与关键指纹\n2) 枚举开放端口与常见管理入口\n3) 用 httpx/指纹/目录探测等方式快速确认可访问面\n4) 输出可复现的命令与结论\n\n已知信息(来自 ${provider} 该行全部字段):\n${summary}`.trim();
|
||||
}
|
||||
|
||||
function bindFofaTableEvents() {
|
||||
@@ -926,12 +1508,14 @@ function exportFofaResults(format) {
|
||||
|
||||
const fields = p.fields || [];
|
||||
const visibleFields = fields.filter(f => !infoCollectState.hiddenFields.has(f));
|
||||
const provider = p.provider || 'fofa';
|
||||
|
||||
const now = new Date();
|
||||
const ts = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}${String(now.getSeconds()).padStart(2, '0')}`;
|
||||
|
||||
if (format === 'json') {
|
||||
const payload = {
|
||||
provider,
|
||||
query: p.query || '',
|
||||
total: p.total || 0,
|
||||
page: p.page || 1,
|
||||
@@ -939,7 +1523,7 @@ function exportFofaResults(format) {
|
||||
fields: fields,
|
||||
results: p.results
|
||||
};
|
||||
downloadBlob(JSON.stringify(payload, null, 2), `fofa_results_${ts}.json`, 'application/json;charset=utf-8');
|
||||
downloadBlob(JSON.stringify(payload, null, 2), `${provider}_results_${ts}.json`, 'application/json;charset=utf-8');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -956,7 +1540,7 @@ function exportFofaResults(format) {
|
||||
const ws = XLSX.utils.aoa_to_sheet(aoa);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, _t('infoCollect.batchScanTitle'));
|
||||
XLSX.writeFile(wb, `fofa_results_${ts}.xlsx`);
|
||||
XLSX.writeFile(wb, `${provider}_results_${ts}.xlsx`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -968,7 +1552,7 @@ function exportFofaResults(format) {
|
||||
});
|
||||
const csv = [header.map(csvEscape).join(','), ...rows.map(cols => cols.join(','))].join('\n');
|
||||
const csvWithBom = '\uFEFF' + csv;
|
||||
downloadBlob(csvWithBom, `fofa_results_${ts}.csv`, 'text/csv;charset=utf-8');
|
||||
downloadBlob(csvWithBom, `${provider}_results_${ts}.csv`, 'text/csv;charset=utf-8');
|
||||
}
|
||||
|
||||
function csvEscape(value) {
|
||||
@@ -1074,13 +1658,19 @@ function showCellDetailModal(field, fullText) {
|
||||
const existing = document.getElementById('info-collect-cell-modal');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const text = fullText == null ? '' : String(fullText);
|
||||
const fieldName = field || _t('infoCollect.field');
|
||||
const charCountLabel = _t('infoCollect.cellValueLength', { count: Array.from(text).length });
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'info-collect-cell-modal';
|
||||
modal.className = 'info-collect-cell-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="info-collect-cell-modal-content" role="dialog" aria-modal="true">
|
||||
<div class="info-collect-cell-modal-header">
|
||||
<div class="info-collect-cell-modal-title">${escapeHtml(field || _t('infoCollect.field'))}</div>
|
||||
<div class="info-collect-cell-modal-heading">
|
||||
<div class="info-collect-cell-modal-title">${escapeHtml(fieldName)}</div>
|
||||
<div class="info-collect-cell-modal-subtitle">${escapeHtml(charCountLabel)}</div>
|
||||
</div>
|
||||
<button class="btn-icon" type="button" id="info-collect-cell-modal-close" title="${_t('common.close')}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
@@ -1088,7 +1678,7 @@ function showCellDetailModal(field, fullText) {
|
||||
</button>
|
||||
</div>
|
||||
<div class="info-collect-cell-modal-body">
|
||||
<pre class="info-collect-cell-modal-pre">${escapeHtml(fullText || '')}</pre>
|
||||
<pre class="info-collect-cell-modal-pre">${escapeHtml(text)}</pre>
|
||||
</div>
|
||||
<div class="info-collect-cell-modal-footer">
|
||||
<button class="btn-secondary" type="button" id="info-collect-cell-modal-copy">${_t('common.copy')}</button>
|
||||
@@ -1100,7 +1690,13 @@ function showCellDetailModal(field, fullText) {
|
||||
document.body.appendChild(modal);
|
||||
openAppModal(modal);
|
||||
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
};
|
||||
const close = function () {
|
||||
document.removeEventListener('keydown', onKey);
|
||||
closeAppModal(modal);
|
||||
modal.remove();
|
||||
syncAppModalBodyLock();
|
||||
@@ -1111,16 +1707,10 @@ function showCellDetailModal(field, fullText) {
|
||||
document.getElementById('info-collect-cell-modal-close')?.addEventListener('click', close);
|
||||
document.getElementById('info-collect-cell-modal-ok')?.addEventListener('click', close);
|
||||
document.getElementById('info-collect-cell-modal-copy')?.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(fullText || '').then(() => showInlineToast(_t('common.copied'))).catch(() => alert(_t('common.copyFailed')));
|
||||
navigator.clipboard.writeText(text).then(() => showInlineToast(_t('common.copied'))).catch(() => alert(_t('common.copyFailed')));
|
||||
});
|
||||
|
||||
// Esc 关闭
|
||||
const onKey = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
close();
|
||||
document.removeEventListener('keydown', onKey);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', onKey);
|
||||
}
|
||||
|
||||
@@ -1129,6 +1719,7 @@ window.initInfoCollectPage = initInfoCollectPage;
|
||||
window.resetFofaForm = resetFofaForm;
|
||||
window.submitFofaSearch = submitFofaSearch;
|
||||
window.parseFofaNaturalLanguage = parseFofaNaturalLanguage;
|
||||
window.setInfoCollectQueryMode = setInfoCollectQueryMode;
|
||||
window.scanFofaRow = scanFofaRow;
|
||||
window.copyFofaTarget = copyFofaTarget;
|
||||
window.copyFofaTargetEncoded = copyFofaTargetEncoded;
|
||||
|
||||
+65
-10
@@ -1915,17 +1915,72 @@ function mergeMcpExecutionIDLists(prev, next) {
|
||||
function formatEinoRunRetryMessage(message, data) {
|
||||
const d = data && typeof data === 'object' ? data : {};
|
||||
const base = String(message || '').trim();
|
||||
const errRaw = d.error != null ? String(d.error).trim() : '';
|
||||
const errRaw = d.errorSummary != null && String(d.errorSummary).trim() !== ''
|
||||
? String(d.errorSummary).trim()
|
||||
: (d.error != null ? String(d.error).trim() : '');
|
||||
const lines = [];
|
||||
if (base) lines.push(base);
|
||||
const attempt = Number(d.attempt || 0);
|
||||
const maxAttempts = Number(d.maxAttempts || 0);
|
||||
const backoffSec = Number(d.backoffSec || 0);
|
||||
const kind = formatEinoRunRetryKind(d.errorKind);
|
||||
if (Number.isFinite(attempt) && attempt > 0 && Number.isFinite(maxAttempts) && maxAttempts > 0) {
|
||||
const retryPlan = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryPlan', { attempt: attempt, maxAttempts: maxAttempts, backoffSec: Number.isFinite(backoffSec) && backoffSec > 0 ? backoffSec : '-' })
|
||||
: ('重试进度:第 ' + attempt + '/' + maxAttempts + ' 次,等待 ' + (Number.isFinite(backoffSec) && backoffSec > 0 ? backoffSec : '-') + ' 秒');
|
||||
if (!base || base.indexOf(String(attempt) + '/' + String(maxAttempts)) === -1) {
|
||||
lines.push(retryPlan);
|
||||
}
|
||||
}
|
||||
if (kind) {
|
||||
const kindLabel = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryReasonKind')
|
||||
: '原因类型';
|
||||
lines.push(kindLabel + ':' + kind);
|
||||
}
|
||||
if (!errRaw) {
|
||||
return base;
|
||||
return lines.join('\n');
|
||||
}
|
||||
const detailLabel = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryErrorDetail')
|
||||
: '错误详情';
|
||||
if (base && base.indexOf(errRaw) !== -1) {
|
||||
return base;
|
||||
if (!base || base.indexOf(errRaw) === -1) {
|
||||
lines.push(detailLabel + ':' + errRaw);
|
||||
}
|
||||
return base ? (base + '\n' + detailLabel + ':' + errRaw) : (detailLabel + ':' + errRaw);
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function formatEinoRunRetryKind(kind) {
|
||||
const key = String(kind || '').trim();
|
||||
if (!key) return '';
|
||||
const labels = {
|
||||
rate_limit: '限流 / 请求过多',
|
||||
retryable_http: '可重试 HTTP 错误',
|
||||
upstream_server: '上游服务错误',
|
||||
http_error: 'HTTP 错误',
|
||||
upstream_busy: '上游繁忙',
|
||||
network: '网络连接异常',
|
||||
stream: '流式读取异常',
|
||||
transient: '临时异常'
|
||||
};
|
||||
if (typeof window.t === 'function') {
|
||||
const translated = window.t('chat.einoRunRetryKind_' + key);
|
||||
if (translated && translated !== 'chat.einoRunRetryKind_' + key) return translated;
|
||||
}
|
||||
return labels[key] || key;
|
||||
}
|
||||
|
||||
function formatEinoRunRetryTitle(data) {
|
||||
const d = data && typeof data === 'object' ? data : {};
|
||||
const base = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryTitle')
|
||||
: '🔁 临时错误重试';
|
||||
const attempt = Number(d.attempt || 0);
|
||||
const maxAttempts = Number(d.maxAttempts || 0);
|
||||
if (Number.isFinite(attempt) && attempt > 0 && Number.isFinite(maxAttempts) && maxAttempts > 0) {
|
||||
return base + '(' + attempt + '/' + maxAttempts + ')';
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
// 处理流式事件
|
||||
@@ -2425,12 +2480,9 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
|
||||
case 'eino_run_retry': {
|
||||
const d = event.data || {};
|
||||
const title = typeof window.t === 'function'
|
||||
? window.t('chat.einoRunRetryTitle')
|
||||
: '🔁 临时错误重试';
|
||||
const msg = formatEinoRunRetryMessage(event.message, d);
|
||||
addTimelineItem(timeline, 'warning', {
|
||||
title: title,
|
||||
title: formatEinoRunRetryTitle(d),
|
||||
message: msg,
|
||||
data: d
|
||||
});
|
||||
@@ -4226,6 +4278,9 @@ function addTimelineItem(timeline, type, options) {
|
||||
const itemId = 'timeline-item-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
item.id = itemId;
|
||||
item.className = `timeline-item timeline-item-${type}`;
|
||||
if (type === 'eino_run_retry') {
|
||||
item.classList.add('timeline-item-warning');
|
||||
}
|
||||
// 记录类型与参数,便于 languagechange 时刷新标题文案
|
||||
item.dataset.timelineType = type;
|
||||
if (type === 'iteration') {
|
||||
@@ -4418,7 +4473,7 @@ function addTimelineItem(timeline, type, options) {
|
||||
${escapeHtml(options.message || taskCancelledLabel)}
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'warning' && options.message) {
|
||||
} else if ((type === 'warning' || type === 'eino_run_retry') && options.message) {
|
||||
const streamBody = typeof formatTimelineStreamBody === 'function'
|
||||
? formatTimelineStreamBody(options.message, options.data)
|
||||
: options.message;
|
||||
|
||||
@@ -711,6 +711,13 @@ async function loadConfig(loadTools = true, options = {}) {
|
||||
const fofaBaseUrlEl = document.getElementById('fofa-base-url');
|
||||
if (fofaKeyEl) fofaKeyEl.value = fofa.api_key || '';
|
||||
if (fofaBaseUrlEl) fofaBaseUrlEl.value = fofa.base_url || '';
|
||||
['zoomeye', 'quake', 'shodan'].forEach((name) => {
|
||||
const cfg = currentConfig[name] || {};
|
||||
const keyEl = document.getElementById(`${name}-api-key`);
|
||||
const baseUrlEl = document.getElementById(`${name}-base-url`);
|
||||
if (keyEl) keyEl.value = cfg.api_key || '';
|
||||
if (baseUrlEl) baseUrlEl.value = cfg.base_url || '';
|
||||
});
|
||||
|
||||
// 填充人机协同配置
|
||||
const hitl = currentConfig.hitl || {};
|
||||
@@ -1906,6 +1913,18 @@ async function applySettings() {
|
||||
api_key: document.getElementById('fofa-api-key')?.value.trim() || '',
|
||||
base_url: document.getElementById('fofa-base-url')?.value.trim() || ''
|
||||
},
|
||||
zoomeye: {
|
||||
api_key: document.getElementById('zoomeye-api-key')?.value.trim() || '',
|
||||
base_url: document.getElementById('zoomeye-base-url')?.value.trim() || ''
|
||||
},
|
||||
quake: {
|
||||
api_key: document.getElementById('quake-api-key')?.value.trim() || '',
|
||||
base_url: document.getElementById('quake-base-url')?.value.trim() || ''
|
||||
},
|
||||
shodan: {
|
||||
api_key: document.getElementById('shodan-api-key')?.value.trim() || '',
|
||||
base_url: document.getElementById('shodan-base-url')?.value.trim() || ''
|
||||
},
|
||||
hitl: {
|
||||
...prevHitl,
|
||||
audit_model: {
|
||||
|
||||
+116
-40
@@ -2026,51 +2026,77 @@
|
||||
<div class="page-content">
|
||||
<div class="info-collect-panel">
|
||||
<div class="info-collect-form">
|
||||
<div class="form-group">
|
||||
<label for="fofa-query" data-i18n="infoCollectPage.fofaQuerySyntax">FOFA 查询语法</label>
|
||||
<textarea id="fofa-query" class="info-collect-query-input" rows="1" data-i18n="infoCollect.queryPlaceholder" data-i18n-attr="placeholder" placeholder='例如:app="Apache" && country="CN"'></textarea>
|
||||
<small class="form-hint" data-i18n="infoCollectPage.formHint">查询语法参考 FOFA 文档,支持 && / || / () 等。</small>
|
||||
<div class="info-collect-presets" aria-label="FOFA 查询示例" data-i18n="infoCollectPage.queryPresetsAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('app="Apache" && country="CN"')" data-i18n="infoCollectPage.presetApache" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">Apache + 中国</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('title="登录" && country="CN"')" data-i18n="infoCollectPage.presetLogin" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">登录页 + 中国</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('domain="example.com"')" data-i18n="infoCollectPage.presetDomain" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">指定域名</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('ip="1.1.1.1"')" data-i18n="infoCollectPage.presetIp" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">指定 IP</button>
|
||||
<div class="info-collect-form-top">
|
||||
<div class="info-collect-provider-card form-group">
|
||||
<div class="info-collect-step-title">
|
||||
<span>01</span>
|
||||
<label for="fofa-provider">数据源</label>
|
||||
</div>
|
||||
<select id="fofa-provider" onchange="handleInfoCollectProviderChange()">
|
||||
<option value="fofa">FOFA</option>
|
||||
<option value="zoomeye">ZoomEye</option>
|
||||
<option value="quake">Quake</option>
|
||||
<option value="shodan">Shodan</option>
|
||||
</select>
|
||||
<small class="form-hint">支持 FOFA、ZoomEye、Quake、Shodan;API Key 可在系统设置或环境变量中配置。</small>
|
||||
</div>
|
||||
<div class="info-collect-query-card form-group">
|
||||
<div class="info-collect-step-title">
|
||||
<span>02</span>
|
||||
<div>查询方式</div>
|
||||
</div>
|
||||
<div id="info-collect-natural-panel" class="info-collect-query-mode-panel info-collect-natural-panel">
|
||||
<label for="fofa-nl" id="info-collect-nl-label" class="info-collect-inner-label">自然语言(可选,AI 解析为查询语法)</label>
|
||||
<div class="info-collect-nl-row">
|
||||
<textarea id="fofa-nl" class="info-collect-query-input" rows="1" placeholder="例如:找中国的 Apache 站点"></textarea>
|
||||
<button id="fofa-nl-parse-btn" class="btn-secondary" type="button" onclick="parseFofaNaturalLanguage()" data-i18n="infoCollectPage.parseBtn" data-i18n-attr="title" data-i18n-title="infoCollectPage.parseBtnTitle" title="将自然语言解析为 FOFA 查询语法">AI 解析</button>
|
||||
</div>
|
||||
<div id="fofa-nl-status" class="fofa-nl-status muted" style="display: none;" aria-live="polite"></div>
|
||||
<small class="form-hint" id="info-collect-parse-hint">解析后会弹窗展示对应数据源语法(可编辑),确认无误后再填入查询框并执行查询。</small>
|
||||
</div>
|
||||
<div id="info-collect-syntax-panel" class="info-collect-query-mode-panel is-active">
|
||||
<label for="fofa-query" id="info-collect-query-label" class="info-collect-inner-label">查询语法(可编辑,可直接查询)</label>
|
||||
<div class="info-collect-query-row">
|
||||
<textarea id="fofa-query" class="info-collect-query-input" rows="1" placeholder='例如:app="Apache" && country="CN"'></textarea>
|
||||
<button id="fofa-query-submit-btn" class="btn-primary" data-require-permission="fofa:execute" type="button" onclick="submitFofaSearch()">查询</button>
|
||||
</div>
|
||||
<small class="form-hint" id="info-collect-query-hint">选择数据源后会显示对应查询语法提示。</small>
|
||||
<div class="info-collect-presets" id="info-collect-query-presets" aria-label="查询示例"></div>
|
||||
<div class="info-collect-syntax-guide" id="info-collect-syntax-guide" hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fofa-nl" data-i18n="infoCollectPage.naturalLanguage">自然语言(AI 解析为 FOFA 语法)</label>
|
||||
<div class="info-collect-nl-row">
|
||||
<textarea id="fofa-nl" class="info-collect-query-input" rows="1" data-i18n="infoCollectPage.nlPlaceholder" data-i18n-attr="placeholder" placeholder="例如:找美国 Missouri 的 Apache 站点,标题包含 Home"></textarea>
|
||||
<button id="fofa-nl-parse-btn" class="btn-secondary" type="button" onclick="parseFofaNaturalLanguage()" data-i18n="infoCollectPage.parseBtn" data-i18n-attr="title" data-i18n-title="infoCollectPage.parseBtnTitle" title="将自然语言解析为 FOFA 查询语法">AI 解析</button>
|
||||
<div class="info-collect-options-card">
|
||||
<div class="info-collect-step-title">
|
||||
<span>03</span>
|
||||
<div>返回配置</div>
|
||||
</div>
|
||||
<div id="fofa-nl-status" class="fofa-nl-status muted" style="display: none;" aria-live="polite"></div>
|
||||
<small class="form-hint" data-i18n="infoCollectPage.parseHint">解析后会弹窗展示 FOFA 语法(可编辑),确认无误后再填入查询框并执行查询。</small>
|
||||
</div>
|
||||
<div class="info-collect-form-row">
|
||||
<div class="form-group">
|
||||
<label for="fofa-size" data-i18n="infoCollectPage.returnCount">返回数量</label>
|
||||
<input type="number" id="fofa-size" min="1" max="10000" value="100" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fofa-page" data-i18n="infoCollectPage.pageNum">页码</label>
|
||||
<input type="number" id="fofa-page" min="1" value="1" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label" style="margin-top: 24px;">
|
||||
<input type="checkbox" id="fofa-full" class="modern-checkbox" />
|
||||
<span class="checkbox-custom"></span>
|
||||
<span class="checkbox-text" data-i18n="infoCollectPage.fullLabel">full</span>
|
||||
</label>
|
||||
<div class="info-collect-form-row">
|
||||
<div class="form-group">
|
||||
<label for="fofa-size" data-i18n="infoCollectPage.returnCount">返回数量</label>
|
||||
<input type="number" id="fofa-size" min="1" max="10000" value="100" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fofa-page" data-i18n="infoCollectPage.pageNum">页码</label>
|
||||
<input type="number" id="fofa-page" min="1" value="1" />
|
||||
</div>
|
||||
<div class="form-group" id="info-collect-full-option">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="fofa-full" class="modern-checkbox" />
|
||||
<span class="checkbox-custom"></span>
|
||||
<span class="checkbox-text" data-i18n="infoCollectPage.fullLabel">full</span>
|
||||
</label>
|
||||
</div>
|
||||
<small class="form-hint info-collect-row-hint" id="info-collect-size-hint">不同数据源的单次返回上限可能不同。</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fofa-fields" data-i18n="infoCollectPage.returnFields">返回字段名(逗号分隔)</label>
|
||||
<div class="info-collect-fields-card form-group">
|
||||
<div class="info-collect-step-title">
|
||||
<span>04</span>
|
||||
<label for="fofa-fields" data-i18n="infoCollectPage.returnFields">返回字段名(逗号分隔)</label>
|
||||
</div>
|
||||
<input type="text" id="fofa-fields" value="host,ip,port,domain,title,protocol,country,province,city,server" />
|
||||
<div class="info-collect-presets" aria-label="FOFA 字段模板" data-i18n="infoCollectPage.fieldsPresetsAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
|
||||
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain')" data-i18n="infoCollectPage.minFields" data-i18n-attr="title" data-i18n-title="infoCollectPage.minFieldsTitle" title="适合快速导出目标">最小字段</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,title,ip,port,domain,protocol,server,icp,country,province,city')" data-i18n="infoCollectPage.webCommon" data-i18n-attr="title" data-i18n-title="infoCollectPage.webCommonTitle" title="适合浏览和筛选">Web 常用</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain,title,protocol,country,province,city,server,as_number,as_organization,icp,header,banner')" data-i18n="infoCollectPage.intelEnhanced" data-i18n-attr="title" data-i18n-title="infoCollectPage.intelEnhancedTitle" title="更偏指纹/情报">情报增强</button>
|
||||
</div>
|
||||
<div class="info-collect-presets" id="info-collect-fields-presets" aria-label="字段模板"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -3559,7 +3585,6 @@
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
|
||||
</div>
|
||||
@@ -3676,6 +3701,57 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-subsection">
|
||||
<h4>ZoomEye 配置</h4>
|
||||
<p class="form-hint">用于信息收集页调用 ZoomEye API;也可设置环境变量 ZOOMEYE_API_KEY。</p>
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="zoomeye-base-url">Base URL</label>
|
||||
<input type="text" id="zoomeye-base-url" placeholder="https://api.zoomeye.org/v2/search(可选)" />
|
||||
<small class="form-hint">留空则使用默认地址。</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="zoomeye-api-key">API Key</label>
|
||||
<input type="password" id="zoomeye-api-key" placeholder="输入 ZoomEye API Key" autocomplete="off" />
|
||||
<small class="form-hint">仅保存在服务器配置中(config.yaml)。</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-subsection">
|
||||
<h4>Quake 配置</h4>
|
||||
<p class="form-hint">用于信息收集页调用 360 Quake API;也可设置环境变量 QUAKE_API_KEY。</p>
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="quake-base-url">Base URL</label>
|
||||
<input type="text" id="quake-base-url" placeholder="https://quake.360.cn/api/v3/search/quake_service(可选)" />
|
||||
<small class="form-hint">留空则使用默认地址。</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="quake-api-key">API Token</label>
|
||||
<input type="password" id="quake-api-key" placeholder="输入 Quake API Token" autocomplete="off" />
|
||||
<small class="form-hint">仅保存在服务器配置中(config.yaml)。</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-subsection">
|
||||
<h4>Shodan 配置</h4>
|
||||
<p class="form-hint">用于信息收集页调用 Shodan API;也可设置环境变量 SHODAN_API_KEY。</p>
|
||||
<div class="settings-form">
|
||||
<div class="form-group">
|
||||
<label for="shodan-base-url">Base URL</label>
|
||||
<input type="text" id="shodan-base-url" placeholder="https://api.shodan.io(可选)" />
|
||||
<small class="form-hint">留空则使用默认地址。</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="shodan-api-key">API Key</label>
|
||||
<input type="password" id="shodan-api-key" placeholder="输入 Shodan API Key" autocomplete="off" />
|
||||
<small class="form-hint">仅保存在服务器配置中(config.yaml)。</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button type="button" class="btn-primary" onclick="applySettings()" data-require-permission="config:write" data-i18n="settings.apply.button">应用配置</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user