diff --git a/internal/app/app.go b/internal/app/app.go
index 02aeaa60..49f78ec0 100644
--- a/internal/app/app.go
+++ b/internal/app/app.go
@@ -318,6 +318,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
roleHandler := handler.NewRoleHandler(cfg, configPath, log.Logger)
roleHandler.SetSkillsManager(skillsManager) // 设置Skills管理器到RoleHandler
skillsHandler := handler.NewSkillsHandler(skillsManager, cfg, configPath, log.Logger)
+ fofaHandler := handler.NewFofaHandler(cfg, log.Logger)
if db != nil {
skillsHandler.SetDB(db) // 设置数据库连接以便获取调用统计
}
@@ -415,6 +416,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
vulnerabilityHandler,
roleHandler,
skillsHandler,
+ fofaHandler,
mcpServer,
authManager,
openAPIHandler,
@@ -478,6 +480,7 @@ func setupRoutes(
vulnerabilityHandler *handler.VulnerabilityHandler,
roleHandler *handler.RoleHandler,
skillsHandler *handler.SkillsHandler,
+ fofaHandler *handler.FofaHandler,
mcpServer *mcp.Server,
authManager *security.AuthManager,
openAPIHandler *handler.OpenAPIHandler,
@@ -506,6 +509,9 @@ func setupRoutes(
protected.GET("/agent-loop/tasks", agentHandler.ListAgentTasks)
protected.GET("/agent-loop/tasks/completed", agentHandler.ListCompletedTasks)
+ // 信息收集 - FOFA 查询(后端代理)
+ protected.POST("/fofa/search", fofaHandler.Search)
+
// 批量任务管理
protected.POST("/batch-tasks", agentHandler.CreateBatchQueue)
protected.GET("/batch-tasks", agentHandler.ListBatchQueues)
diff --git a/internal/config/config.go b/internal/config/config.go
index 4ecbcd9a..ca65f926 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -18,6 +18,7 @@ type Config struct {
Log LogConfig `yaml:"log"`
MCP MCPConfig `yaml:"mcp"`
OpenAI OpenAIConfig `yaml:"openai"`
+ FOFA FofaConfig `yaml:"fofa,omitempty" json:"fofa,omitempty"`
Agent AgentConfig `yaml:"agent"`
Security SecurityConfig `yaml:"security"`
Database DatabaseConfig `yaml:"database"`
@@ -52,6 +53,13 @@ type OpenAIConfig struct {
MaxTotalTokens int `yaml:"max_total_tokens,omitempty" json:"max_total_tokens,omitempty"`
}
+type FofaConfig struct {
+ // Email 为 FOFA 账号邮箱;APIKey 为 FOFA API Key(建议使用只读权限的 Key)
+ Email string `yaml:"email,omitempty" json:"email,omitempty"`
+ APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"`
+ BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"` // 默认 https://fofa.info/api/v1/search/all
+}
+
type SecurityConfig struct {
Tools []ToolConfig `yaml:"tools,omitempty"` // 向后兼容:支持在主配置文件中定义工具
ToolsDir string `yaml:"tools_dir,omitempty"` // 工具配置文件目录(新方式)
diff --git a/internal/handler/config.go b/internal/handler/config.go
index a8aa4112..efdd2543 100644
--- a/internal/handler/config.go
+++ b/internal/handler/config.go
@@ -145,6 +145,7 @@ func (h *ConfigHandler) SetAppUpdater(updater AppUpdater) {
// GetConfigResponse 获取配置响应
type GetConfigResponse struct {
OpenAI config.OpenAIConfig `json:"openai"`
+ FOFA config.FofaConfig `json:"fofa"`
MCP config.MCPConfig `json:"mcp"`
Tools []ToolConfigInfo `json:"tools"`
Agent config.AgentConfig `json:"agent"`
@@ -216,6 +217,7 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) {
c.JSON(http.StatusOK, GetConfigResponse{
OpenAI: h.config.OpenAI,
+ FOFA: h.config.FOFA,
MCP: h.config.MCP,
Tools: tools,
Agent: h.config.Agent,
@@ -472,6 +474,7 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
// UpdateConfigRequest 更新配置请求
type UpdateConfigRequest struct {
OpenAI *config.OpenAIConfig `json:"openai,omitempty"`
+ FOFA *config.FofaConfig `json:"fofa,omitempty"`
MCP *config.MCPConfig `json:"mcp,omitempty"`
Tools []ToolEnableStatus `json:"tools,omitempty"`
Agent *config.AgentConfig `json:"agent,omitempty"`
@@ -506,6 +509,12 @@ func (h *ConfigHandler) UpdateConfig(c *gin.Context) {
)
}
+ // 更新FOFA配置
+ if req.FOFA != nil {
+ h.config.FOFA = *req.FOFA
+ h.logger.Info("更新FOFA配置", zap.String("email", h.config.FOFA.Email))
+ }
+
// 更新MCP配置
if req.MCP != nil {
h.config.MCP = *req.MCP
diff --git a/internal/handler/fofa.go b/internal/handler/fofa.go
new file mode 100644
index 00000000..2c07b304
--- /dev/null
+++ b/internal/handler/fofa.go
@@ -0,0 +1,223 @@
+package handler
+
+import (
+ "encoding/base64"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "cyberstrike-ai/internal/config"
+
+ "github.com/gin-gonic/gin"
+ "go.uber.org/zap"
+)
+
+type FofaHandler struct {
+ cfg *config.Config
+ logger *zap.Logger
+ client *http.Client
+}
+
+func NewFofaHandler(cfg *config.Config, logger *zap.Logger) *FofaHandler {
+ return &FofaHandler{
+ cfg: cfg,
+ logger: logger,
+ client: &http.Client{Timeout: 30 * time.Second},
+ }
+}
+
+type fofaSearchRequest struct {
+ Query string `json:"query" binding:"required"`
+ Size int `json:"size,omitempty"`
+ Page int `json:"page,omitempty"`
+ Fields string `json:"fields,omitempty"`
+ Full bool `json:"full,omitempty"`
+}
+
+type fofaAPIResponse struct {
+ Error bool `json:"error"`
+ ErrMsg string `json:"errmsg"`
+ Size int `json:"size"`
+ Page int `json:"page"`
+ Total int `json:"total"`
+ Mode string `json:"mode"`
+ Query string `json:"query"`
+ Results [][]interface{} `json:"results"`
+}
+
+type fofaSearchResponse struct {
+ Query string `json:"query"`
+ Size int `json:"size"`
+ Page int `json:"page"`
+ Total int `json:"total"`
+ Fields []string `json:"fields"`
+ ResultsCount int `json:"results_count"`
+ Results []map[string]interface{} `json:"results"`
+}
+
+func (h *FofaHandler) resolveCredentials() (email, apiKey string) {
+ // 优先环境变量(便于容器部署),其次配置文件
+ email = strings.TrimSpace(os.Getenv("FOFA_EMAIL"))
+ apiKey = strings.TrimSpace(os.Getenv("FOFA_API_KEY"))
+ if email != "" && apiKey != "" {
+ return email, apiKey
+ }
+ if h.cfg != nil {
+ if email == "" {
+ email = strings.TrimSpace(h.cfg.FOFA.Email)
+ }
+ if apiKey == "" {
+ apiKey = strings.TrimSpace(h.cfg.FOFA.APIKey)
+ }
+ }
+ return email, apiKey
+}
+
+func (h *FofaHandler) resolveBaseURL() string {
+ if h.cfg != nil {
+ if v := strings.TrimSpace(h.cfg.FOFA.BaseURL); v != "" {
+ return v
+ }
+ }
+ return "https://fofa.info/api/v1/search/all"
+}
+
+// Search FOFA 查询(后端代理,避免前端暴露 key)
+func (h *FofaHandler) Search(c *gin.Context) {
+ var req fofaSearchRequest
+ if err := c.ShouldBindJSON(&req); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
+ return
+ }
+
+ req.Query = strings.TrimSpace(req.Query)
+ if req.Query == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "query 不能为空"})
+ return
+ }
+ if req.Size <= 0 {
+ req.Size = 100
+ }
+ if req.Page <= 0 {
+ req.Page = 1
+ }
+ // FOFA 接口 size 上限和账户权限相关,这里只做一个合理的保护
+ if req.Size > 10000 {
+ req.Size = 10000
+ }
+ if req.Fields == "" {
+ req.Fields = "host,ip,port,domain,title,protocol,country,province,city,server"
+ }
+
+ email, apiKey := h.resolveCredentials()
+ if email == "" || apiKey == "" {
+ c.JSON(http.StatusBadRequest, gin.H{
+ "error": "FOFA 未配置:请在系统设置中填写 FOFA Email/API Key,或设置环境变量 FOFA_EMAIL/FOFA_API_KEY",
+ "need": []string{"fofa.email", "fofa.api_key"},
+ "env_key": []string{"FOFA_EMAIL", "FOFA_API_KEY"},
+ })
+ return
+ }
+
+ baseURL := h.resolveBaseURL()
+ qb64 := base64.StdEncoding.EncodeToString([]byte(req.Query))
+
+ u, err := url.Parse(baseURL)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "FOFA base_url 无效: " + err.Error()})
+ return
+ }
+
+ params := u.Query()
+ params.Set("email", email)
+ params.Set("key", apiKey)
+ params.Set("qbase64", qb64)
+ params.Set("size", fmt.Sprintf("%d", req.Size))
+ params.Set("page", fmt.Sprintf("%d", req.Page))
+ params.Set("fields", strings.TrimSpace(req.Fields))
+ if req.Full {
+ params.Set("full", "true")
+ } else {
+ // 明确传 false,便于排查
+ params.Set("full", "false")
+ }
+ u.RawQuery = params.Encode()
+
+ httpReq, err := http.NewRequestWithContext(c.Request.Context(), http.MethodGet, u.String(), nil)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "创建请求失败: " + err.Error()})
+ return
+ }
+
+ resp, err := h.client.Do(httpReq)
+ if err != nil {
+ c.JSON(http.StatusBadGateway, gin.H{"error": "请求 FOFA 失败: " + err.Error()})
+ return
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("FOFA 返回非 2xx: %d", resp.StatusCode)})
+ return
+ }
+
+ var apiResp fofaAPIResponse
+ if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
+ c.JSON(http.StatusBadGateway, gin.H{"error": "解析 FOFA 响应失败: " + err.Error()})
+ return
+ }
+ if apiResp.Error {
+ msg := strings.TrimSpace(apiResp.ErrMsg)
+ if msg == "" {
+ msg = "FOFA 返回错误"
+ }
+ c.JSON(http.StatusBadGateway, gin.H{"error": msg})
+ return
+ }
+
+ fields := splitAndCleanCSV(req.Fields)
+ results := make([]map[string]interface{}, 0, len(apiResp.Results))
+ for _, row := range apiResp.Results {
+ item := make(map[string]interface{}, len(fields))
+ for i, f := range fields {
+ if i < len(row) {
+ item[f] = row[i]
+ } else {
+ item[f] = nil
+ }
+ }
+ results = append(results, item)
+ }
+
+ c.JSON(http.StatusOK, fofaSearchResponse{
+ Query: req.Query,
+ Size: apiResp.Size,
+ Page: apiResp.Page,
+ Total: apiResp.Total,
+ Fields: fields,
+ ResultsCount: len(results),
+ Results: results,
+ })
+}
+
+func splitAndCleanCSV(s string) []string {
+ parts := strings.Split(s, ",")
+ out := make([]string, 0, len(parts))
+ seen := make(map[string]struct{}, len(parts))
+ for _, p := range parts {
+ v := strings.TrimSpace(p)
+ if v == "" {
+ continue
+ }
+ if _, ok := seen[v]; ok {
+ continue
+ }
+ seen[v] = struct{}{}
+ out = append(out, v)
+ }
+ return out
+}
diff --git a/web/static/css/style.css b/web/static/css/style.css
index 96419805..0821556b 100644
--- a/web/static/css/style.css
+++ b/web/static/css/style.css
@@ -10941,3 +10941,370 @@ header {
font-size: 0.8125rem;
color: var(--text-secondary);
}
+
+/* ==================== 信息收集(FOFA)页面 ==================== */
+.info-collect-panel {
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ padding: 16px;
+ margin-bottom: 16px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
+}
+
+@media (max-width: 980px) {
+ .info-collect-form-row {
+ grid-template-columns: 1fr;
+ }
+ .info-collect-col-actions {
+ width: 140px;
+ }
+}
+
+.info-collect-form-row {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(180px, 1fr));
+ gap: 12px;
+}
+
+.info-collect-form-row .form-group {
+ min-width: 0;
+}
+
+.info-collect-results {
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ overflow: hidden;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.04);
+}
+
+.info-collect-results-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ border-bottom: 1px solid var(--border-color);
+ gap: 12px;
+}
+
+.info-collect-results-header-left {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 240px;
+}
+
+.info-collect-results-toolbar {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+
+.info-collect-selected {
+ font-size: 0.8125rem;
+ color: var(--text-secondary);
+ padding: 4px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 999px;
+ background: var(--bg-secondary);
+ margin-right: 4px;
+}
+
+.info-collect-results-title {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.info-collect-results-meta {
+ font-size: 0.8125rem;
+ color: var(--text-secondary);
+}
+
+.info-collect-results-table-wrap {
+ overflow: auto;
+ max-height: 60vh;
+ position: relative;
+}
+
+.info-collect-table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.875rem;
+ table-layout: fixed;
+}
+
+.info-collect-table th,
+.info-collect-table td {
+ padding: 9px 12px;
+ border-bottom: 1px solid var(--border-color);
+ vertical-align: top;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.info-collect-table th + th,
+.info-collect-table td + td {
+ border-left: 1px solid rgba(0, 0, 0, 0.04);
+}
+
+.info-collect-table thead th {
+ position: sticky;
+ top: 0;
+ background: var(--bg-secondary);
+ color: var(--text-secondary);
+ font-weight: 600;
+ z-index: 1;
+}
+
+.info-collect-table tbody tr:nth-child(even) td {
+ background: rgba(0, 0, 0, 0.012);
+}
+
+.info-collect-table tbody tr:hover td {
+ background: rgba(0, 102, 255, 0.04);
+}
+
+.info-collect-table .muted {
+ color: var(--text-secondary);
+}
+
+/* 操作列:放到最右侧并固定,避免横向滚动找不到 */
+.info-collect-col-actions {
+ width: 150px;
+ text-align: left;
+}
+
+.info-collect-table thead th.info-collect-col-actions,
+.info-collect-table tbody td.info-collect-col-actions {
+ position: sticky;
+ right: 0;
+ z-index: 2;
+ background: var(--bg-primary);
+ border-left: 1px solid var(--border-color);
+}
+
+.info-collect-table thead th.info-collect-col-actions {
+ background: var(--bg-secondary);
+ z-index: 3;
+ text-align: center; /* 表头“操作”居中 */
+}
+
+.info-collect-actions {
+ display: flex;
+ gap: 8px;
+ justify-content: flex-start; /* 按钮向左 */
+}
+
+.info-collect-actions .btn-icon {
+ padding: 6px;
+ border-radius: 8px;
+}
+
+.info-collect-presets {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+ margin-top: 8px;
+}
+
+.info-collect-query-input {
+ min-height: 40px;
+ max-height: 110px;
+ line-height: 1.45;
+ overflow: hidden; /* 配合 JS 自动增高 */
+ resize: none;
+}
+
+.preset-chip {
+ border: 1px solid var(--border-color);
+ background: var(--bg-secondary);
+ color: var(--text-secondary);
+ padding: 6px 10px;
+ border-radius: 999px;
+ cursor: pointer;
+ font-size: 0.8125rem;
+ line-height: 1;
+ transition: all 0.15s ease;
+}
+
+.preset-chip:hover {
+ border-color: var(--accent-color);
+ color: var(--accent-color);
+ background: rgba(0, 102, 255, 0.06);
+}
+
+.info-collect-link {
+ color: var(--accent-color);
+ text-decoration: none;
+ border-bottom: 1px dashed rgba(0, 102, 255, 0.35);
+}
+
+.info-collect-link:hover {
+ border-bottom-color: rgba(0, 102, 255, 0.75);
+}
+
+/* 勾选列(左侧固定) */
+.info-collect-col-select {
+ width: 44px;
+ text-align: center;
+}
+
+.info-collect-table thead th.info-collect-col-select,
+.info-collect-table tbody td.info-collect-col-select {
+ position: sticky;
+ left: 0;
+ z-index: 2;
+ background: var(--bg-primary);
+ border-right: 1px solid var(--border-color);
+}
+
+.info-collect-table thead th.info-collect-col-select {
+ background: var(--bg-secondary);
+ z-index: 4;
+}
+
+/* 单元格点击展开提示(非强制) */
+.info-collect-cell {
+ cursor: pointer;
+}
+
+.info-collect-cell-text {
+ display: inline-block;
+ max-width: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: top;
+}
+
+/* 字段显示/隐藏面板 */
+.info-collect-columns-panel {
+ position: sticky;
+ top: 0;
+ z-index: 6;
+ background: var(--bg-primary);
+ border-bottom: 1px solid var(--border-color);
+ padding: 12px 16px;
+}
+
+.info-collect-columns-panel-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ margin-bottom: 10px;
+}
+
+.info-collect-columns-title {
+ font-weight: 600;
+ color: var(--text-primary);
+}
+
+.info-collect-columns-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+
+.info-collect-columns-list {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(140px, 1fr));
+ gap: 8px;
+}
+
+.info-collect-col-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ border: 1px solid var(--border-color);
+ border-radius: 10px;
+ background: var(--bg-secondary);
+ cursor: pointer;
+ user-select: none;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+ overflow: hidden;
+}
+
+.info-collect-col-item span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* 单元格详情弹窗 */
+.info-collect-cell-modal {
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.45);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 10000;
+ padding: 24px;
+}
+
+.info-collect-cell-modal-content {
+ width: min(920px, 100%);
+ max-height: 86vh;
+ background: var(--bg-primary);
+ border: 1px solid var(--border-color);
+ border-radius: 12px;
+ box-shadow: 0 10px 30px rgba(0,0,0,0.20);
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.info-collect-cell-modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.info-collect-cell-modal-title {
+ font-weight: 600;
+ color: var(--text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-right: 12px;
+}
+
+.info-collect-cell-modal-body {
+ padding: 12px 14px;
+ overflow: auto;
+}
+
+.info-collect-cell-modal-pre {
+ margin: 0;
+ 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);
+}
+
+.info-collect-cell-modal-footer {
+ padding: 12px 14px;
+ border-top: 1px solid var(--border-color);
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+@media (max-width: 980px) {
+ .info-collect-columns-list {
+ grid-template-columns: repeat(2, minmax(140px, 1fr));
+ }
+}
+
diff --git a/web/static/js/info-collect.js b/web/static/js/info-collect.js
new file mode 100644
index 00000000..99b19f2e
--- /dev/null
+++ b/web/static/js/info-collect.js
@@ -0,0 +1,795 @@
+// 信息收集页面(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
+};
+
+// 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'),
+ 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();
+ }
+ });
+
+ // 单行输入:按内容自动增高(避免默认留空白行)
+ const autoGrow = () => {
+ try {
+ els.query.style.height = '40px';
+ const max = 110;
+ const h = Math.min(max, els.query.scrollHeight);
+ els.query.style.height = `${h}px`;
+ } catch (e) {
+ // ignore
+ }
+ };
+ els.query.addEventListener('input', autoGrow);
+ // 初始化时也执行一次
+ setTimeout(autoGrow, 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);
+ }
+}
+
+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 = `
| 加载中... |
`;
+ }
+}
+
+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 = [
+ ' | ',
+ ...visibleFields.map(f => `${escapeHtml(String(f))} | `),
+ '操作 | '
+ ].join('');
+ els.thead.innerHTML = `${headerCells}
`;
+
+ // 表体
+ if (results.length === 0) {
+ const colspan = Math.max(1, visibleFields.length + 2);
+ els.tbody.innerHTML = `| 暂无数据 |
`;
+ 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 = ` | `;
+
+ 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 `${escapeHtml(text)} | `;
+ }
+ }
+ return `${escapeHtml(text)} | `;
+ }).join('');
+
+ const actionHtml = `
+
+
+
+
+ `;
+
+ return `${selectHtml}${cellsHtml}| ${actionHtml} |
`;
+ }).join('');
+
+ els.tbody.innerHTML = rowsHtml;
+
+ // 更新全选框状态
+ syncSelectAllCheckbox();
+}
+
+function inferTargetFromRow(row, fields) {
+ // 优先 host(FOFA 常见返回 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 || '');
+ }
+}
+
+function showInlineToast(text) {
+ const toast = document.createElement('div');
+ toast.textContent = 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;';
+ document.body.appendChild(toast);
+ setTimeout(() => toast.remove(), 1200);
+}
+
+function scanFofaRow(encodedRowJson) {
+ 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';
+ }
+
+ // 尽量切到“信息收集”角色(如果存在)
+ try {
+ if (typeof selectRole === 'function') {
+ selectRole('信息收集');
+ } else if (typeof handleRoleChange === 'function') {
+ handleRoleChange('信息收集');
+ }
+ } catch (e) {
+ // ignore
+ }
+
+ const message = buildScanMessage(target, row);
+
+ setTimeout(() => {
+ const input = document.getElementById('chat-input');
+ if (input) {
+ input.value = message;
+ // 触发自动高度调整(chat.js 里如果监听 input)
+ input.dispatchEvent(new Event('input', { bubbles: true }));
+ }
+ if (typeof sendMessage === 'function') {
+ sendMessage();
+ } else {
+ alert('未找到 sendMessage(),请刷新页面后重试');
+ }
+ }, 200);
+}
+
+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 `
+
+ `;
+ }).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 {
+ 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 = `
+
+
+
+
${escapeHtml(fullText || '')}
+
+
+
+ `;
+
+ 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.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;
+
diff --git a/web/static/js/router.js b/web/static/js/router.js
index 1e88113c..19866608 100644
--- a/web/static/js/router.js
+++ b/web/static/js/router.js
@@ -8,7 +8,7 @@ function initRouter() {
if (hash) {
const hashParts = hash.split('?');
const pageId = hashParts[0];
- if (pageId && ['dashboard', 'chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
+ if (pageId && ['dashboard', 'chat', 'info-collect', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
switchPage(pageId);
// 如果是chat页面且带有conversation参数,加载对应对话
@@ -245,6 +245,12 @@ function initPage(pageId) {
case 'chat':
// 对话页面已由chat.js初始化
break;
+ case 'info-collect':
+ // 信息收集页面
+ if (typeof initInfoCollectPage === 'function') {
+ initInfoCollectPage();
+ }
+ break;
case 'tasks':
// 初始化任务管理页面
if (typeof initTasksPage === 'function') {
@@ -355,7 +361,7 @@ document.addEventListener('DOMContentLoaded', function() {
const hashParts = hash.split('?');
const pageId = hashParts[0];
- if (pageId && ['chat', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
+ if (pageId && ['chat', 'info-collect', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
switchPage(pageId);
// 如果是chat页面且带有conversation参数,加载对应对话
diff --git a/web/static/js/settings.js b/web/static/js/settings.js
index a06d1e08..e4675d2a 100644
--- a/web/static/js/settings.js
+++ b/web/static/js/settings.js
@@ -102,6 +102,15 @@ async function loadConfig(loadTools = true) {
document.getElementById('openai-api-key').value = currentConfig.openai.api_key || '';
document.getElementById('openai-base-url').value = currentConfig.openai.base_url || '';
document.getElementById('openai-model').value = currentConfig.openai.model || '';
+
+ // 填充FOFA配置
+ const fofa = currentConfig.fofa || {};
+ const fofaEmailEl = document.getElementById('fofa-email');
+ const fofaKeyEl = document.getElementById('fofa-api-key');
+ const fofaBaseUrlEl = document.getElementById('fofa-base-url');
+ if (fofaEmailEl) fofaEmailEl.value = fofa.email || '';
+ if (fofaKeyEl) fofaKeyEl.value = fofa.api_key || '';
+ if (fofaBaseUrlEl) fofaBaseUrlEl.value = fofa.base_url || '';
// 填充Agent配置
document.getElementById('agent-max-iterations').value = currentConfig.agent.max_iterations || 30;
@@ -693,6 +702,11 @@ async function applySettings() {
base_url: baseUrl,
model: model
},
+ fofa: {
+ email: document.getElementById('fofa-email')?.value.trim() || '',
+ api_key: document.getElementById('fofa-api-key')?.value.trim() || '',
+ base_url: document.getElementById('fofa-base-url')?.value.trim() || ''
+ },
agent: {
max_iterations: parseInt(document.getElementById('agent-max-iterations').value) || 30
},
diff --git a/web/templates/index.html b/web/templates/index.html
index cc7ba333..f9058c59 100644
--- a/web/templates/index.html
+++ b/web/templates/index.html
@@ -92,6 +92,15 @@
对话
+
+
+
+
+
+
+
+
+
+
+
+
+
+
显示字段
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Agent 配置
@@ -1880,6 +2000,7 @@ version: 1.0.0
+