Files
CyberStrikeAI/web/static/js/info-collect.js
T
2026-02-20 18:10:29 +08:00

1062 lines
40 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 信息收集页面(FOFA
const FOFA_FORM_STORAGE_KEY = 'info-collect-fofa-form';
const FOFA_HIDDEN_FIELDS_STORAGE_KEY = 'info-collect-fofa-hidden-fields';
const infoCollectState = {
currentPayload: null, // { fields, results, query, total, page, size }
hiddenFields: new Set(),
selectedRowIndexes: new Set(),
tableBound: false
};
// AI 解析(自然语言 -> FOFA)交互状态
let fofaParseAbortController = null;
let fofaParseSlowTimer = null;
let fofaParseToastHandle = null;
// HTML转义(如果未定义)
if (typeof escapeHtml === 'undefined') {
function escapeHtml(text) {
if (text == null) return '';
const div = document.createElement('div');
div.textContent = String(text);
return div.innerHTML;
}
}
function getFofaFormElements() {
return {
query: document.getElementById('fofa-query'),
nl: document.getElementById('fofa-nl'),
size: document.getElementById('fofa-size'),
page: document.getElementById('fofa-page'),
fields: document.getElementById('fofa-fields'),
full: document.getElementById('fofa-full'),
meta: document.getElementById('fofa-results-meta'),
selectedMeta: document.getElementById('fofa-selected-meta'),
thead: document.getElementById('fofa-results-thead'),
tbody: document.getElementById('fofa-results-tbody'),
columnsPanel: document.getElementById('fofa-columns-panel'),
columnsList: document.getElementById('fofa-columns-list')
};
}
function loadHiddenFieldsFromStorage() {
try {
const raw = localStorage.getItem(FOFA_HIDDEN_FIELDS_STORAGE_KEY);
if (!raw) return [];
const arr = JSON.parse(raw);
if (!Array.isArray(arr)) return [];
return arr.filter(x => typeof x === 'string');
} catch (e) {
return [];
}
}
function saveHiddenFieldsToStorage() {
try {
localStorage.setItem(FOFA_HIDDEN_FIELDS_STORAGE_KEY, JSON.stringify(Array.from(infoCollectState.hiddenFields)));
} catch (e) {
// ignore
}
}
function loadFofaFormFromStorage() {
try {
const raw = localStorage.getItem(FOFA_FORM_STORAGE_KEY);
if (!raw) return null;
const data = JSON.parse(raw);
if (!data || typeof data !== 'object') return null;
return data;
} catch (e) {
return null;
}
}
function saveFofaFormToStorage(payload) {
try {
localStorage.setItem(FOFA_FORM_STORAGE_KEY, JSON.stringify(payload));
} catch (e) {
// ignore
}
}
function initInfoCollectPage() {
const els = getFofaFormElements();
if (!els.query || !els.size || !els.fields || !els.tbody) return;
// 恢复隐藏字段
infoCollectState.hiddenFields = new Set(loadHiddenFieldsFromStorage());
// 恢复上次输入
const saved = loadFofaFormFromStorage();
if (saved) {
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;
}
// 绑定 Enter 快捷查询(在 query 里用 Ctrl/Cmd+Enter
els.query.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
submitFofaSearch();
}
});
// 自然语言输入:Ctrl/Cmd+Enter 触发解析
if (els.nl) {
els.nl.addEventListener('keydown', (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
e.preventDefault();
parseFofaNaturalLanguage();
}
});
}
// textarea:按内容自动增高(避免默认留空白行)
const autoGrowTextarea = (el) => {
if (!el) return;
try {
el.style.height = '36px';
const max = 96;
const h = Math.min(max, el.scrollHeight);
el.style.height = `${h}px`;
} catch (e) {
// ignore
}
};
els.query.addEventListener('input', () => autoGrowTextarea(els.query));
if (els.nl) els.nl.addEventListener('input', () => autoGrowTextarea(els.nl));
// 初始化时也执行一次
setTimeout(() => {
autoGrowTextarea(els.query);
autoGrowTextarea(els.nl);
}, 0);
// 绑定表格事件(事件委托,只绑定一次)
bindFofaTableEvents();
updateSelectedMeta();
}
function applyFofaQueryPreset(preset) {
const els = getFofaFormElements();
if (!els.query) return;
els.query.value = (preset || '').trim();
els.query.focus();
saveFofaFormToStorage({
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
});
}
function applyFofaFieldsPreset(preset) {
const els = getFofaFormElements();
if (!els.fields) return;
els.fields.value = (preset || '').trim();
els.fields.focus();
saveFofaFormToStorage({
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
});
}
function resetFofaForm() {
const els = getFofaFormElements();
if (!els.query) return;
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.full) els.full.checked = false;
saveFofaFormToStorage({
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
});
renderFofaResults({ query: '', fields: [], results: [], total: 0, page: 1, size: 0 });
}
async function submitFofaSearch() {
const els = getFofaFormElements();
const query = (els.query?.value || '').trim();
const size = parseInt(els.size?.value, 10) || 100;
const page = parseInt(els.page?.value, 10) || 1;
const fields = (els.fields?.value || '').trim();
const full = !!els.full?.checked;
if (!query) {
alert('请输入 FOFA 查询语法');
return;
}
saveFofaFormToStorage({ query, size, page, fields, full });
setFofaMeta('查询中...');
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 })
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || `请求失败: ${response.status}`);
}
renderFofaResults(result);
} catch (e) {
console.error('FOFA 查询失败:', e);
setFofaMeta('查询失败');
renderFofaResults({ query, fields: [], results: [], total: 0, page: 1, size: 0 });
alert('FOFA 查询失败: ' + (e && e.message ? e.message : String(e)));
} finally {
setFofaLoading(false);
}
}
async function parseFofaNaturalLanguage() {
const els = getFofaFormElements();
const text = (els.nl?.value || '').trim();
if (!text) {
alert('请输入自然语言描述');
return;
}
// 二次点击:取消进行中的解析(避免“以为卡死/失败”)
if (fofaParseAbortController) {
try { fofaParseAbortController.abort(); } catch (e) { /* ignore */ }
return;
}
// 先创建 controller,避免极快的重复点击触发并发请求
fofaParseAbortController = new AbortController();
setFofaParseLoading(true, 'AI 解析中...');
// 持续提示:直到请求完成/取消/失败才消失
fofaParseToastHandle = showInlineToast('AI 解析中...(点击按钮可取消)', { duration: 0, id: 'fofa-parse-pending' });
// 如果超过一小段时间还没返回,再强调“仍在进行中”,降低误判为失败的概率
fofaParseSlowTimer = setTimeout(() => {
const status = document.getElementById('fofa-nl-status');
if (status) {
status.textContent = 'AI 解析耗时较长,仍在处理中…';
status.style.display = 'block';
}
}, 1800);
try {
const resp = await apiFetch('/api/fofa/parse', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text }),
signal: fofaParseAbortController.signal
});
const result = await resp.json().catch(() => ({}));
if (!resp.ok) {
throw new Error(result.error || `请求失败: ${resp.status}`);
}
showFofaParseModal(text, result);
showInlineToast('AI 解析完成');
} catch (e) {
// AbortController 取消:不视为失败
if (e && (e.name === 'AbortError' || String(e).includes('AbortError'))) {
showInlineToast('已取消 AI 解析');
return;
}
console.error('FOFA 自然语言解析失败:', e);
showInlineToast('AI 解析失败:' + (e && e.message ? e.message : String(e)), { duration: 2800 });
}
finally {
fofaParseAbortController = null;
if (fofaParseSlowTimer) {
clearTimeout(fofaParseSlowTimer);
fofaParseSlowTimer = null;
}
if (fofaParseToastHandle && typeof fofaParseToastHandle.remove === 'function') {
fofaParseToastHandle.remove();
}
fofaParseToastHandle = null;
setFofaParseLoading(false, '');
}
}
function setFofaParseLoading(loading, statusText) {
const btn = document.getElementById('fofa-nl-parse-btn');
const status = document.getElementById('fofa-nl-status');
if (btn) {
if (loading) {
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent || 'AI 解析';
btn.classList.add('btn-loading');
btn.textContent = '取消解析';
btn.title = '点击取消 AI 解析';
btn.dataset.loading = '1';
btn.setAttribute('aria-busy', 'true');
btn.disabled = false;
} else {
btn.classList.remove('btn-loading');
btn.textContent = btn.dataset.originalText || 'AI 解析';
btn.title = '将自然语言解析为 FOFA 查询语法';
btn.disabled = false;
delete btn.dataset.loading;
btn.removeAttribute('aria-busy');
}
}
if (status) {
const text = (statusText || '').trim();
if (loading && text) {
status.textContent = text;
status.style.display = 'block';
} else {
status.textContent = '';
status.style.display = 'none';
}
}
}
function showFofaParseModal(nlText, parsed) {
const existing = document.getElementById('fofa-parse-modal');
if (existing) existing.remove();
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) : '';
const warningsHtml = warnings.length
? `<ul style="margin: 8px 0 0 18px;">${warnings.map(w => `<li>${escapeHtml(w)}</li>`).join('')}</ul>`
: `<div class="muted" style="margin-top: 8px;">无</div>`;
const modal = document.createElement('div');
modal.id = 'fofa-parse-modal';
modal.className = 'modal';
modal.style.display = 'block';
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2>AI 解析结果</h2>
<span class="modal-close" id="fofa-parse-modal-close" title="关闭">&times;</span>
</div>
<div style="padding: 18px 28px; overflow: auto;">
<div class="form-group">
<label>自然语言</label>
<div class="muted" style="margin-top: 6px; white-space: pre-wrap;">${safeNL || '-'}</div>
</div>
<div class="form-group" style="margin-top: 14px;">
<label for="fofa-parse-query">FOFA 查询语法(可编辑)</label>
<textarea id="fofa-parse-query" class="info-collect-query-input" rows="2" placeholder='例如:app="Apache" && country="CN"'></textarea>
<small class="form-hint">请人工确认语法与范围无误后再执行查询。</small>
</div>
<div class="form-group" style="margin-top: 14px;">
<label>提醒</label>
<div style="background: #fff8e1; border: 1px solid #ffe8a3; border-radius: 10px; padding: 10px 12px;">
${warningsHtml}
</div>
</div>
${explanation ? `
<div class="form-group" style="margin-top: 14px;">
<label>解析说明</label>
<pre style="margin-top: 8px; white-space: pre-wrap; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 10px; padding: 10px 12px; font-size: 13px;">${escapeHtml(explanation)}</pre>
</div>` : ''}
</div>
<div class="modal-footer" style="padding: 18px 28px;">
<button class="btn-secondary" type="button" id="fofa-parse-cancel">取消</button>
<button class="btn-secondary" type="button" id="fofa-parse-apply">填入查询框</button>
<button class="btn-primary" type="button" id="fofa-parse-apply-run">填入并查询</button>
</div>
</div>
`;
document.body.appendChild(modal);
const queryTextarea = document.getElementById('fofa-parse-query');
if (queryTextarea) {
queryTextarea.value = (parsed?.query || '').trim();
setTimeout(() => {
try { queryTextarea.focus(); } catch (e) { /* ignore */ }
}, 0);
}
const close = () => modal.remove();
modal.addEventListener('click', (e) => {
if (e.target === modal) close();
});
document.getElementById('fofa-parse-modal-close')?.addEventListener('click', close);
document.getElementById('fofa-parse-cancel')?.addEventListener('click', close);
const applyToQuery = (run) => {
const els = getFofaFormElements();
const q = (queryTextarea?.value || '').trim();
if (!q) {
showInlineToast('解析结果为空:请在弹窗中补充/修改 FOFA 查询语法', { duration: 2600 });
return;
}
if (els.query) {
els.query.value = q;
try { els.query.focus(); } catch (e) { /* ignore */ }
}
// 写入表单缓存(与现有“直接查询”一致)
saveFofaFormToStorage({
query: q,
size: parseInt(els.size?.value, 10) || 100,
page: parseInt(els.page?.value, 10) || 1,
fields: (els.fields?.value || '').trim(),
full: !!els.full?.checked
});
close();
if (run) submitFofaSearch();
};
document.getElementById('fofa-parse-apply')?.addEventListener('click', () => applyToQuery(false));
document.getElementById('fofa-parse-apply-run')?.addEventListener('click', () => applyToQuery(true));
// Esc 关闭
const onKey = (e) => {
if (e.key === 'Escape') {
close();
document.removeEventListener('keydown', onKey);
}
};
document.addEventListener('keydown', onKey);
}
function setFofaMeta(text) {
const els = getFofaFormElements();
if (els.meta) {
els.meta.textContent = text || '-';
}
}
function updateSelectedMeta() {
const els = getFofaFormElements();
if (els.selectedMeta) {
els.selectedMeta.textContent = `已选择 ${infoCollectState.selectedRowIndexes.size}`;
}
}
function setFofaLoading(loading) {
const els = getFofaFormElements();
if (!els.tbody) return;
if (loading) {
const fieldsCount = (document.getElementById('fofa-fields')?.value || '').split(',').filter(Boolean).length;
const colspan = Math.max(1, fieldsCount + 1);
els.tbody.innerHTML = `<tr><td class="muted" style="padding: 16px;" colspan="${colspan}">加载中...</td></tr>`;
}
}
function renderFofaResults(payload) {
const els = getFofaFormElements();
if (!els.thead || !els.tbody) return;
const fields = Array.isArray(payload.fields) ? payload.fields : [];
const results = Array.isArray(payload.results) ? payload.results : [];
// 保存当前 payload 到 state
infoCollectState.currentPayload = {
query: payload.query || '',
total: typeof payload.total === 'number' ? payload.total : 0,
page: typeof payload.page === 'number' ? payload.page : 1,
size: typeof payload.size === 'number' ? payload.size : 0,
fields,
results
};
// 清理选择(避免字段/结果变化导致错位)
infoCollectState.selectedRowIndexes.clear();
updateSelectedMeta();
// 修剪隐藏字段:只保留当前 fields 中存在的
const allowed = new Set(fields);
infoCollectState.hiddenFields.forEach(f => {
if (!allowed.has(f)) infoCollectState.hiddenFields.delete(f);
});
saveHiddenFieldsToStorage();
const total = typeof payload.total === 'number' ? payload.total : 0;
const size = typeof payload.size === 'number' ? payload.size : 0;
const page = typeof payload.page === 'number' ? payload.page : 1;
setFofaMeta(`${total} 条 · 本页 ${results.length} 条 · page=${page} · size=${size}`);
// 可见字段
const visibleFields = fields.filter(f => !infoCollectState.hiddenFields.has(f));
// 列面板
renderFofaColumnsPanel(fields, visibleFields);
// 表头(左:勾选列;右:操作列固定)
const headerCells = [
'<th class="info-collect-col-select"><input type="checkbox" id="fofa-select-all" title="全选/全不选"/></th>',
...visibleFields.map(f => `<th>${escapeHtml(String(f))}</th>`),
'<th class="info-collect-col-actions">操作</th>'
].join('');
els.thead.innerHTML = `<tr>${headerCells}</tr>`;
// 表体
if (results.length === 0) {
const colspan = Math.max(1, visibleFields.length + 2);
els.tbody.innerHTML = `<tr><td class="muted" style="padding: 16px;" colspan="${colspan}">暂无数据</td></tr>`;
return;
}
const rowsHtml = results.map((row, idx) => {
const safeRow = row && typeof row === 'object' ? row : {};
const target = inferTargetFromRow(safeRow, fields);
const encoded = encodeURIComponent(JSON.stringify(safeRow));
const encodedTarget = encodeURIComponent(target || '');
const selectHtml = `<td class="info-collect-col-select"><input class="fofa-row-select" type="checkbox" data-index="${idx}" title="选择该行"/></td>`;
const cellsHtml = visibleFields.map(f => {
const val = safeRow[f];
const text = val == null ? '' : String(val);
// host 字段:尽量渲染为可点击链接
if (f === 'host') {
const href = normalizeHttpLink(text);
if (href) {
const safeHref = escapeHtml(href);
return `<td class="info-collect-cell" data-field="${escapeHtml(f)}" data-full="${escapeHtml(text)}" title="${escapeHtml(text)}"><a class="info-collect-link" href="${safeHref}" target="_blank" rel="noopener noreferrer" onclick="event.stopPropagation();">${escapeHtml(text)}</a></td>`;
}
}
return `<td class="info-collect-cell" data-field="${escapeHtml(f)}" data-full="${escapeHtml(text)}" title="${escapeHtml(text)}"><span class="info-collect-cell-text">${escapeHtml(text)}</span></td>`;
}).join('');
const actionHtml = `
<div class="info-collect-actions">
<button class="btn-icon" onclick="copyFofaTargetEncoded('${encodedTarget}'); event.stopPropagation();" title="复制目标">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" stroke-width="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
<button class="btn-icon" onclick="scanFofaRow('${encoded}', event); event.stopPropagation();" title="发送到对话(可编辑;Ctrl/⌘+点击可直接发送)">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 13.5l3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M8 8H5a4 4 0 1 0 0 8h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M16 8h3a4 4 0 0 1 0 8h-3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</button>
</div>
`;
return `<tr data-index="${idx}">${selectHtml}${cellsHtml}<td class="info-collect-col-actions">${actionHtml}</td></tr>`;
}).join('');
els.tbody.innerHTML = rowsHtml;
// 更新全选框状态
syncSelectAllCheckbox();
}
function inferTargetFromRow(row, fields) {
// 优先 hostFOFA 常见返回 http(s)://...
const host = row.host != null ? String(row.host).trim() : '';
if (host) return host;
const domain = row.domain != null ? String(row.domain).trim() : '';
const ip = row.ip != null ? String(row.ip).trim() : '';
const port = row.port != null ? String(row.port).trim() : '';
const protocol = row.protocol != null ? String(row.protocol).trim().toLowerCase() : '';
const base = domain || ip;
if (!base) return '';
if (port) {
// 仅做一个轻量推断:443 -> https, 80 -> http,其余不强行加 scheme
const p = parseInt(port, 10);
if (!isNaN(p) && (p === 80 || p === 443)) {
const scheme = p === 443 ? 'https' : 'http';
return `${scheme}://${base}:${p}`;
}
if (protocol === 'https' || protocol === 'http') {
return `${protocol}://${base}:${port}`;
}
return `${base}:${port}`;
}
return base;
}
function normalizeHttpLink(raw) {
const v = (raw || '').trim();
if (!v) return '';
if (v.startsWith('http://') || v.startsWith('https://')) return v;
// 某些 host 可能是 domain 或 ip:port;这里不强行拼装,避免误导
return '';
}
function copyFofaTarget(target) {
const text = (target || '').trim();
if (!text) {
alert('没有可复制的目标');
return;
}
navigator.clipboard.writeText(text).then(() => {
// 简单提示
showInlineToast('已复制目标');
}).catch(() => {
alert('复制失败,请手动复制:' + text);
});
}
function copyFofaTargetEncoded(encodedTarget) {
try {
copyFofaTarget(decodeURIComponent(encodedTarget || ''));
} catch (e) {
copyFofaTarget(encodedTarget || '');
}
}
// showInlineToast('xxx');也支持 showInlineToast('xxx', { duration: 0, id: '...' })
function showInlineToast(text, options) {
const opts = options && typeof options === 'object' ? options : {};
const duration = typeof opts.duration === 'number' ? opts.duration : 1200;
const id = typeof opts.id === 'string' && opts.id.trim() ? opts.id.trim() : '';
const replace = opts.replace !== false;
if (id && replace) {
document.getElementById(id)?.remove();
}
const toast = document.createElement('div');
if (id) toast.id = id;
toast.textContent = String(text == null ? '' : text);
toast.style.cssText = 'position: fixed; top: 24px; right: 24px; background: rgba(0,0,0,0.85); color: #fff; padding: 10px 12px; border-radius: 8px; z-index: 10000; font-size: 13px; max-width: 420px; line-height: 1.4; box-shadow: 0 6px 18px rgba(0,0,0,0.22);';
document.body.appendChild(toast);
let timer = null;
const remove = () => {
try { if (timer) clearTimeout(timer); } catch (e) { /* ignore */ }
timer = null;
try { toast.remove(); } catch (e) { /* ignore */ }
};
if (duration > 0) {
timer = setTimeout(remove, duration);
}
return { el: toast, remove };
}
function scanFofaRow(encodedRowJson, clickEvent) {
let row = {};
try {
row = JSON.parse(decodeURIComponent(encodedRowJson));
} catch (e) {
console.warn('解析行数据失败', e);
}
const fields = (document.getElementById('fofa-fields')?.value || '').split(',').map(s => s.trim()).filter(Boolean);
const target = inferTargetFromRow(row, fields);
if (!target) {
alert('无法从该行推断扫描目标(建议在 fields 中包含 host/ip/port/domain');
return;
}
// 切换到对话页并发送消息(每次点击都新建会话,避免发到历史会话)
if (typeof switchPage === 'function') {
switchPage('chat');
} else {
window.location.hash = 'chat';
}
const message = buildScanMessage(target, row);
const autoSend = !!(clickEvent && (clickEvent.ctrlKey || clickEvent.metaKey));
setTimeout(async () => {
// 新建会话:必须等待其完成,否则它会在后续把输入框清空
try {
if (typeof startNewConversation === 'function') {
const maybePromise = startNewConversation();
if (maybePromise && typeof maybePromise.then === 'function') {
await maybePromise;
}
}
} catch (e) {
// ignore
}
const input = document.getElementById('chat-input');
if (input) {
input.value = message;
// 触发自动高度调整(chat.js 里如果监听 input
input.dispatchEvent(new Event('input', { bubbles: true }));
input.focus();
}
if (autoSend) {
if (typeof sendMessage === 'function') {
sendMessage();
} else {
alert('未找到 sendMessage(),请刷新页面后重试');
}
} else {
showInlineToast('已填入对话输入框,可编辑后发送');
}
}, 250);
}
function buildScanMessage(target, row) {
const title = row && row.title != null ? String(row.title).trim() : '';
const server = row && row.server != null ? String(row.server).trim() : '';
const hintParts = [];
if (title) hintParts.push(`title="${title}"`);
if (server) hintParts.push(`server="${server}"`);
const hint = hintParts.length ? `FOFA提示:${hintParts.join(', ')}` : '';
return `对以下目标做信息收集与基础扫描:\n${target}\n\n要求:\n1) 识别服务/框架与关键指纹\n2) 枚举开放端口与常见管理入口\n3) 用 httpx/指纹/目录探测等方式快速确认可访问面\n4) 输出可复现的命令与结论\n\n${hint}`.trim();
}
function bindFofaTableEvents() {
if (infoCollectState.tableBound) return;
infoCollectState.tableBound = true;
const els = getFofaFormElements();
if (!els.tbody) return;
// 事件委托:选择/单元格展开
els.tbody.addEventListener('click', (e) => {
const checkbox = e.target && e.target.classList && e.target.classList.contains('fofa-row-select') ? e.target : null;
if (checkbox) {
const idx = parseInt(checkbox.getAttribute('data-index'), 10);
if (!isNaN(idx)) {
if (checkbox.checked) infoCollectState.selectedRowIndexes.add(idx);
else infoCollectState.selectedRowIndexes.delete(idx);
updateSelectedMeta();
syncSelectAllCheckbox();
}
return;
}
const cell = e.target && e.target.closest ? e.target.closest('.info-collect-cell') : null;
if (cell) {
const full = cell.getAttribute('data-full') || '';
const field = cell.getAttribute('data-field') || '';
// 点击链接不弹窗
if (e.target && e.target.tagName === 'A') return;
if (full && full.length > 0) {
showCellDetailModal(field, full);
}
}
});
// thead 的全选(因为 thead 会重渲染,用事件捕获到 document)
document.addEventListener('change', (e) => {
const t = e.target;
if (!t || t.id !== 'fofa-select-all') return;
const checked = !!t.checked;
toggleSelectAllRows(checked);
});
}
function toggleSelectAllRows(checked) {
const els = getFofaFormElements();
if (!els.tbody) return;
const boxes = els.tbody.querySelectorAll('input.fofa-row-select');
infoCollectState.selectedRowIndexes.clear();
boxes.forEach(b => {
b.checked = checked;
const idx = parseInt(b.getAttribute('data-index'), 10);
if (checked && !isNaN(idx)) infoCollectState.selectedRowIndexes.add(idx);
});
updateSelectedMeta();
syncSelectAllCheckbox();
}
function syncSelectAllCheckbox() {
const selectAll = document.getElementById('fofa-select-all');
const els = getFofaFormElements();
if (!selectAll || !els.tbody) return;
const boxes = els.tbody.querySelectorAll('input.fofa-row-select');
const total = boxes.length;
const selected = infoCollectState.selectedRowIndexes.size;
if (total === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
return;
}
if (selected === 0) {
selectAll.checked = false;
selectAll.indeterminate = false;
} else if (selected === total) {
selectAll.checked = true;
selectAll.indeterminate = false;
} else {
selectAll.checked = false;
selectAll.indeterminate = true;
}
}
function renderFofaColumnsPanel(allFields, visibleFields) {
const els = getFofaFormElements();
if (!els.columnsList) return;
const currentVisible = new Set(visibleFields);
els.columnsList.innerHTML = allFields.map(f => {
const checked = currentVisible.has(f);
const safe = escapeHtml(f);
return `
<label class="info-collect-col-item" title="${safe}">
<input type="checkbox" ${checked ? 'checked' : ''} onchange="toggleFofaColumn('${safe}', this.checked)" />
<span>${safe}</span>
</label>
`;
}).join('');
}
function toggleFofaColumn(field, visible) {
const f = String(field || '').trim();
if (!f) return;
if (visible) infoCollectState.hiddenFields.delete(f);
else infoCollectState.hiddenFields.add(f);
saveHiddenFieldsToStorage();
// 重新渲染表格(用 state 中缓存的 payload
if (infoCollectState.currentPayload) {
renderFofaResults(infoCollectState.currentPayload);
}
}
function toggleFofaColumnsPanel() {
const els = getFofaFormElements();
if (!els.columnsPanel) return;
const show = els.columnsPanel.style.display === 'none' || !els.columnsPanel.style.display;
els.columnsPanel.style.display = show ? 'block' : 'none';
}
function closeFofaColumnsPanel() {
const els = getFofaFormElements();
if (els.columnsPanel) els.columnsPanel.style.display = 'none';
}
// 点击面板外部关闭(避免一直占着表格顶部)
document.addEventListener('click', (e) => {
const panel = document.getElementById('fofa-columns-panel');
const btn = e.target && e.target.closest ? e.target.closest('button') : null;
const isColumnsBtn = btn && btn.getAttribute && btn.getAttribute('onclick') && String(btn.getAttribute('onclick')).includes('toggleFofaColumnsPanel');
if (!panel || panel.style.display === 'none') return;
if (panel.contains(e.target) || isColumnsBtn) return;
panel.style.display = 'none';
});
function showAllFofaColumns() {
infoCollectState.hiddenFields.clear();
saveHiddenFieldsToStorage();
if (infoCollectState.currentPayload) renderFofaResults(infoCollectState.currentPayload);
}
function hideAllFofaColumns() {
const p = infoCollectState.currentPayload;
if (!p || !Array.isArray(p.fields)) return;
// 允许隐藏全部,但给用户一个最小可用:至少保留 host/ip/domain 中之一(如果存在)
const keep = ['host', 'ip', 'domain'].find(x => p.fields.includes(x));
infoCollectState.hiddenFields = new Set(p.fields.filter(f => f !== keep));
saveHiddenFieldsToStorage();
renderFofaResults(p);
}
function exportFofaResults(format) {
const p = infoCollectState.currentPayload;
if (!p || !Array.isArray(p.results) || p.results.length === 0) {
alert('暂无可导出的结果');
return;
}
const fields = p.fields || [];
const visibleFields = fields.filter(f => !infoCollectState.hiddenFields.has(f));
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 = {
query: p.query || '',
total: p.total || 0,
page: p.page || 1,
size: p.size || 0,
fields: fields,
results: p.results
};
downloadBlob(JSON.stringify(payload, null, 2), `fofa_results_${ts}.json`, 'application/json;charset=utf-8');
return;
}
// csv:默认导出可见字段(更符合“列隐藏”直觉)
const header = visibleFields;
const rows = p.results.map(row => {
const r = row && typeof row === 'object' ? row : {};
return header.map(f => csvEscape(r[f]));
});
const csv = [header.map(csvEscape).join(','), ...rows.map(cols => cols.join(','))].join('\n');
downloadBlob(csv, `fofa_results_${ts}.csv`, 'text/csv;charset=utf-8');
}
function csvEscape(value) {
if (value == null) return '""';
const s = String(value).replace(/"/g, '""');
return `"${s}"`;
}
function downloadBlob(content, filename, mime) {
const blob = new Blob([content], { type: mime });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
async function batchScanSelectedFofaRows() {
const p = infoCollectState.currentPayload;
if (!p || !Array.isArray(p.results) || p.results.length === 0) {
alert('暂无结果');
return;
}
const selected = Array.from(infoCollectState.selectedRowIndexes).sort((a, b) => a - b);
if (selected.length === 0) {
alert('请先勾选需要扫描的行');
return;
}
const fields = p.fields || [];
const tasks = [];
const skipped = [];
selected.forEach(idx => {
const row = p.results[idx];
const target = inferTargetFromRow(row || {}, fields);
if (!target) {
skipped.push(idx + 1);
return;
}
tasks.push(buildScanMessage(target, row || {}));
});
if (tasks.length === 0) {
alert('未能从所选行推断任何可扫描目标(建议 fields 中包含 host/ip/port/domain');
return;
}
const title = (p.query ? `FOFA 批量扫描:${p.query}` : 'FOFA 批量扫描').slice(0, 80);
try {
// 不强制切换到“信息收集”角色:沿用当前已选角色;若为默认则传空字符串交给后端走默认逻辑
let role = '';
if (typeof getCurrentRole === 'function') {
try { role = getCurrentRole() || ''; } catch (e) { /* ignore */ }
}
if (role === '默认') role = '';
const resp = await apiFetch('/api/batch-tasks', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, tasks, role })
});
const result = await resp.json().catch(() => ({}));
if (!resp.ok) {
throw new Error(result.error || `创建批量队列失败: ${resp.status}`);
}
const queueId = result.queueId;
if (!queueId) {
throw new Error('创建成功但未返回 queueId');
}
// 跳到任务管理并打开队列详情
if (typeof switchPage === 'function') switchPage('tasks');
setTimeout(() => {
if (typeof showBatchQueueDetail === 'function') {
showBatchQueueDetail(queueId);
}
}, 250);
if (skipped.length > 0) {
showInlineToast(`已创建队列(跳过 ${skipped.length} 条无目标行)`);
} else {
showInlineToast('已创建批量扫描队列');
}
} catch (e) {
console.error('批量扫描失败:', e);
alert('批量扫描失败: ' + (e && e.message ? e.message : String(e)));
}
}
function showCellDetailModal(field, fullText) {
const existing = document.getElementById('info-collect-cell-modal');
if (existing) existing.remove();
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 || '字段')}</div>
<button class="btn-icon" type="button" id="info-collect-cell-modal-close" title="关闭">
<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"/>
</svg>
</button>
</div>
<div class="info-collect-cell-modal-body">
<pre class="info-collect-cell-modal-pre">${escapeHtml(fullText || '')}</pre>
</div>
<div class="info-collect-cell-modal-footer">
<button class="btn-secondary" type="button" id="info-collect-cell-modal-copy">复制</button>
<button class="btn-primary" type="button" id="info-collect-cell-modal-ok">关闭</button>
</div>
</div>
`;
document.body.appendChild(modal);
const close = () => modal.remove();
modal.addEventListener('click', (e) => {
if (e.target === modal) close();
});
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('已复制')).catch(() => alert('复制失败'));
});
// Esc 关闭
const onKey = (e) => {
if (e.key === 'Escape') {
close();
document.removeEventListener('keydown', onKey);
}
};
document.addEventListener('keydown', onKey);
}
// 暴露到全局(供 index.html onclick 调用)
window.initInfoCollectPage = initInfoCollectPage;
window.resetFofaForm = resetFofaForm;
window.submitFofaSearch = submitFofaSearch;
window.parseFofaNaturalLanguage = parseFofaNaturalLanguage;
window.scanFofaRow = scanFofaRow;
window.copyFofaTarget = copyFofaTarget;
window.copyFofaTargetEncoded = copyFofaTargetEncoded;
window.applyFofaQueryPreset = applyFofaQueryPreset;
window.applyFofaFieldsPreset = applyFofaFieldsPreset;
window.toggleFofaColumnsPanel = toggleFofaColumnsPanel;
window.closeFofaColumnsPanel = closeFofaColumnsPanel;
window.showAllFofaColumns = showAllFofaColumns;
window.hideAllFofaColumns = hideAllFofaColumns;
window.toggleFofaColumn = toggleFofaColumn;
window.exportFofaResults = exportFofaResults;
window.batchScanSelectedFofaRows = batchScanSelectedFofaRows;