mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 05:33:32 +02:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d4c1dfb11 | |||
| 747c4a4c01 | |||
| 3d9f600e73 | |||
| 81757948eb | |||
| 98d36f750b | |||
| d598c40570 | |||
| 2064e89356 | |||
| 4a7422cbc4 | |||
| c5fc0fa2c1 | |||
| a98bfa35fd | |||
| bb05f6677f | |||
| 231ef57642 | |||
| 12eecfe5d2 | |||
| 5fa25eacb5 | |||
| 885203358c | |||
| 6fdd2c88da | |||
| 8581027bbe | |||
| 6084d2d84f | |||
| 9e7ef85510 | |||
| 89b4517a83 | |||
| ae528843ff | |||
| fc40b42d35 | |||
| 1336d6f9a6 | |||
| 5ce1fb7501 | |||
| aa9819a2c8 | |||
| 3aee7022c4 | |||
| 4ca1aa9aa8 | |||
| 3448c661b8 | |||
| b524ce68ea | |||
| 2c973f8c3b | |||
| c3a1d95a92 |
+169
@@ -0,0 +1,169 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.2.0] - 2026-01-11
|
||||
|
||||
### Added
|
||||
- Role-based testing feature: predefined security testing roles with custom system prompts and tool restrictions. Users can select roles (Penetration Testing, CTF, Web App Scanning, etc.) from the chat interface to customize AI behavior and available tools. Roles are defined as YAML files in the `roles/` directory with support for hot-reload.
|
||||
|
||||
## [1.1.0] - 2026-01-08
|
||||
|
||||
### Added
|
||||
- SSE (Server-Sent Events) transport mode support for external MCP servers. External MCP federation now supports HTTP, stdio, and SSE modes. SSE mode enables real-time streaming communication for push-based scenarios.
|
||||
|
||||
## [1.0.0] - 2026-01-01
|
||||
|
||||
### Added
|
||||
- Batch task management feature: create task queues with multiple tasks, add/edit/delete tasks before execution, and execute them sequentially. Each task runs as a separate conversation with status tracking (pending/running/completed/failed/cancelled). All queues and tasks are persisted in the database.
|
||||
|
||||
## [0.7.0] - 2025-12-25
|
||||
|
||||
### Added
|
||||
- Vulnerability management feature: full CRUD operations for tracking vulnerabilities discovered during testing. Supports severity levels (critical/high/medium/low/info), status workflow (open/confirmed/fixed/false_positive), filtering by conversation/severity/status, and comprehensive statistics dashboard.
|
||||
- Conversation grouping feature: organize conversations into groups, pin groups to top, rename/delete groups via context menu. All group data is persisted in the database.
|
||||
|
||||
## [0.6.1] - 2025-12-24
|
||||
|
||||
### Changed
|
||||
- Refactored attack chain generation logic, achieving 2x faster generation speed. Redesigned attack chain frontend visualization for improved user experience.
|
||||
|
||||
## [0.6.0] - 2025-12-20
|
||||
|
||||
### Added
|
||||
- Knowledge base feature with vector search, hybrid retrieval, and automatic indexing. AI agent can now search security knowledge during conversations.
|
||||
|
||||
## [0.5.1] - 2025-12-19
|
||||
|
||||
### Added
|
||||
- ZoomEye network space search engine tool (zoomeye_search) with support for IPv4/IPv6/web assets, facets statistics, and flexible query parameters.
|
||||
|
||||
## [0.5.0] - 2025-12-18
|
||||
|
||||
### Changed
|
||||
- Optimized web frontend with enhanced sidebar navigation and improved user experience.
|
||||
|
||||
## [0.4.1] - 2025-12-07
|
||||
|
||||
### Added
|
||||
- FOFA network space search engine tool (fofa_search) with flexible query parameters and field configuration.
|
||||
|
||||
### Fixed
|
||||
- Positional parameter handling bug: ensure correct parameter position when using default values.
|
||||
|
||||
## [0.4.0] - 2025-11-20
|
||||
|
||||
### Added
|
||||
- Automatic compression/summarization for oversized tool logs and MCP transcripts.
|
||||
|
||||
## [0.3.0] - 2025-11-17
|
||||
|
||||
### Added
|
||||
- AI-built attack-chain visualization with interactive graph and risk scoring.
|
||||
|
||||
## [0.2.0] - 2025-11-15
|
||||
|
||||
### Added
|
||||
- Large-result pagination, advanced filtering, and external MCP federation.
|
||||
|
||||
## [0.1.1] - 2025-11-14
|
||||
|
||||
### Changed
|
||||
- Optimized tool lookups to O(1) time complexity.
|
||||
- Execution record cleanup and DB pagination improvements.
|
||||
|
||||
## [0.1.0] - 2025-11-13
|
||||
|
||||
### Added
|
||||
- Web authentication, settings UI, and MCP stdio mode integration.
|
||||
|
||||
---
|
||||
|
||||
# 更新日志
|
||||
|
||||
本项目的重要变更将记录在此文件中。
|
||||
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
并遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||
|
||||
## [未发布]
|
||||
|
||||
## [1.2.0] - 2026-01-11
|
||||
|
||||
### 新增
|
||||
- 角色化测试功能:预设安全测试角色,支持自定义系统提示词和工具限制。用户可在聊天界面选择角色(渗透测试、CTF、Web 应用扫描等),以自定义 AI 行为和可用工具。角色以 YAML 文件形式定义在 `roles/` 目录,支持热加载。
|
||||
|
||||
## [1.1.0] - 2026-01-08
|
||||
|
||||
### 新增
|
||||
- SSE(Server-Sent Events)传输模式支持,外部 MCP 联邦现支持 HTTP、stdio 和 SSE 三种模式。SSE 模式支持实时流式通信,适用于基于推送的场景。
|
||||
|
||||
## [1.0.0] - 2026-01-01
|
||||
|
||||
### 新增
|
||||
- 批量任务管理功能:支持创建任务队列,批量添加多个任务,执行前可编辑或删除任务,然后依次顺序执行。每个任务作为独立对话运行,支持状态跟踪(待执行/执行中/已完成/失败/已取消),所有队列和任务数据持久化存储到数据库。
|
||||
|
||||
## [0.7.0] - 2025-12-25
|
||||
|
||||
### 新增
|
||||
- 漏洞管理功能:完整的漏洞 CRUD 操作,支持跟踪测试过程中发现的漏洞。支持严重程度分级(严重/高/中/低/信息)、状态流转(待确认/已确认/已修复/误报)、按对话/严重程度/状态过滤,以及统计看板。
|
||||
- 对话分组功能:支持创建分组、将对话移动到分组、分组置顶、重命名和删除等操作,所有分组数据持久化存储到数据库。
|
||||
|
||||
## [0.6.1] - 2025-12-24
|
||||
|
||||
### 变更
|
||||
- 重构攻击链生成逻辑,生成速度提升一倍。重构攻击链前端页面展示,优化用户体验。
|
||||
|
||||
## [0.6.0] - 2025-12-20
|
||||
|
||||
### 新增
|
||||
- 知识库功能:支持向量检索、混合搜索与自动索引,AI 智能体可在对话中自动搜索安全知识。
|
||||
|
||||
## [0.5.1] - 2025-12-19
|
||||
|
||||
### 新增
|
||||
- 钟馗之眼(ZoomEye)网络空间搜索引擎工具(zoomeye_search),支持 IPv4/IPv6/Web 等资产搜索、统计项查询与灵活的查询参数配置。
|
||||
|
||||
## [0.5.0] - 2025-12-18
|
||||
|
||||
### 变更
|
||||
- 优化 Web 前端界面,增加侧边栏导航,提升用户体验。
|
||||
|
||||
## [0.4.1] - 2025-12-07
|
||||
|
||||
### 新增
|
||||
- FOFA 网络空间搜索引擎工具(fofa_search),支持灵活的查询参数与字段配置。
|
||||
|
||||
### 修复
|
||||
- 修复位置参数处理 bug:当工具参数使用默认值时,确保后续参数位置正确传递。
|
||||
|
||||
## [0.4.0] - 2025-11-20
|
||||
|
||||
### 新增
|
||||
- 支持超大日志/MCP 记录的自动压缩与摘要回写。
|
||||
|
||||
## [0.3.0] - 2025-11-17
|
||||
|
||||
### 新增
|
||||
- 上线 AI 驱动的攻击链图谱与风险评分。
|
||||
|
||||
## [0.2.0] - 2025-11-15
|
||||
|
||||
### 新增
|
||||
- 提供大结果分页检索与外部 MCP 挂载能力。
|
||||
|
||||
## [0.1.1] - 2025-11-14
|
||||
|
||||
### 变更
|
||||
- 工具检索优化至 O(1) 时间复杂度。
|
||||
- 执行记录清理、数据库分页优化。
|
||||
|
||||
## [0.1.0] - 2025-11-13
|
||||
|
||||
### 新增
|
||||
- Web 鉴权、Settings 面板与 MCP stdio 模式发布。
|
||||
@@ -30,6 +30,9 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
|
||||
### Task Management
|
||||
<img src="./img/任务.png" alt="Task Management" width="560">
|
||||
|
||||
### Role Management
|
||||
<img src="./img/角色管理.png" alt="Role Management" width="560">
|
||||
|
||||
## Highlights
|
||||
|
||||
- 🤖 AI decision engine with OpenAI-compatible models (GPT, Claude, DeepSeek, etc.)
|
||||
@@ -42,6 +45,7 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
|
||||
- 📁 Conversation grouping with pinning, rename, and batch management
|
||||
- 🛡️ Vulnerability management with CRUD operations, severity tracking, status workflow, and statistics
|
||||
- 📋 Batch task management: create task queues, add multiple tasks, and execute them sequentially
|
||||
- 🎭 Role-based testing: predefined security testing roles (Penetration Testing, CTF, Web App Scanning, etc.) with custom prompts and tool restrictions
|
||||
|
||||
## Tool Overview
|
||||
|
||||
@@ -121,6 +125,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
|
||||
### Core Workflows
|
||||
- **Conversation testing** – Natural-language prompts trigger toolchains with streaming SSE output.
|
||||
- **Role-based testing** – Select from predefined security testing roles (Penetration Testing, CTF, Web App Scanning, API Security Testing, etc.) to customize AI behavior and tool availability. Each role applies custom system prompts and can restrict available tools for focused testing scenarios.
|
||||
- **Tool monitor** – Inspect running jobs, execution logs, and large-result attachments.
|
||||
- **History & audit** – Every conversation and tool invocation is stored in SQLite with replay.
|
||||
- **Conversation groups** – Organize conversations into groups, pin important groups, rename or delete groups via context menu.
|
||||
@@ -136,6 +141,28 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
|
||||
## Advanced Usage
|
||||
|
||||
### Role-Based Testing
|
||||
- **Predefined roles** – System includes 12+ predefined security testing roles (Penetration Testing, CTF, Web App Scanning, API Security Testing, Binary Analysis, Cloud Security Audit, etc.) in the `roles/` directory.
|
||||
- **Custom prompts** – Each role can define a `user_prompt` that prepends to user messages, guiding the AI to adopt specialized testing methodologies and focus areas.
|
||||
- **Tool restrictions** – Roles can specify a `tools` list to limit available tools, ensuring focused testing workflows (e.g., CTF role restricts to CTF-specific utilities).
|
||||
- **Easy role creation** – Create custom roles by adding YAML files to the `roles/` directory. Each role defines `name`, `description`, `user_prompt`, `icon`, `tools`, and `enabled` fields.
|
||||
- **Web UI integration** – Select roles from a dropdown in the chat interface. Role selection affects both AI behavior and available tool suggestions.
|
||||
|
||||
**Creating a custom role (example):**
|
||||
1. Create a YAML file in `roles/` (e.g., `roles/custom-role.yaml`):
|
||||
```yaml
|
||||
name: Custom Role
|
||||
description: Specialized testing scenario
|
||||
user_prompt: You are a specialized security tester focusing on API security...
|
||||
icon: "\U0001F4E1"
|
||||
tools:
|
||||
- api-fuzzer
|
||||
- arjun
|
||||
- graphql-scanner
|
||||
enabled: true
|
||||
```
|
||||
2. Restart the server or reload configuration; the role appears in the role selector dropdown.
|
||||
|
||||
### Tool Orchestration & Extensions
|
||||
- **YAML recipes** in `tools/*.yaml` describe commands, arguments, prompts, and metadata.
|
||||
- **Directory hot-reload** – pointing `security.tools_dir` to a folder is usually enough; inline definitions in `config.yaml` remain supported for quick experiments.
|
||||
@@ -292,7 +319,8 @@ A test SSE MCP server is available at `cmd/test-sse-mcp-server/` for validation
|
||||
|
||||
|
||||
### Automation Hooks
|
||||
- **REST APIs** – everything the UI uses (auth, conversations, tool runs, monitor, vulnerabilities) is available over JSON.
|
||||
- **REST APIs** – everything the UI uses (auth, conversations, tool runs, monitor, vulnerabilities, roles) is available over JSON.
|
||||
- **Role APIs** – manage security testing roles via `/api/roles` endpoints: `GET /api/roles` (list all roles), `GET /api/roles/:name` (get role), `POST /api/roles` (create role), `PUT /api/roles/:name` (update role), `DELETE /api/roles/:name` (delete role). Roles are stored as YAML files in the `roles/` directory and support hot-reload.
|
||||
- **Vulnerability APIs** – manage vulnerabilities via `/api/vulnerabilities` endpoints: `GET /api/vulnerabilities` (list with filters), `POST /api/vulnerabilities` (create), `GET /api/vulnerabilities/:id` (get), `PUT /api/vulnerabilities/:id` (update), `DELETE /api/vulnerabilities/:id` (delete), `GET /api/vulnerabilities/stats` (statistics).
|
||||
- **Batch Task APIs** – manage batch task queues via `/api/batch-tasks` endpoints: `POST /api/batch-tasks` (create queue), `GET /api/batch-tasks` (list queues), `GET /api/batch-tasks/:queueId` (get queue), `POST /api/batch-tasks/:queueId/start` (start execution), `POST /api/batch-tasks/:queueId/cancel` (cancel), `DELETE /api/batch-tasks/:queueId` (delete), `POST /api/batch-tasks/:queueId/tasks` (add task), `PUT /api/batch-tasks/:queueId/tasks/:taskId` (update task), `DELETE /api/batch-tasks/:queueId/tasks/:taskId` (delete task). Tasks execute sequentially, each creating a separate conversation with full status tracking.
|
||||
- **Task control** – pause/resume/stop long scans, re-run steps with new params, or stream transcripts.
|
||||
@@ -335,6 +363,7 @@ knowledge:
|
||||
top_k: 5 # Number of top results to return
|
||||
similarity_threshold: 0.7 # Minimum similarity score (0-1)
|
||||
hybrid_weight: 0.7 # Weight for vector search (1.0 = pure vector, 0.0 = pure keyword)
|
||||
roles_dir: "roles" # Role configuration directory (relative to config file)
|
||||
```
|
||||
|
||||
### Tool Definition Example (`tools/nmap.yaml`)
|
||||
@@ -357,6 +386,26 @@ parameters:
|
||||
description: "Range, e.g. 1-1000"
|
||||
```
|
||||
|
||||
### Role Definition Example (`roles/penetration-testing.yaml`)
|
||||
|
||||
```yaml
|
||||
name: Penetration Testing
|
||||
description: Professional penetration testing expert for comprehensive security testing
|
||||
user_prompt: You are a professional cybersecurity penetration testing expert. Please use professional penetration testing methods and tools to conduct comprehensive security testing on targets, including but not limited to SQL injection, XSS, CSRF, file inclusion, command execution and other common vulnerabilities.
|
||||
icon: "\U0001F3AF"
|
||||
tools:
|
||||
- nmap
|
||||
- sqlmap
|
||||
- nuclei
|
||||
- burpsuite
|
||||
- metasploit
|
||||
- httpx
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
```
|
||||
|
||||
## Project Layout
|
||||
|
||||
```
|
||||
@@ -365,6 +414,7 @@ CyberStrikeAI/
|
||||
├── internal/ # Agent, MCP core, handlers, security executor
|
||||
├── web/ # Static SPA + templates
|
||||
├── tools/ # YAML tool recipes (100+ examples provided)
|
||||
├── roles/ # Role configurations (12+ predefined security testing roles)
|
||||
├── img/ # Docs screenshots & diagrams
|
||||
├── config.yaml # Runtime configuration
|
||||
├── run.sh # Convenience launcher
|
||||
@@ -390,23 +440,17 @@ Compress the 5 MB nuclei report, summarize critical CVEs, and attach the artifac
|
||||
Build an attack chain for the latest engagement and export the node list with severity >= high.
|
||||
```
|
||||
|
||||
## Changelog (Recent)
|
||||
## Changelog
|
||||
|
||||
- 2026-01-08 – Added SSE (Server-Sent Events) transport mode support for external MCP servers. External MCP federation now supports HTTP, stdio, and SSE modes. SSE mode enables real-time streaming communication for push-based scenarios.
|
||||
- 2026-01-01 – Added batch task management feature: create task queues with multiple tasks, add/edit/delete tasks before execution, and execute them sequentially. Each task runs as a separate conversation with status tracking (pending/running/completed/failed/cancelled). All queues and tasks are persisted in the database.
|
||||
- 2025-12-25 – Added vulnerability management feature: full CRUD operations for tracking vulnerabilities discovered during testing. Supports severity levels (critical/high/medium/low/info), status workflow (open/confirmed/fixed/false_positive), filtering by conversation/severity/status, and comprehensive statistics dashboard.
|
||||
- 2025-12-25 – Added conversation grouping feature: organize conversations into groups, pin groups to top, rename/delete groups via context menu. All group data is persisted in the database.
|
||||
- 2025-12-24 – Refactored attack chain generation logic, achieving 2x faster generation speed. Redesigned attack chain frontend visualization for improved user experience.
|
||||
- 2025-12-20 – Added knowledge base feature with vector search, hybrid retrieval, and automatic indexing. AI agent can now search security knowledge during conversations.
|
||||
- 2025-12-19 – Added ZoomEye network space search engine tool (zoomeye_search) with support for IPv4/IPv6/web assets, facets statistics, and flexible query parameters.
|
||||
- 2025-12-18 – Optimized web frontend with enhanced sidebar navigation and improved user experience.
|
||||
- 2025-12-07 – Added FOFA network space search engine tool (fofa_search) with flexible query parameters and field configuration.
|
||||
- 2025-12-07 – Fixed positional parameter handling bug: ensure correct parameter position when using default values.
|
||||
- 2025-11-20 – Added automatic compression/summarization for oversized tool logs and MCP transcripts.
|
||||
- 2025-11-17 – Introduced AI-built attack-chain visualization with interactive graph and risk scoring.
|
||||
- 2025-11-15 – Delivered large-result pagination, advanced filtering, and external MCP federation.
|
||||
- 2025-11-14 – Optimized tool lookups (O(1)), execution record cleanup, and DB pagination.
|
||||
- 2025-11-13 – Added web authentication, settings UI, and MCP stdio mode integration.
|
||||
See [CHANGELOG.md](CHANGELOG.md) for detailed version history and all changes.
|
||||
|
||||
### Recent Highlights
|
||||
|
||||
- **2026-01-11** – Role-based testing with predefined security testing roles
|
||||
- **2026-01-08** – SSE transport mode support for external MCP servers
|
||||
- **2026-01-01** – Batch task management with queue-based execution
|
||||
- **2025-12-25** – Vulnerability management and conversation grouping features
|
||||
- **2025-12-20** – Knowledge base with vector search and hybrid retrieval
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
+62
-17
@@ -29,6 +29,9 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
||||
### 任务管理
|
||||
<img src="./img/任务.png" alt="任务管理" width="560">
|
||||
|
||||
### 角色管理
|
||||
<img src="./img/角色管理.png" alt="角色管理" width="560">
|
||||
|
||||
## 特性速览
|
||||
|
||||
- 🤖 兼容 OpenAI/DeepSeek/Claude 等模型的智能决策引擎
|
||||
@@ -41,6 +44,7 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
||||
- 📁 对话分组管理:支持分组创建、置顶、重命名、删除等操作
|
||||
- 🛡️ 漏洞管理功能:完整的漏洞 CRUD 操作,支持严重程度分级、状态流转、按对话/严重程度/状态过滤,以及统计看板
|
||||
- 📋 批量任务管理:创建任务队列,批量添加任务,依次顺序执行,支持任务编辑与状态跟踪
|
||||
- 🎭 角色化测试:预设安全测试角色(渗透测试、CTF、Web 应用扫描等),支持自定义提示词和工具限制
|
||||
|
||||
## 工具概览
|
||||
|
||||
@@ -120,6 +124,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
|
||||
### 常用流程
|
||||
- **对话测试**:自然语言触发多步工具编排,SSE 实时输出。
|
||||
- **角色化测试**:从预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试等)中选择,自定义 AI 行为和可用工具。每个角色可应用自定义系统提示词,并可限制可用工具列表,实现聚焦的测试场景。
|
||||
- **工具监控**:查看任务队列、执行日志、大文件附件。
|
||||
- **会话历史**:所有对话与工具调用保存在 SQLite,可随时重放。
|
||||
- **对话分组**:将对话按项目或主题组织到不同分组,支持置顶、重命名、删除等操作,所有数据持久化存储。
|
||||
@@ -135,6 +140,28 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
|
||||
## 进阶使用
|
||||
|
||||
### 角色化测试
|
||||
- **预设角色**:系统内置 12+ 个预设的安全测试角色(渗透测试、CTF、Web 应用扫描、API 安全测试、二进制分析、云安全审计等),位于 `roles/` 目录。
|
||||
- **自定义提示词**:每个角色可定义 `user_prompt`,会在用户消息前自动添加,引导 AI 采用特定的测试方法和关注重点。
|
||||
- **工具限制**:角色可指定 `tools` 列表,限制可用工具,实现聚焦的测试流程(如 CTF 角色限制为 CTF 专用工具)。
|
||||
- **轻松创建角色**:通过在 `roles/` 目录添加 YAML 文件即可创建自定义角色。每个角色定义 `name`、`description`、`user_prompt`、`icon`、`tools`、`enabled` 字段。
|
||||
- **Web 界面集成**:在聊天界面通过下拉菜单选择角色。角色选择会影响 AI 行为和可用工具建议。
|
||||
|
||||
**创建自定义角色示例:**
|
||||
1. 在 `roles/` 目录创建 YAML 文件(如 `roles/custom-role.yaml`):
|
||||
```yaml
|
||||
name: 自定义角色
|
||||
description: 专用测试场景
|
||||
user_prompt: 你是一个专注于 API 安全的专业安全测试人员...
|
||||
icon: "\U0001F4E1"
|
||||
tools:
|
||||
- api-fuzzer
|
||||
- arjun
|
||||
- graphql-scanner
|
||||
enabled: true
|
||||
```
|
||||
2. 重启服务或重新加载配置,角色会出现在角色选择下拉菜单中。
|
||||
|
||||
### 工具编排与扩展
|
||||
- `tools/*.yaml` 定义命令、参数、提示词与元数据,可热加载。
|
||||
- `security.tools_dir` 指向目录即可批量启用;仍支持在主配置里内联定义。
|
||||
@@ -291,7 +318,8 @@ CyberStrikeAI 支持通过三种传输模式连接外部 MCP 服务器:
|
||||
|
||||
|
||||
### 自动化与安全
|
||||
- **REST API**:认证、会话、任务、监控、漏洞管理等接口全部开放,可与 CI/CD 集成。
|
||||
- **REST API**:认证、会话、任务、监控、漏洞管理、角色管理等接口全部开放,可与 CI/CD 集成。
|
||||
- **角色管理 API**:通过 `/api/roles` 端点管理安全测试角色:`GET /api/roles`(列表)、`GET /api/roles/:name`(获取角色)、`POST /api/roles`(创建角色)、`PUT /api/roles/:name`(更新角色)、`DELETE /api/roles/:name`(删除角色)。角色以 YAML 文件形式存储在 `roles/` 目录,支持热加载。
|
||||
- **漏洞管理 API**:通过 `/api/vulnerabilities` 端点管理漏洞:`GET /api/vulnerabilities`(列表,支持过滤)、`POST /api/vulnerabilities`(创建)、`GET /api/vulnerabilities/:id`(获取)、`PUT /api/vulnerabilities/:id`(更新)、`DELETE /api/vulnerabilities/:id`(删除)、`GET /api/vulnerabilities/stats`(统计)。
|
||||
- **批量任务 API**:通过 `/api/batch-tasks` 端点管理批量任务队列:`POST /api/batch-tasks`(创建队列)、`GET /api/batch-tasks`(列表)、`GET /api/batch-tasks/:queueId`(获取队列)、`POST /api/batch-tasks/:queueId/start`(开始执行)、`POST /api/batch-tasks/:queueId/cancel`(取消)、`DELETE /api/batch-tasks/:queueId`(删除队列)、`POST /api/batch-tasks/:queueId/tasks`(添加任务)、`PUT /api/batch-tasks/:queueId/tasks/:taskId`(更新任务)、`DELETE /api/batch-tasks/:queueId/tasks/:taskId`(删除任务)。任务依次顺序执行,每个任务创建独立对话,支持完整状态跟踪。
|
||||
- **任务控制**:支持暂停/终止长任务、修改参数后重跑、流式获取日志。
|
||||
@@ -334,6 +362,7 @@ knowledge:
|
||||
top_k: 5 # 检索返回的 Top-K 结果数量
|
||||
similarity_threshold: 0.7 # 相似度阈值(0-1),低于此值的结果将被过滤
|
||||
hybrid_weight: 0.7 # 混合检索权重(0-1),向量检索的权重,1.0 表示纯向量检索,0.0 表示纯关键词检索
|
||||
roles_dir: "roles" # 角色配置文件目录(相对于配置文件所在目录)
|
||||
```
|
||||
|
||||
### 工具模版示例(`tools/nmap.yaml`)
|
||||
@@ -356,6 +385,26 @@ parameters:
|
||||
description: "端口范围,如 1-1000"
|
||||
```
|
||||
|
||||
### 角色配置示例(`roles/渗透测试.yaml`)
|
||||
|
||||
```yaml
|
||||
name: 渗透测试
|
||||
description: 专业渗透测试专家,全面深入的漏洞检测
|
||||
user_prompt: 你是一个专业的网络安全渗透测试专家。请使用专业的渗透测试方法和工具,对目标进行全面的安全测试,包括但不限于SQL注入、XSS、CSRF、文件包含、命令执行等常见漏洞。
|
||||
icon: "\U0001F3AF"
|
||||
tools:
|
||||
- nmap
|
||||
- sqlmap
|
||||
- nuclei
|
||||
- burpsuite
|
||||
- metasploit
|
||||
- httpx
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
```
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
@@ -364,6 +413,7 @@ CyberStrikeAI/
|
||||
├── internal/ # Agent、MCP 核心、路由与执行器
|
||||
├── web/ # 前端静态资源与模板
|
||||
├── tools/ # YAML 工具目录(含 100+ 示例)
|
||||
├── roles/ # 角色配置文件目录(含 12+ 预设安全测试角色)
|
||||
├── img/ # 文档配图
|
||||
├── config.yaml # 运行配置
|
||||
├── run.sh # 启动脚本
|
||||
@@ -389,22 +439,17 @@ CyberStrikeAI/
|
||||
构建最新一次测试的攻击链,只导出风险 >= 高的节点列表。
|
||||
```
|
||||
|
||||
## Changelog(近期)
|
||||
- 2026-01-08 —— 新增 SSE(Server-Sent Events)传输模式支持,外部 MCP 联邦现支持 HTTP、stdio 和 SSE 三种模式。SSE 模式支持实时流式通信,适用于基于推送的场景。
|
||||
- 2026-01-01 —— 新增批量任务管理功能:支持创建任务队列,批量添加多个任务,执行前可编辑或删除任务,然后依次顺序执行。每个任务作为独立对话运行,支持状态跟踪(待执行/执行中/已完成/失败/已取消),所有队列和任务数据持久化存储到数据库。
|
||||
- 2025-12-25 —— 新增漏洞管理功能:完整的漏洞 CRUD 操作,支持跟踪测试过程中发现的漏洞。支持严重程度分级(严重/高/中/低/信息)、状态流转(待确认/已确认/已修复/误报)、按对话/严重程度/状态过滤,以及统计看板。
|
||||
- 2025-12-25 —— 新增对话分组功能:支持创建分组、将对话移动到分组、分组置顶、重命名和删除等操作,所有分组数据持久化存储到数据库。
|
||||
- 2025-12-24 —— 重构攻击链生成逻辑,生成速度提升一倍。重构攻击链前端页面展示,优化用户体验。
|
||||
- 2025-12-20 —— 新增知识库功能:支持向量检索、混合搜索与自动索引,AI 智能体可在对话中自动搜索安全知识。
|
||||
- 2025-12-19 —— 新增钟馗之眼(ZoomEye)网络空间搜索引擎工具(zoomeye_search),支持 IPv4/IPv6/Web 等资产搜索、统计项查询与灵活的查询参数配置。
|
||||
- 2025-12-18 —— 优化 Web 前端界面,增加侧边栏导航,提升用户体验。
|
||||
- 2025-12-07 —— 新增 FOFA 网络空间搜索引擎工具(fofa_search),支持灵活的查询参数与字段配置。
|
||||
- 2025-12-07 —— 修复位置参数处理 bug:当工具参数使用默认值时,确保后续参数位置正确传递。
|
||||
- 2025-11-20 —— 支持超大日志/MCP 记录的自动压缩与摘要回写。
|
||||
- 2025-11-17 —— 上线 AI 驱动的攻击链图谱与风险评分。
|
||||
- 2025-11-15 —— 提供大结果分页检索与外部 MCP 挂载能力。
|
||||
- 2025-11-14 —— 工具检索 O(1)、执行记录清理、数据库分页优化。
|
||||
- 2025-11-13 —— Web 鉴权、Settings 面板与 MCP stdio 模式发布。
|
||||
## 更新日志
|
||||
|
||||
详细版本历史和所有变更请查看 [CHANGELOG.md](CHANGELOG.md)。
|
||||
|
||||
### 近期亮点
|
||||
|
||||
- **2026-01-11** – 新增角色化测试功能,支持预设安全测试角色
|
||||
- **2026-01-08** – 新增 SSE 传输模式支持,外部 MCP 联邦支持三种模式
|
||||
- **2026-01-01** – 新增批量任务管理功能,支持队列式任务执行
|
||||
- **2025-12-25** – 新增漏洞管理和对话分组功能
|
||||
- **2025-12-20** – 新增知识库功能,支持向量检索和混合搜索
|
||||
|
||||
## Star History
|
||||
|
||||
|
||||
@@ -66,3 +66,7 @@ knowledge:
|
||||
top_k: 5 # 检索返回的Top-K结果数量
|
||||
similarity_threshold: 0.7 # 相似度阈值(0-1),低于此值的结果将被过滤
|
||||
hybrid_weight: 0.7 # 混合检索权重(0-1),向量检索的权重,1.0表示纯向量检索,0.0表示纯关键词检索
|
||||
# 角色配置
|
||||
roles_dir: roles # 角色配置文件目录(相对于配置文件所在目录)
|
||||
# 系统会从该目录加载所有 .yaml 格式的角色配置文件
|
||||
# 每个角色应创建独立的配置文件,例如:roles/CTF.yaml, roles/默认.yaml 等
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 543 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 506 KiB |
+35
-8
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
"cyberstrike-ai/internal/mcp/builtin"
|
||||
"cyberstrike-ai/internal/openai"
|
||||
"cyberstrike-ai/internal/storage"
|
||||
|
||||
@@ -302,16 +303,16 @@ type ProgressCallback func(eventType, message string, data interface{})
|
||||
|
||||
// AgentLoop 执行Agent循环
|
||||
func (a *Agent) AgentLoop(ctx context.Context, userInput string, historyMessages []ChatMessage) (*AgentLoopResult, error) {
|
||||
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, "", nil)
|
||||
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, "", nil, nil)
|
||||
}
|
||||
|
||||
// AgentLoopWithConversationID 执行Agent循环(带对话ID)
|
||||
func (a *Agent) AgentLoopWithConversationID(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string) (*AgentLoopResult, error) {
|
||||
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil)
|
||||
return a.AgentLoopWithProgress(ctx, userInput, historyMessages, conversationID, nil, nil)
|
||||
}
|
||||
|
||||
// AgentLoopWithProgress 执行Agent循环(带进度回调和对话ID)
|
||||
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback) (*AgentLoopResult, error) {
|
||||
func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, historyMessages []ChatMessage, conversationID string, callback ProgressCallback, roleTools []string) (*AgentLoopResult, error) {
|
||||
// 设置当前对话ID
|
||||
a.mu.Lock()
|
||||
a.currentConversationID = conversationID
|
||||
@@ -401,8 +402,8 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his
|
||||
当工具返回错误时,错误信息会包含在工具响应中,请仔细阅读并做出合理的决策。
|
||||
|
||||
漏洞记录要求:
|
||||
- 当你发现有效漏洞时,必须使用 record_vulnerability 工具记录漏洞详情
|
||||
- 漏洞记录应包含:标题、描述、严重程度、类型、目标、证明(POC)、影响和修复建议
|
||||
- 当你发现有效漏洞时,必须使用 ` + builtin.ToolRecordVulnerability + ` 工具记录漏洞详情
|
||||
` + `- 漏洞记录应包含:标题、描述、严重程度、类型、目标、证明(POC)、影响和修复建议
|
||||
- 严重程度评估标准:
|
||||
* critical(严重):可导致系统完全被控制、数据泄露、服务中断等
|
||||
* high(高):可导致敏感信息泄露、权限提升、重要功能被绕过等
|
||||
@@ -512,7 +513,7 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his
|
||||
}
|
||||
|
||||
// 获取可用工具
|
||||
tools := a.getAvailableTools()
|
||||
tools := a.getAvailableTools(roleTools)
|
||||
|
||||
// 记录当前上下文的Token用量,展示压缩器运行状态
|
||||
if a.memoryCompressor != nil {
|
||||
@@ -837,13 +838,29 @@ func (a *Agent) AgentLoopWithProgress(ctx context.Context, userInput string, his
|
||||
|
||||
// getAvailableTools 获取可用工具
|
||||
// 从MCP服务器动态获取工具列表,使用简短描述以减少token消耗
|
||||
func (a *Agent) getAvailableTools() []Tool {
|
||||
// roleTools: 角色配置的工具列表(toolKey格式),如果为空或nil,则使用所有工具(默认角色)
|
||||
func (a *Agent) getAvailableTools(roleTools []string) []Tool {
|
||||
// 构建角色工具集合(用于快速查找)
|
||||
roleToolSet := make(map[string]bool)
|
||||
if len(roleTools) > 0 {
|
||||
for _, toolKey := range roleTools {
|
||||
roleToolSet[toolKey] = true
|
||||
}
|
||||
}
|
||||
|
||||
// 从MCP服务器获取所有已注册的内部工具
|
||||
mcpTools := a.mcpServer.GetAllTools()
|
||||
|
||||
// 转换为OpenAI格式的工具定义
|
||||
tools := make([]Tool, 0, len(mcpTools))
|
||||
for _, mcpTool := range mcpTools {
|
||||
// 如果指定了角色工具列表,只添加在列表中的工具
|
||||
if len(roleToolSet) > 0 {
|
||||
toolKey := mcpTool.Name // 内置工具使用工具名称作为key
|
||||
if !roleToolSet[toolKey] {
|
||||
continue // 不在角色工具列表中,跳过
|
||||
}
|
||||
}
|
||||
// 使用简短描述(如果存在),否则使用详细描述
|
||||
description := mcpTool.ShortDescription
|
||||
if description == "" {
|
||||
@@ -883,6 +900,16 @@ func (a *Agent) getAvailableTools() []Tool {
|
||||
|
||||
// 将外部MCP工具添加到工具列表(只添加启用的工具)
|
||||
for _, externalTool := range externalTools {
|
||||
// 外部工具使用 "mcpName::toolName" 作为toolKey
|
||||
externalToolKey := externalTool.Name
|
||||
|
||||
// 如果指定了角色工具列表,只添加在列表中的工具
|
||||
if len(roleToolSet) > 0 {
|
||||
if !roleToolSet[externalToolKey] {
|
||||
continue // 不在角色工具列表中,跳过
|
||||
}
|
||||
}
|
||||
|
||||
// 解析工具名称:mcpName::toolName
|
||||
var mcpName, actualToolName string
|
||||
if idx := strings.Index(externalTool.Name, "::"); idx > 0 {
|
||||
@@ -1136,7 +1163,7 @@ func (a *Agent) executeToolViaMCP(ctx context.Context, toolName string, args map
|
||||
)
|
||||
|
||||
// 如果是record_vulnerability工具,自动添加conversation_id
|
||||
if toolName == "record_vulnerability" {
|
||||
if toolName == builtin.ToolRecordVulnerability {
|
||||
a.mu.RLock()
|
||||
conversationID := a.currentConversationID
|
||||
a.mu.RUnlock()
|
||||
|
||||
+13
-2
@@ -16,6 +16,7 @@ import (
|
||||
"cyberstrike-ai/internal/knowledge"
|
||||
"cyberstrike-ai/internal/logger"
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
"cyberstrike-ai/internal/mcp/builtin"
|
||||
"cyberstrike-ai/internal/openai"
|
||||
"cyberstrike-ai/internal/security"
|
||||
"cyberstrike-ai/internal/storage"
|
||||
@@ -278,7 +279,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
}
|
||||
|
||||
// 创建处理器
|
||||
agentHandler := handler.NewAgentHandler(agent, db, log.Logger)
|
||||
agentHandler := handler.NewAgentHandler(agent, db, cfg, log.Logger)
|
||||
// 如果知识库已启用,设置知识库管理器到AgentHandler以便记录检索日志
|
||||
if knowledgeManager != nil {
|
||||
agentHandler.SetKnowledgeManager(knowledgeManager)
|
||||
@@ -292,6 +293,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
vulnerabilityHandler := handler.NewVulnerabilityHandler(db, log.Logger)
|
||||
configHandler := handler.NewConfigHandler(configPath, cfg, mcpServer, executor, agent, attackChainHandler, externalMCPMgr, log.Logger)
|
||||
externalMCPHandler := handler.NewExternalMCPHandler(externalMCPMgr, cfg, configPath, log.Logger)
|
||||
roleHandler := handler.NewRoleHandler(cfg, configPath, log.Logger)
|
||||
|
||||
// 创建 App 实例(部分字段稍后填充)
|
||||
app := &App{
|
||||
@@ -368,6 +370,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
attackChainHandler,
|
||||
app, // 传递 App 实例以便动态获取 knowledgeHandler
|
||||
vulnerabilityHandler,
|
||||
roleHandler,
|
||||
mcpServer,
|
||||
authManager,
|
||||
)
|
||||
@@ -428,6 +431,7 @@ func setupRoutes(
|
||||
attackChainHandler *handler.AttackChainHandler,
|
||||
app *App, // 传递 App 实例以便动态获取 knowledgeHandler
|
||||
vulnerabilityHandler *handler.VulnerabilityHandler,
|
||||
roleHandler *handler.RoleHandler,
|
||||
mcpServer *mcp.Server,
|
||||
authManager *security.AuthManager,
|
||||
) {
|
||||
@@ -653,6 +657,13 @@ func setupRoutes(
|
||||
protected.PUT("/vulnerabilities/:id", vulnerabilityHandler.UpdateVulnerability)
|
||||
protected.DELETE("/vulnerabilities/:id", vulnerabilityHandler.DeleteVulnerability)
|
||||
|
||||
// 角色管理
|
||||
protected.GET("/roles", roleHandler.GetRoles)
|
||||
protected.GET("/roles/:name", roleHandler.GetRole)
|
||||
protected.POST("/roles", roleHandler.CreateRole)
|
||||
protected.PUT("/roles/:name", roleHandler.UpdateRole)
|
||||
protected.DELETE("/roles/:name", roleHandler.DeleteRole)
|
||||
|
||||
// MCP端点
|
||||
protected.POST("/mcp", func(c *gin.Context) {
|
||||
mcpServer.HandleHTTP(c.Writer, c.Request)
|
||||
@@ -672,7 +683,7 @@ func setupRoutes(
|
||||
// registerVulnerabilityTool 注册漏洞记录工具到MCP服务器
|
||||
func registerVulnerabilityTool(mcpServer *mcp.Server, db *database.DB, logger *zap.Logger) {
|
||||
tool := mcp.Tool{
|
||||
Name: "record_vulnerability",
|
||||
Name: builtin.ToolRecordVulnerability,
|
||||
Description: "记录发现的漏洞详情到漏洞管理系统。当发现有效漏洞时,使用此工具记录漏洞信息,包括标题、描述、严重程度、类型、目标、证明、影响和建议等。",
|
||||
ShortDescription: "记录发现的漏洞详情到漏洞管理系统",
|
||||
InputSchema: map[string]interface{}{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
@@ -22,6 +23,8 @@ type Config struct {
|
||||
Auth AuthConfig `yaml:"auth"`
|
||||
ExternalMCP ExternalMCPConfig `yaml:"external_mcp,omitempty"`
|
||||
Knowledge KnowledgeConfig `yaml:"knowledge,omitempty"`
|
||||
RolesDir string `yaml:"roles_dir,omitempty" json:"roles_dir,omitempty"` // 角色配置文件目录(新方式)
|
||||
Roles map[string]RoleConfig `yaml:"roles,omitempty" json:"roles,omitempty"` // 向后兼容:支持在主配置文件中定义角色
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -81,6 +84,7 @@ type ExternalMCPServerConfig struct {
|
||||
// stdio模式配置
|
||||
Command string `yaml:"command,omitempty" json:"command,omitempty"`
|
||||
Args []string `yaml:"args,omitempty" json:"args,omitempty"`
|
||||
Env map[string]string `yaml:"env,omitempty" json:"env,omitempty"` // 环境变量(用于stdio模式)
|
||||
|
||||
// HTTP模式配置
|
||||
Transport string `yaml:"transport,omitempty" json:"transport,omitempty"` // "http" 或 "stdio"
|
||||
@@ -206,6 +210,29 @@ func Load(path string) (*Config, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 从角色目录加载角色配置
|
||||
if cfg.RolesDir != "" {
|
||||
configDir := filepath.Dir(path)
|
||||
rolesDir := cfg.RolesDir
|
||||
|
||||
// 如果是相对路径,相对于配置文件所在目录
|
||||
if !filepath.IsAbs(rolesDir) {
|
||||
rolesDir = filepath.Join(configDir, rolesDir)
|
||||
}
|
||||
|
||||
roles, err := LoadRolesFromDir(rolesDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("从角色目录加载角色配置失败: %w", err)
|
||||
}
|
||||
|
||||
cfg.Roles = roles
|
||||
} else {
|
||||
// 如果未配置 roles_dir,初始化为空 map
|
||||
if cfg.Roles == nil {
|
||||
cfg.Roles = make(map[string]RoleConfig)
|
||||
}
|
||||
}
|
||||
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
@@ -374,6 +401,98 @@ func LoadToolFromFile(path string) (*ToolConfig, error) {
|
||||
return &tool, nil
|
||||
}
|
||||
|
||||
// LoadRolesFromDir 从目录加载所有角色配置文件
|
||||
func LoadRolesFromDir(dir string) (map[string]RoleConfig, error) {
|
||||
roles := make(map[string]RoleConfig)
|
||||
|
||||
// 检查目录是否存在
|
||||
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||
return roles, nil // 目录不存在时返回空map,不报错
|
||||
}
|
||||
|
||||
// 读取目录中的所有 .yaml 和 .yml 文件
|
||||
entries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取角色目录失败: %w", err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
name := entry.Name()
|
||||
if !strings.HasSuffix(name, ".yaml") && !strings.HasSuffix(name, ".yml") {
|
||||
continue
|
||||
}
|
||||
|
||||
filePath := filepath.Join(dir, name)
|
||||
role, err := LoadRoleFromFile(filePath)
|
||||
if err != nil {
|
||||
// 记录错误但继续加载其他文件
|
||||
fmt.Printf("警告: 加载角色配置文件 %s 失败: %v\n", filePath, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// 使用角色名称作为key
|
||||
roleName := role.Name
|
||||
if roleName == "" {
|
||||
// 如果角色名称为空,使用文件名(去掉扩展名)作为名称
|
||||
roleName = strings.TrimSuffix(strings.TrimSuffix(name, ".yaml"), ".yml")
|
||||
role.Name = roleName
|
||||
}
|
||||
|
||||
roles[roleName] = *role
|
||||
}
|
||||
|
||||
return roles, nil
|
||||
}
|
||||
|
||||
// LoadRoleFromFile 从单个文件加载角色配置
|
||||
func LoadRoleFromFile(path string) (*RoleConfig, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取文件失败: %w", err)
|
||||
}
|
||||
|
||||
var role RoleConfig
|
||||
if err := yaml.Unmarshal(data, &role); err != nil {
|
||||
return nil, fmt.Errorf("解析角色配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 处理 icon 字段:如果包含 Unicode 转义格式(\U0001F3C6),转换为实际的 Unicode 字符
|
||||
// Go 的 yaml 库可能不会自动解析 \U 转义序列,需要手动转换
|
||||
if role.Icon != "" {
|
||||
icon := role.Icon
|
||||
// 去除可能的引号
|
||||
icon = strings.Trim(icon, `"`)
|
||||
|
||||
// 检查是否是 Unicode 转义格式 \U0001F3C6(8位十六进制)或 \uXXXX(4位十六进制)
|
||||
if len(icon) >= 3 && icon[0] == '\\' {
|
||||
if icon[1] == 'U' && len(icon) >= 10 {
|
||||
// \U0001F3C6 格式(8位十六进制)
|
||||
if codePoint, err := strconv.ParseInt(icon[2:10], 16, 32); err == nil {
|
||||
role.Icon = string(rune(codePoint))
|
||||
}
|
||||
} else if icon[1] == 'u' && len(icon) >= 6 {
|
||||
// \uXXXX 格式(4位十六进制)
|
||||
if codePoint, err := strconv.ParseInt(icon[2:6], 16, 32); err == nil {
|
||||
role.Icon = string(rune(codePoint))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 验证必需字段
|
||||
if role.Name == "" {
|
||||
// 如果名称为空,尝试从文件名获取
|
||||
baseName := filepath.Base(path)
|
||||
role.Name = strings.TrimSuffix(strings.TrimSuffix(baseName, ".yaml"), ".yml")
|
||||
}
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
func Default() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
@@ -447,3 +566,20 @@ type RetrievalConfig struct {
|
||||
SimilarityThreshold float64 `yaml:"similarity_threshold" json:"similarity_threshold"` // 相似度阈值
|
||||
HybridWeight float64 `yaml:"hybrid_weight" json:"hybrid_weight"` // 向量检索权重(0-1)
|
||||
}
|
||||
|
||||
// RolesConfig 角色配置(已废弃,使用 map[string]RoleConfig 替代)
|
||||
// 保留此类型以兼容旧代码,但建议直接使用 map[string]RoleConfig
|
||||
type RolesConfig struct {
|
||||
Roles map[string]RoleConfig `yaml:"roles,omitempty" json:"roles,omitempty"`
|
||||
}
|
||||
|
||||
// RoleConfig 单个角色配置
|
||||
type RoleConfig struct {
|
||||
Name string `yaml:"name" json:"name"` // 角色名称
|
||||
Description string `yaml:"description" json:"description"` // 角色描述
|
||||
UserPrompt string `yaml:"user_prompt" json:"user_prompt"` // 用户提示词(追加到用户消息前)
|
||||
Icon string `yaml:"icon,omitempty" json:"icon,omitempty"` // 角色图标(可选)
|
||||
Tools []string `yaml:"tools,omitempty" json:"tools,omitempty"` // 关联的工具列表(toolKey格式,如 "toolName" 或 "mcpName::toolName")
|
||||
MCPs []string `yaml:"mcps,omitempty" json:"mcps,omitempty"` // 向后兼容:关联的MCP服务器列表(已废弃,使用tools替代)
|
||||
Enabled bool `yaml:"enabled" json:"enabled"` // 是否启用
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
type BatchTaskQueueRow struct {
|
||||
ID string
|
||||
Title sql.NullString
|
||||
Role sql.NullString
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
StartedAt sql.NullTime
|
||||
@@ -33,7 +34,7 @@ type BatchTaskRow struct {
|
||||
}
|
||||
|
||||
// CreateBatchQueue 创建批量任务队列
|
||||
func (db *DB) CreateBatchQueue(queueID string, title string, tasks []map[string]interface{}) error {
|
||||
func (db *DB) CreateBatchQueue(queueID string, title string, role string, tasks []map[string]interface{}) error {
|
||||
tx, err := db.Begin()
|
||||
if err != nil {
|
||||
return fmt.Errorf("开始事务失败: %w", err)
|
||||
@@ -42,8 +43,8 @@ func (db *DB) CreateBatchQueue(queueID string, title string, tasks []map[string]
|
||||
|
||||
now := time.Now()
|
||||
_, err = tx.Exec(
|
||||
"INSERT INTO batch_task_queues (id, title, status, created_at, current_index) VALUES (?, ?, ?, ?, ?)",
|
||||
queueID, title, "pending", now, 0,
|
||||
"INSERT INTO batch_task_queues (id, title, role, status, created_at, current_index) VALUES (?, ?, ?, ?, ?, ?)",
|
||||
queueID, title, role, "pending", now, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建批量任务队列失败: %w", err)
|
||||
@@ -77,9 +78,9 @@ func (db *DB) GetBatchQueue(queueID string) (*BatchTaskQueueRow, error) {
|
||||
var row BatchTaskQueueRow
|
||||
var createdAt string
|
||||
err := db.QueryRow(
|
||||
"SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE id = ?",
|
||||
"SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE id = ?",
|
||||
queueID,
|
||||
).Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex)
|
||||
).Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -103,7 +104,7 @@ func (db *DB) GetBatchQueue(queueID string) (*BatchTaskQueueRow, error) {
|
||||
// GetAllBatchQueues 获取所有批量任务队列
|
||||
func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
|
||||
rows, err := db.Query(
|
||||
"SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues ORDER BY created_at DESC",
|
||||
"SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues ORDER BY created_at DESC",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询批量任务队列列表失败: %w", err)
|
||||
@@ -114,7 +115,7 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
|
||||
for rows.Next() {
|
||||
var row BatchTaskQueueRow
|
||||
var createdAt string
|
||||
if err := rows.Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
|
||||
if err := rows.Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
|
||||
return nil, fmt.Errorf("扫描批量任务队列失败: %w", err)
|
||||
}
|
||||
parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
@@ -134,7 +135,7 @@ func (db *DB) GetAllBatchQueues() ([]*BatchTaskQueueRow, error) {
|
||||
|
||||
// ListBatchQueues 列出批量任务队列(支持筛选和分页)
|
||||
func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*BatchTaskQueueRow, error) {
|
||||
query := "SELECT id, title, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE 1=1"
|
||||
query := "SELECT id, title, role, status, created_at, started_at, completed_at, current_index FROM batch_task_queues WHERE 1=1"
|
||||
args := []interface{}{}
|
||||
|
||||
// 状态筛选
|
||||
@@ -162,7 +163,7 @@ func (db *DB) ListBatchQueues(limit, offset int, status, keyword string) ([]*Bat
|
||||
for rows.Next() {
|
||||
var row BatchTaskQueueRow
|
||||
var createdAt string
|
||||
if err := rows.Scan(&row.ID, &row.Title, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
|
||||
if err := rows.Scan(&row.ID, &row.Title, &row.Role, &row.Status, &createdAt, &row.StartedAt, &row.CompletedAt, &row.CurrentIndex); err != nil {
|
||||
return nil, fmt.Errorf("扫描批量任务队列失败: %w", err)
|
||||
}
|
||||
parsedTime, parseErr := time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
|
||||
@@ -433,7 +433,7 @@ func (db *DB) migrateConversationGroupMappingsTable() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// migrateBatchTaskQueuesTable 迁移batch_task_queues表,添加title字段
|
||||
// migrateBatchTaskQueuesTable 迁移batch_task_queues表,添加title和role字段
|
||||
func (db *DB) migrateBatchTaskQueuesTable() error {
|
||||
// 检查title字段是否存在
|
||||
var count int
|
||||
@@ -454,6 +454,25 @@ func (db *DB) migrateBatchTaskQueuesTable() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查role字段是否存在
|
||||
var roleCount int
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('batch_task_queues') WHERE name='role'").Scan(&roleCount)
|
||||
if err != nil {
|
||||
// 如果查询失败,尝试添加字段
|
||||
if _, addErr := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN role TEXT"); addErr != nil {
|
||||
// 如果字段已存在,忽略错误
|
||||
errMsg := strings.ToLower(addErr.Error())
|
||||
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||
db.logger.Warn("添加role字段失败", zap.Error(addErr))
|
||||
}
|
||||
}
|
||||
} else if roleCount == 0 {
|
||||
// 字段不存在,添加它
|
||||
if _, err := db.Exec("ALTER TABLE batch_task_queues ADD COLUMN role TEXT"); err != nil {
|
||||
db.logger.Warn("添加role字段失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -205,7 +205,7 @@ func (db *DB) AddConversationToGroup(conversationID, groupID string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("删除对话旧分组关联失败: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// 然后插入新的分组关联
|
||||
id := uuid.New().String()
|
||||
_, err = db.Exec(
|
||||
@@ -282,6 +282,78 @@ func (db *DB) GetConversationsByGroup(groupID string) ([]*Conversation, error) {
|
||||
return conversations, nil
|
||||
}
|
||||
|
||||
// SearchConversationsByGroup 搜索分组中的对话(按标题和消息内容模糊匹配)
|
||||
func (db *DB) SearchConversationsByGroup(groupID string, searchQuery string) ([]*Conversation, error) {
|
||||
// 构建SQL查询,支持按标题和消息内容搜索
|
||||
// 使用 DISTINCT 避免因为一个对话有多条匹配消息而重复
|
||||
query := `SELECT DISTINCT c.id, c.title, COALESCE(c.pinned, 0), c.created_at, c.updated_at, COALESCE(cgm.pinned, 0) as group_pinned
|
||||
FROM conversations c
|
||||
INNER JOIN conversation_group_mappings cgm ON c.id = cgm.conversation_id
|
||||
WHERE cgm.group_id = ?`
|
||||
|
||||
args := []interface{}{groupID}
|
||||
|
||||
// 如果有搜索关键词,添加标题和消息内容搜索条件
|
||||
if searchQuery != "" {
|
||||
searchPattern := "%" + searchQuery + "%"
|
||||
// 搜索标题或消息内容
|
||||
// 使用 LEFT JOIN 连接消息表,这样即使没有消息的对话也能被搜索到(通过标题)
|
||||
query += ` AND (
|
||||
LOWER(c.title) LIKE LOWER(?)
|
||||
OR EXISTS (
|
||||
SELECT 1 FROM messages m
|
||||
WHERE m.conversation_id = c.id
|
||||
AND LOWER(m.content) LIKE LOWER(?)
|
||||
)
|
||||
)`
|
||||
args = append(args, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
query += " ORDER BY COALESCE(cgm.pinned, 0) DESC, c.updated_at DESC"
|
||||
|
||||
rows, err := db.Query(query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("搜索分组对话失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var conversations []*Conversation
|
||||
for rows.Next() {
|
||||
var conv Conversation
|
||||
var createdAt, updatedAt string
|
||||
var pinned int
|
||||
var groupPinned int
|
||||
|
||||
if err := rows.Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt, &groupPinned); err != nil {
|
||||
return nil, fmt.Errorf("扫描对话失败: %w", err)
|
||||
}
|
||||
|
||||
// 尝试多种时间格式解析
|
||||
var err1, err2 error
|
||||
conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt)
|
||||
if err1 != nil {
|
||||
conv.CreatedAt, err1 = time.Parse("2006-01-02 15:04:05", createdAt)
|
||||
}
|
||||
if err1 != nil {
|
||||
conv.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
}
|
||||
|
||||
conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt)
|
||||
if err2 != nil {
|
||||
conv.UpdatedAt, err2 = time.Parse("2006-01-02 15:04:05", updatedAt)
|
||||
}
|
||||
if err2 != nil {
|
||||
conv.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
}
|
||||
|
||||
conv.Pinned = pinned != 0
|
||||
|
||||
conversations = append(conversations, &conv)
|
||||
}
|
||||
|
||||
return conversations, nil
|
||||
}
|
||||
|
||||
// GetGroupByConversation 获取对话所属的分组
|
||||
func (db *DB) GetGroupByConversation(conversationID string) (string, error) {
|
||||
var groupID string
|
||||
|
||||
+88
-16
@@ -12,7 +12,9 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"cyberstrike-ai/internal/agent"
|
||||
"cyberstrike-ai/internal/config"
|
||||
"cyberstrike-ai/internal/database"
|
||||
"cyberstrike-ai/internal/mcp/builtin"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
@@ -66,13 +68,14 @@ type AgentHandler struct {
|
||||
logger *zap.Logger
|
||||
tasks *AgentTaskManager
|
||||
batchTaskManager *BatchTaskManager
|
||||
knowledgeManager interface { // 知识库管理器接口
|
||||
config *config.Config // 配置引用,用于获取角色信息
|
||||
knowledgeManager interface { // 知识库管理器接口
|
||||
LogRetrieval(conversationID, messageID, query, riskType string, retrievedItems []string) error
|
||||
}
|
||||
}
|
||||
|
||||
// NewAgentHandler 创建新的Agent处理器
|
||||
func NewAgentHandler(agent *agent.Agent, db *database.DB, logger *zap.Logger) *AgentHandler {
|
||||
func NewAgentHandler(agent *agent.Agent, db *database.DB, cfg *config.Config, logger *zap.Logger) *AgentHandler {
|
||||
batchTaskManager := NewBatchTaskManager()
|
||||
batchTaskManager.SetDB(db)
|
||||
|
||||
@@ -87,6 +90,7 @@ func NewAgentHandler(agent *agent.Agent, db *database.DB, logger *zap.Logger) *A
|
||||
logger: logger,
|
||||
tasks: NewAgentTaskManager(),
|
||||
batchTaskManager: batchTaskManager,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +105,7 @@ func (h *AgentHandler) SetKnowledgeManager(manager interface {
|
||||
type ChatRequest struct {
|
||||
Message string `json:"message" binding:"required"`
|
||||
ConversationID string `json:"conversationId,omitempty"`
|
||||
Role string `json:"role,omitempty"` // 角色名称
|
||||
}
|
||||
|
||||
// ChatResponse 聊天响应
|
||||
@@ -161,14 +166,34 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
|
||||
h.logger.Info("从ReAct数据恢复历史上下文", zap.Int("count", len(agentHistoryMessages)))
|
||||
}
|
||||
|
||||
// 保存用户消息
|
||||
// 应用角色用户提示词和工具配置
|
||||
finalMessage := req.Message
|
||||
var roleTools []string // 角色配置的工具列表
|
||||
if req.Role != "" && req.Role != "默认" {
|
||||
if h.config.Roles != nil {
|
||||
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
|
||||
// 应用用户提示词
|
||||
if role.UserPrompt != "" {
|
||||
finalMessage = role.UserPrompt + "\n\n" + req.Message
|
||||
h.logger.Info("应用角色用户提示词", zap.String("role", req.Role))
|
||||
}
|
||||
// 获取角色配置的工具列表(优先使用tools字段,向后兼容mcps字段)
|
||||
if len(role.Tools) > 0 {
|
||||
roleTools = role.Tools
|
||||
h.logger.Info("使用角色配置的工具列表", zap.String("role", req.Role), zap.Int("toolCount", len(roleTools)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户消息(保存原始消息,不包含角色提示词)
|
||||
_, err = h.db.AddMessage(conversationID, "user", req.Message, nil)
|
||||
if err != nil {
|
||||
h.logger.Error("保存用户消息失败", zap.Error(err))
|
||||
}
|
||||
|
||||
// 执行Agent Loop,传入历史消息和对话ID
|
||||
result, err := h.agent.AgentLoopWithConversationID(c.Request.Context(), req.Message, agentHistoryMessages, conversationID)
|
||||
// 执行Agent Loop,传入历史消息和对话ID(使用包含角色提示词的finalMessage和角色工具列表)
|
||||
result, err := h.agent.AgentLoopWithProgress(c.Request.Context(), finalMessage, agentHistoryMessages, conversationID, nil, roleTools)
|
||||
if err != nil {
|
||||
h.logger.Error("Agent Loop执行失败", zap.Error(err))
|
||||
|
||||
@@ -231,7 +256,7 @@ func (h *AgentHandler) createProgressCallback(conversationID, assistantMessageID
|
||||
if eventType == "tool_call" {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
toolName, _ := dataMap["toolName"].(string)
|
||||
if toolName == "search_knowledge_base" {
|
||||
if toolName == builtin.ToolSearchKnowledgeBase {
|
||||
if toolCallId, ok := dataMap["toolCallId"].(string); ok && toolCallId != "" {
|
||||
if argumentsObj, ok := dataMap["argumentsObj"].(map[string]interface{}); ok {
|
||||
toolCallCache[toolCallId] = argumentsObj
|
||||
@@ -245,7 +270,7 @@ func (h *AgentHandler) createProgressCallback(conversationID, assistantMessageID
|
||||
if eventType == "tool_result" && h.knowledgeManager != nil {
|
||||
if dataMap, ok := data.(map[string]interface{}); ok {
|
||||
toolName, _ := dataMap["toolName"].(string)
|
||||
if toolName == "search_knowledge_base" {
|
||||
if toolName == builtin.ToolSearchKnowledgeBase {
|
||||
// 提取检索信息
|
||||
query := ""
|
||||
riskType := ""
|
||||
@@ -470,7 +495,32 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
h.logger.Info("从ReAct数据恢复历史上下文", zap.Int("count", len(agentHistoryMessages)))
|
||||
}
|
||||
|
||||
// 保存用户消息
|
||||
// 应用角色用户提示词和工具配置
|
||||
finalMessage := req.Message
|
||||
var roleTools []string // 角色配置的工具列表
|
||||
if req.Role != "" && req.Role != "默认" {
|
||||
if h.config.Roles != nil {
|
||||
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
|
||||
// 应用用户提示词
|
||||
if role.UserPrompt != "" {
|
||||
finalMessage = role.UserPrompt + "\n\n" + req.Message
|
||||
h.logger.Info("应用角色用户提示词", zap.String("role", req.Role))
|
||||
}
|
||||
// 获取角色配置的工具列表(优先使用tools字段,向后兼容mcps字段)
|
||||
if len(role.Tools) > 0 {
|
||||
roleTools = role.Tools
|
||||
h.logger.Info("使用角色配置的工具列表", zap.String("role", req.Role), zap.Int("toolCount", len(roleTools)))
|
||||
} else if len(role.MCPs) > 0 {
|
||||
// 向后兼容:如果只有mcps字段,暂时使用空列表(表示使用所有工具)
|
||||
// 因为mcps是MCP服务器名称,不是工具列表
|
||||
h.logger.Info("角色配置使用旧的mcps字段,将使用所有工具", zap.String("role", req.Role))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 如果roleTools为空,表示使用所有工具(默认角色或未配置工具的角色)
|
||||
|
||||
// 保存用户消息(保存原始消息,不包含角色提示词)
|
||||
_, err = h.db.AddMessage(conversationID, "user", req.Message, nil)
|
||||
if err != nil {
|
||||
h.logger.Error("保存用户消息失败", zap.Error(err))
|
||||
@@ -547,9 +597,9 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
taskStatus := "completed"
|
||||
defer h.tasks.FinishTask(conversationID, taskStatus)
|
||||
|
||||
// 执行Agent Loop,传入独立的上下文,确保任务不会因客户端断开而中断
|
||||
// 执行Agent Loop,传入独立的上下文,确保任务不会因客户端断开而中断(使用包含角色提示词的finalMessage和角色工具列表)
|
||||
sendEvent("progress", "正在分析您的请求...", nil)
|
||||
result, err := h.agent.AgentLoopWithProgress(taskCtx, req.Message, agentHistoryMessages, conversationID, progressCallback)
|
||||
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools)
|
||||
if err != nil {
|
||||
h.logger.Error("Agent Loop执行失败", zap.Error(err))
|
||||
cause := context.Cause(baseCtx)
|
||||
@@ -759,8 +809,9 @@ func (h *AgentHandler) ListCompletedTasks(c *gin.Context) {
|
||||
|
||||
// BatchTaskRequest 批量任务请求
|
||||
type BatchTaskRequest struct {
|
||||
Title string `json:"title"` // 任务标题(可选)
|
||||
Title string `json:"title"` // 任务标题(可选)
|
||||
Tasks []string `json:"tasks" binding:"required"` // 任务列表,每行一个任务
|
||||
Role string `json:"role,omitempty"` // 角色名称(可选,空字符串表示默认角色)
|
||||
}
|
||||
|
||||
// CreateBatchQueue 创建批量任务队列
|
||||
@@ -789,7 +840,7 @@ func (h *AgentHandler) CreateBatchQueue(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
queue := h.batchTaskManager.CreateBatchQueue(req.Title, validTasks)
|
||||
queue := h.batchTaskManager.CreateBatchQueue(req.Title, req.Role, validTasks)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"queueId": queue.ID,
|
||||
"queue": queue,
|
||||
@@ -1045,7 +1096,27 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
// 保存conversationId到任务中(即使是运行中状态也要保存,以便查看对话)
|
||||
h.batchTaskManager.UpdateTaskStatusWithConversationID(queueID, task.ID, "running", "", "", conversationID)
|
||||
|
||||
// 保存用户消息
|
||||
// 应用角色用户提示词和工具配置
|
||||
finalMessage := task.Message
|
||||
var roleTools []string // 角色配置的工具列表
|
||||
if queue.Role != "" && queue.Role != "默认" {
|
||||
if h.config.Roles != nil {
|
||||
if role, exists := h.config.Roles[queue.Role]; exists && role.Enabled {
|
||||
// 应用用户提示词
|
||||
if role.UserPrompt != "" {
|
||||
finalMessage = role.UserPrompt + "\n\n" + task.Message
|
||||
h.logger.Info("应用角色用户提示词", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role))
|
||||
}
|
||||
// 获取角色配置的工具列表(优先使用tools字段,向后兼容mcps字段)
|
||||
if len(role.Tools) > 0 {
|
||||
roleTools = role.Tools
|
||||
h.logger.Info("使用角色配置的工具列表", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("role", queue.Role), zap.Int("toolCount", len(roleTools)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户消息(保存原始消息,不包含角色提示词)
|
||||
_, err = h.db.AddMessage(conversationID, "user", task.Message, nil)
|
||||
if err != nil {
|
||||
h.logger.Error("保存用户消息失败", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("conversationId", conversationID), zap.Error(err))
|
||||
@@ -1066,13 +1137,14 @@ func (h *AgentHandler) executeBatchQueue(queueID string) {
|
||||
}
|
||||
progressCallback := h.createProgressCallback(conversationID, assistantMessageID, nil)
|
||||
|
||||
// 执行任务
|
||||
h.logger.Info("执行批量任务", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("message", task.Message), zap.String("conversationId", conversationID))
|
||||
// 执行任务(使用包含角色提示词的finalMessage和角色工具列表)
|
||||
h.logger.Info("执行批量任务", zap.String("queueId", queueID), zap.String("taskId", task.ID), zap.String("message", task.Message), zap.String("role", queue.Role), zap.String("conversationId", conversationID))
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
// 存储取消函数,以便在取消队列时能够取消当前任务
|
||||
h.batchTaskManager.SetTaskCancel(queueID, cancel)
|
||||
result, err := h.agent.AgentLoopWithProgress(ctx, task.Message, []agent.ChatMessage{}, conversationID, progressCallback)
|
||||
// 使用队列配置的角色工具列表(如果为空,表示使用所有工具)
|
||||
result, err := h.agent.AgentLoopWithProgress(ctx, finalMessage, []agent.ChatMessage{}, conversationID, progressCallback, roleTools)
|
||||
// 任务执行完成,清理取消函数
|
||||
h.batchTaskManager.SetTaskCancel(queueID, nil)
|
||||
cancel()
|
||||
|
||||
@@ -29,6 +29,7 @@ type BatchTask struct {
|
||||
type BatchTaskQueue struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Role string `json:"role,omitempty"` // 角色名称(空字符串表示默认角色)
|
||||
Tasks []*BatchTask `json:"tasks"`
|
||||
Status string `json:"status"` // pending, running, paused, completed, cancelled
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
@@ -62,7 +63,7 @@ func (m *BatchTaskManager) SetDB(db *database.DB) {
|
||||
}
|
||||
|
||||
// CreateBatchQueue 创建批量任务队列
|
||||
func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *BatchTaskQueue {
|
||||
func (m *BatchTaskManager) CreateBatchQueue(title, role string, tasks []string) *BatchTaskQueue {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -70,6 +71,7 @@ func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *Batch
|
||||
queue := &BatchTaskQueue{
|
||||
ID: queueID,
|
||||
Title: title,
|
||||
Role: role,
|
||||
Tasks: make([]*BatchTask, 0, len(tasks)),
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
@@ -98,7 +100,7 @@ func (m *BatchTaskManager) CreateBatchQueue(title string, tasks []string) *Batch
|
||||
|
||||
// 保存到数据库
|
||||
if m.db != nil {
|
||||
if err := m.db.CreateBatchQueue(queueID, title, dbTasks); err != nil {
|
||||
if err := m.db.CreateBatchQueue(queueID, title, role, dbTasks); err != nil {
|
||||
// 如果数据库保存失败,记录错误但继续(使用内存缓存)
|
||||
// 这里可以添加日志记录
|
||||
}
|
||||
@@ -158,6 +160,9 @@ func (m *BatchTaskManager) loadQueueFromDB(queueID string) *BatchTaskQueue {
|
||||
if queueRow.Title.Valid {
|
||||
queue.Title = queueRow.Title.String
|
||||
}
|
||||
if queueRow.Role.Valid {
|
||||
queue.Role = queueRow.Role.String
|
||||
}
|
||||
if queueRow.StartedAt.Valid {
|
||||
queue.StartedAt = &queueRow.StartedAt.Time
|
||||
}
|
||||
@@ -351,6 +356,9 @@ func (m *BatchTaskManager) LoadFromDB() error {
|
||||
if queueRow.Title.Valid {
|
||||
queue.Title = queueRow.Title.String
|
||||
}
|
||||
if queueRow.Role.Valid {
|
||||
queue.Role = queueRow.Role.String
|
||||
}
|
||||
if queueRow.StartedAt.Valid {
|
||||
queue.StartedAt = &queueRow.StartedAt.Time
|
||||
}
|
||||
|
||||
+142
-40
@@ -47,17 +47,17 @@ type ConfigHandler struct {
|
||||
config *config.Config
|
||||
mcpServer *mcp.Server
|
||||
executor *security.Executor
|
||||
agent AgentUpdater // Agent接口,用于更新Agent配置
|
||||
attackChainHandler AttackChainUpdater // 攻击链处理器接口,用于更新配置
|
||||
agent AgentUpdater // Agent接口,用于更新Agent配置
|
||||
attackChainHandler AttackChainUpdater // 攻击链处理器接口,用于更新配置
|
||||
externalMCPMgr *mcp.ExternalMCPManager // 外部MCP管理器
|
||||
knowledgeToolRegistrar KnowledgeToolRegistrar // 知识库工具注册器(可选)
|
||||
knowledgeToolRegistrar KnowledgeToolRegistrar // 知识库工具注册器(可选)
|
||||
vulnerabilityToolRegistrar VulnerabilityToolRegistrar // 漏洞工具注册器(可选)
|
||||
retrieverUpdater RetrieverUpdater // 检索器更新器(可选)
|
||||
knowledgeInitializer KnowledgeInitializer // 知识库初始化器(可选)
|
||||
appUpdater AppUpdater // App更新器(可选)
|
||||
retrieverUpdater RetrieverUpdater // 检索器更新器(可选)
|
||||
knowledgeInitializer KnowledgeInitializer // 知识库初始化器(可选)
|
||||
appUpdater AppUpdater // App更新器(可选)
|
||||
logger *zap.Logger
|
||||
mu sync.RWMutex
|
||||
lastEmbeddingConfig *config.EmbeddingConfig // 上一次的嵌入模型配置(用于检测变更)
|
||||
lastEmbeddingConfig *config.EmbeddingConfig // 上一次的嵌入模型配置(用于检测变更)
|
||||
}
|
||||
|
||||
// AttackChainUpdater 攻击链处理器更新接口
|
||||
@@ -147,6 +147,7 @@ type ToolConfigInfo struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
IsExternal bool `json:"is_external,omitempty"` // 是否为外部MCP工具
|
||||
ExternalMCP string `json:"external_mcp,omitempty"` // 外部MCP名称(如果是外部工具)
|
||||
RoleEnabled *bool `json:"role_enabled,omitempty"` // 该工具在当前角色中是否启用(nil表示未指定角色或使用所有工具)
|
||||
}
|
||||
|
||||
// GetConfig 获取当前配置
|
||||
@@ -272,11 +273,12 @@ func (h *ConfigHandler) GetConfig(c *gin.Context) {
|
||||
|
||||
// GetToolsResponse 获取工具列表响应(分页)
|
||||
type GetToolsResponse struct {
|
||||
Tools []ToolConfigInfo `json:"tools"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
Tools []ToolConfigInfo `json:"tools"`
|
||||
Total int `json:"total"`
|
||||
TotalEnabled int `json:"total_enabled"` // 已启用的工具总数
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
}
|
||||
|
||||
// GetTools 获取工具列表(支持分页和搜索)
|
||||
@@ -305,6 +307,23 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
||||
searchTermLower = strings.ToLower(searchTerm)
|
||||
}
|
||||
|
||||
// 解析角色参数,用于过滤工具并标注启用状态
|
||||
roleName := c.Query("role")
|
||||
var roleToolsSet map[string]bool // 角色配置的工具集合
|
||||
var roleUsesAllTools bool = true // 角色是否使用所有工具(默认角色)
|
||||
if roleName != "" && roleName != "默认" && h.config.Roles != nil {
|
||||
if role, exists := h.config.Roles[roleName]; exists && role.Enabled {
|
||||
if len(role.Tools) > 0 {
|
||||
// 角色配置了工具列表,只使用这些工具
|
||||
roleToolsSet = make(map[string]bool)
|
||||
for _, toolKey := range role.Tools {
|
||||
roleToolsSet[toolKey] = true
|
||||
}
|
||||
roleUsesAllTools = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取所有内部工具并应用搜索过滤
|
||||
configToolMap := make(map[string]bool)
|
||||
allTools := make([]ToolConfigInfo, 0, len(h.config.Security.Tools))
|
||||
@@ -325,6 +344,31 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
||||
toolInfo.Description = desc
|
||||
}
|
||||
|
||||
// 根据角色配置标注工具状态
|
||||
if roleName != "" {
|
||||
if roleUsesAllTools {
|
||||
// 角色使用所有工具,标注启用的工具为role_enabled=true
|
||||
if tool.Enabled {
|
||||
roleEnabled := true
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
} else {
|
||||
roleEnabled := false
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
}
|
||||
} else {
|
||||
// 角色配置了工具列表,检查工具是否在列表中
|
||||
// 内部工具使用工具名称作为key
|
||||
if roleToolsSet[tool.Name] {
|
||||
roleEnabled := tool.Enabled // 工具必须在角色列表中且本身启用
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
} else {
|
||||
// 不在角色列表中,标记为false
|
||||
roleEnabled := false
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有关键词,进行搜索过滤
|
||||
if searchTermLower != "" {
|
||||
nameLower := strings.ToLower(toolInfo.Name)
|
||||
@@ -361,6 +405,26 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
||||
IsExternal: false,
|
||||
}
|
||||
|
||||
// 根据角色配置标注工具状态
|
||||
if roleName != "" {
|
||||
if roleUsesAllTools {
|
||||
// 角色使用所有工具,直接注册的工具默认启用
|
||||
roleEnabled := true
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
} else {
|
||||
// 角色配置了工具列表,检查工具是否在列表中
|
||||
// 内部工具使用工具名称作为key
|
||||
if roleToolsSet[mcpTool.Name] {
|
||||
roleEnabled := true // 在角色列表中且工具本身启用
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
} else {
|
||||
// 不在角色列表中,标记为false
|
||||
roleEnabled := false
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果有关键词,进行搜索过滤
|
||||
if searchTermLower != "" {
|
||||
nameLower := strings.ToLower(toolInfo.Name)
|
||||
@@ -439,18 +503,55 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
allTools = append(allTools, ToolConfigInfo{
|
||||
toolInfo := ToolConfigInfo{
|
||||
Name: actualToolName, // 显示实际工具名称,不带前缀
|
||||
Description: description,
|
||||
Enabled: enabled,
|
||||
IsExternal: true,
|
||||
ExternalMCP: mcpName,
|
||||
})
|
||||
}
|
||||
|
||||
// 根据角色配置标注工具状态
|
||||
if roleName != "" {
|
||||
if roleUsesAllTools {
|
||||
// 角色使用所有工具,标注启用的工具为role_enabled=true
|
||||
toolInfo.RoleEnabled = &enabled
|
||||
} else {
|
||||
// 角色配置了工具列表,检查工具是否在列表中
|
||||
// 外部工具使用 "mcpName::toolName" 格式作为key
|
||||
externalToolKey := externalTool.Name // 这是 "mcpName::toolName" 格式
|
||||
if roleToolsSet[externalToolKey] {
|
||||
roleEnabled := enabled // 工具必须在角色列表中且本身启用
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
} else {
|
||||
// 不在角色列表中,标记为false
|
||||
roleEnabled := false
|
||||
toolInfo.RoleEnabled = &roleEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
allTools = append(allTools, toolInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果角色配置了工具列表,过滤工具(只保留列表中的工具,但保留其他工具并标记为禁用)
|
||||
// 注意:这里我们不直接过滤掉工具,而是保留所有工具,但通过 role_enabled 字段标注状态
|
||||
// 这样前端可以显示所有工具,并标注哪些工具在当前角色中可用
|
||||
|
||||
total := len(allTools)
|
||||
// 统计已启用的工具数(在角色中的启用工具数)
|
||||
totalEnabled := 0
|
||||
for _, tool := range allTools {
|
||||
if tool.RoleEnabled != nil && *tool.RoleEnabled {
|
||||
totalEnabled++
|
||||
} else if tool.RoleEnabled == nil && tool.Enabled {
|
||||
// 如果未指定角色,统计所有启用的工具
|
||||
totalEnabled++
|
||||
}
|
||||
}
|
||||
|
||||
totalPages := (total + pageSize - 1) / pageSize
|
||||
if totalPages == 0 {
|
||||
totalPages = 1
|
||||
@@ -471,11 +572,12 @@ func (h *ConfigHandler) GetTools(c *gin.Context) {
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, GetToolsResponse{
|
||||
Tools: tools,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
Tools: tools,
|
||||
Total: total,
|
||||
TotalEnabled: totalEnabled,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
TotalPages: totalPages,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -790,30 +892,30 @@ func (h *ConfigHandler) ApplyConfig(c *gin.Context) {
|
||||
h.logger.Info("AttackChainHandler配置已更新")
|
||||
}
|
||||
|
||||
// 更新检索器配置(如果知识库启用)
|
||||
if h.config.Knowledge.Enabled && h.retrieverUpdater != nil {
|
||||
retrievalConfig := &knowledge.RetrievalConfig{
|
||||
TopK: h.config.Knowledge.Retrieval.TopK,
|
||||
SimilarityThreshold: h.config.Knowledge.Retrieval.SimilarityThreshold,
|
||||
HybridWeight: h.config.Knowledge.Retrieval.HybridWeight,
|
||||
}
|
||||
h.retrieverUpdater.UpdateConfig(retrievalConfig)
|
||||
h.logger.Info("检索器配置已更新",
|
||||
zap.Int("top_k", retrievalConfig.TopK),
|
||||
zap.Float64("similarity_threshold", retrievalConfig.SimilarityThreshold),
|
||||
zap.Float64("hybrid_weight", retrievalConfig.HybridWeight),
|
||||
)
|
||||
// 更新检索器配置(如果知识库启用)
|
||||
if h.config.Knowledge.Enabled && h.retrieverUpdater != nil {
|
||||
retrievalConfig := &knowledge.RetrievalConfig{
|
||||
TopK: h.config.Knowledge.Retrieval.TopK,
|
||||
SimilarityThreshold: h.config.Knowledge.Retrieval.SimilarityThreshold,
|
||||
HybridWeight: h.config.Knowledge.Retrieval.HybridWeight,
|
||||
}
|
||||
h.retrieverUpdater.UpdateConfig(retrievalConfig)
|
||||
h.logger.Info("检索器配置已更新",
|
||||
zap.Int("top_k", retrievalConfig.TopK),
|
||||
zap.Float64("similarity_threshold", retrievalConfig.SimilarityThreshold),
|
||||
zap.Float64("hybrid_weight", retrievalConfig.HybridWeight),
|
||||
)
|
||||
}
|
||||
|
||||
// 更新嵌入模型配置记录(如果知识库启用)
|
||||
if h.config.Knowledge.Enabled {
|
||||
h.lastEmbeddingConfig = &config.EmbeddingConfig{
|
||||
Provider: h.config.Knowledge.Embedding.Provider,
|
||||
Model: h.config.Knowledge.Embedding.Model,
|
||||
BaseURL: h.config.Knowledge.Embedding.BaseURL,
|
||||
APIKey: h.config.Knowledge.Embedding.APIKey,
|
||||
}
|
||||
// 更新嵌入模型配置记录(如果知识库启用)
|
||||
if h.config.Knowledge.Enabled {
|
||||
h.lastEmbeddingConfig = &config.EmbeddingConfig{
|
||||
Provider: h.config.Knowledge.Embedding.Provider,
|
||||
Model: h.config.Knowledge.Embedding.Model,
|
||||
BaseURL: h.config.Knowledge.Embedding.BaseURL,
|
||||
APIKey: h.config.Knowledge.Embedding.APIKey,
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("配置已应用",
|
||||
zap.Int("tools_count", len(h.config.Security.Tools)),
|
||||
|
||||
@@ -446,6 +446,13 @@ func updateExternalMCPConfig(doc *yaml.Node, cfg config.ExternalMCPConfig, origi
|
||||
if len(serverCfg.Args) > 0 {
|
||||
setStringArrayInMap(serverNode, "args", serverCfg.Args)
|
||||
}
|
||||
// 保存 env 字段(环境变量)
|
||||
if serverCfg.Env != nil && len(serverCfg.Env) > 0 {
|
||||
envNode := ensureMap(serverNode, "env")
|
||||
for envKey, envValue := range serverCfg.Env {
|
||||
setStringInMap(envNode, envKey, envValue)
|
||||
}
|
||||
}
|
||||
if serverCfg.Transport != "" {
|
||||
setStringInMap(serverNode, "transport", serverCfg.Transport)
|
||||
}
|
||||
|
||||
@@ -189,8 +189,18 @@ type GroupConversation struct {
|
||||
// GetGroupConversations 获取分组中的所有对话
|
||||
func (h *GroupHandler) GetGroupConversations(c *gin.Context) {
|
||||
groupID := c.Param("id")
|
||||
searchQuery := c.Query("search") // 获取搜索参数
|
||||
|
||||
var conversations []*database.Conversation
|
||||
var err error
|
||||
|
||||
// 如果有搜索关键词,使用搜索方法;否则使用普通方法
|
||||
if searchQuery != "" {
|
||||
conversations, err = h.db.SearchConversationsByGroup(groupID, searchQuery)
|
||||
} else {
|
||||
conversations, err = h.db.GetConversationsByGroup(groupID)
|
||||
}
|
||||
|
||||
conversations, err := h.db.GetConversationsByGroup(groupID)
|
||||
if err != nil {
|
||||
h.logger.Error("获取分组对话失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// RoleHandler 角色处理器
|
||||
type RoleHandler struct {
|
||||
config *config.Config
|
||||
configPath string
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewRoleHandler 创建新的角色处理器
|
||||
func NewRoleHandler(cfg *config.Config, configPath string, logger *zap.Logger) *RoleHandler {
|
||||
return &RoleHandler{
|
||||
config: cfg,
|
||||
configPath: configPath,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRoles 获取所有角色
|
||||
func (h *RoleHandler) GetRoles(c *gin.Context) {
|
||||
if h.config.Roles == nil {
|
||||
h.config.Roles = make(map[string]config.RoleConfig)
|
||||
}
|
||||
|
||||
roles := make([]config.RoleConfig, 0, len(h.config.Roles))
|
||||
for key, role := range h.config.Roles {
|
||||
// 确保角色的key与name一致
|
||||
if role.Name == "" {
|
||||
role.Name = key
|
||||
}
|
||||
roles = append(roles, role)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"roles": roles,
|
||||
})
|
||||
}
|
||||
|
||||
// GetRole 获取单个角色
|
||||
func (h *RoleHandler) GetRole(c *gin.Context) {
|
||||
roleName := c.Param("name")
|
||||
if roleName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "角色名称不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
if h.config.Roles == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "角色不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
role, exists := h.config.Roles[roleName]
|
||||
if !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "角色不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 确保角色的name与key一致
|
||||
if role.Name == "" {
|
||||
role.Name = roleName
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"role": role,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRole 更新角色
|
||||
func (h *RoleHandler) UpdateRole(c *gin.Context) {
|
||||
roleName := c.Param("name")
|
||||
if roleName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "角色名称不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var req config.RoleConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 确保角色名称与请求中的name一致
|
||||
if req.Name == "" {
|
||||
req.Name = roleName
|
||||
}
|
||||
|
||||
// 初始化Roles map
|
||||
if h.config.Roles == nil {
|
||||
h.config.Roles = make(map[string]config.RoleConfig)
|
||||
}
|
||||
|
||||
// 删除所有与角色name相同但key不同的旧角色(避免重复)
|
||||
// 使用角色name作为key,确保唯一性
|
||||
finalKey := req.Name
|
||||
keysToDelete := make([]string, 0)
|
||||
for key := range h.config.Roles {
|
||||
// 如果key与最终的key不同,但name相同,则标记为删除
|
||||
if key != finalKey {
|
||||
role := h.config.Roles[key]
|
||||
// 确保角色的name字段正确设置
|
||||
if role.Name == "" {
|
||||
role.Name = key
|
||||
}
|
||||
if role.Name == req.Name {
|
||||
keysToDelete = append(keysToDelete, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 删除旧的角色
|
||||
for _, key := range keysToDelete {
|
||||
delete(h.config.Roles, key)
|
||||
h.logger.Info("删除重复的角色", zap.String("oldKey", key), zap.String("name", req.Name))
|
||||
}
|
||||
|
||||
// 如果当前更新的key与最终key不同,也需要删除旧的
|
||||
if roleName != finalKey {
|
||||
delete(h.config.Roles, roleName)
|
||||
}
|
||||
|
||||
// 如果角色名称改变,需要删除旧文件
|
||||
if roleName != finalKey {
|
||||
configDir := filepath.Dir(h.configPath)
|
||||
rolesDir := h.config.RolesDir
|
||||
if rolesDir == "" {
|
||||
rolesDir = "roles" // 默认目录
|
||||
}
|
||||
|
||||
// 如果是相对路径,相对于配置文件所在目录
|
||||
if !filepath.IsAbs(rolesDir) {
|
||||
rolesDir = filepath.Join(configDir, rolesDir)
|
||||
}
|
||||
|
||||
// 删除旧的角色文件
|
||||
oldSafeFileName := sanitizeFileName(roleName)
|
||||
oldRoleFileYaml := filepath.Join(rolesDir, oldSafeFileName+".yaml")
|
||||
oldRoleFileYml := filepath.Join(rolesDir, oldSafeFileName+".yml")
|
||||
|
||||
if _, err := os.Stat(oldRoleFileYaml); err == nil {
|
||||
if err := os.Remove(oldRoleFileYaml); err != nil {
|
||||
h.logger.Warn("删除旧角色配置文件失败", zap.String("file", oldRoleFileYaml), zap.Error(err))
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(oldRoleFileYml); err == nil {
|
||||
if err := os.Remove(oldRoleFileYml); err != nil {
|
||||
h.logger.Warn("删除旧角色配置文件失败", zap.String("file", oldRoleFileYml), zap.Error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 使用角色name作为key来保存(确保唯一性)
|
||||
h.config.Roles[finalKey] = req
|
||||
|
||||
// 保存配置到文件
|
||||
if err := h.saveConfig(); err != nil {
|
||||
h.logger.Error("保存配置失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("更新角色", zap.String("oldKey", roleName), zap.String("newKey", finalKey), zap.String("name", req.Name))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "角色已更新",
|
||||
"role": req,
|
||||
})
|
||||
}
|
||||
|
||||
// CreateRole 创建新角色
|
||||
func (h *RoleHandler) CreateRole(c *gin.Context) {
|
||||
var req config.RoleConfig
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的请求参数: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "角色名称不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 初始化Roles map
|
||||
if h.config.Roles == nil {
|
||||
h.config.Roles = make(map[string]config.RoleConfig)
|
||||
}
|
||||
|
||||
// 检查角色是否已存在
|
||||
if _, exists := h.config.Roles[req.Name]; exists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "角色已存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 创建角色(默认启用)
|
||||
if !req.Enabled {
|
||||
req.Enabled = true
|
||||
}
|
||||
|
||||
h.config.Roles[req.Name] = req
|
||||
|
||||
// 保存配置到文件
|
||||
if err := h.saveConfig(); err != nil {
|
||||
h.logger.Error("保存配置失败", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Info("创建角色", zap.String("roleName", req.Name))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "角色已创建",
|
||||
"role": req,
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteRole 删除角色
|
||||
func (h *RoleHandler) DeleteRole(c *gin.Context) {
|
||||
roleName := c.Param("name")
|
||||
if roleName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "角色名称不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
if h.config.Roles == nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "角色不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
if _, exists := h.config.Roles[roleName]; !exists {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "角色不存在"})
|
||||
return
|
||||
}
|
||||
|
||||
// 不允许删除"默认"角色
|
||||
if roleName == "默认" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "不能删除默认角色"})
|
||||
return
|
||||
}
|
||||
|
||||
delete(h.config.Roles, roleName)
|
||||
|
||||
// 删除对应的角色文件
|
||||
configDir := filepath.Dir(h.configPath)
|
||||
rolesDir := h.config.RolesDir
|
||||
if rolesDir == "" {
|
||||
rolesDir = "roles" // 默认目录
|
||||
}
|
||||
|
||||
// 如果是相对路径,相对于配置文件所在目录
|
||||
if !filepath.IsAbs(rolesDir) {
|
||||
rolesDir = filepath.Join(configDir, rolesDir)
|
||||
}
|
||||
|
||||
// 尝试删除角色文件(.yaml 和 .yml)
|
||||
safeFileName := sanitizeFileName(roleName)
|
||||
roleFileYaml := filepath.Join(rolesDir, safeFileName+".yaml")
|
||||
roleFileYml := filepath.Join(rolesDir, safeFileName+".yml")
|
||||
|
||||
// 删除 .yaml 文件(如果存在)
|
||||
if _, err := os.Stat(roleFileYaml); err == nil {
|
||||
if err := os.Remove(roleFileYaml); err != nil {
|
||||
h.logger.Warn("删除角色配置文件失败", zap.String("file", roleFileYaml), zap.Error(err))
|
||||
} else {
|
||||
h.logger.Info("已删除角色配置文件", zap.String("file", roleFileYaml))
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 .yml 文件(如果存在)
|
||||
if _, err := os.Stat(roleFileYml); err == nil {
|
||||
if err := os.Remove(roleFileYml); err != nil {
|
||||
h.logger.Warn("删除角色配置文件失败", zap.String("file", roleFileYml), zap.Error(err))
|
||||
} else {
|
||||
h.logger.Info("已删除角色配置文件", zap.String("file", roleFileYml))
|
||||
}
|
||||
}
|
||||
|
||||
h.logger.Info("删除角色", zap.String("roleName", roleName))
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "角色已删除",
|
||||
})
|
||||
}
|
||||
|
||||
// saveConfig 保存配置到目录中的文件
|
||||
func (h *RoleHandler) saveConfig() error {
|
||||
configDir := filepath.Dir(h.configPath)
|
||||
rolesDir := h.config.RolesDir
|
||||
if rolesDir == "" {
|
||||
rolesDir = "roles" // 默认目录
|
||||
}
|
||||
|
||||
// 如果是相对路径,相对于配置文件所在目录
|
||||
if !filepath.IsAbs(rolesDir) {
|
||||
rolesDir = filepath.Join(configDir, rolesDir)
|
||||
}
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(rolesDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建角色目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 保存每个角色到独立的文件
|
||||
if h.config.Roles != nil {
|
||||
for roleName, role := range h.config.Roles {
|
||||
// 确保角色名称正确设置
|
||||
if role.Name == "" {
|
||||
role.Name = roleName
|
||||
}
|
||||
|
||||
// 使用角色名称作为文件名(安全化文件名,避免特殊字符)
|
||||
safeFileName := sanitizeFileName(role.Name)
|
||||
roleFile := filepath.Join(rolesDir, safeFileName+".yaml")
|
||||
|
||||
// 将角色配置序列化为YAML
|
||||
roleData, err := yaml.Marshal(&role)
|
||||
if err != nil {
|
||||
h.logger.Error("序列化角色配置失败", zap.String("role", roleName), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 处理icon字段:确保包含\U的icon值被引号包围(YAML需要引号才能正确解析Unicode转义)
|
||||
roleDataStr := string(roleData)
|
||||
if role.Icon != "" && strings.HasPrefix(role.Icon, "\\U") {
|
||||
// 匹配 icon: \UXXXXXXXX 格式(没有引号),排除已经有引号的情况
|
||||
// 使用负向前瞻确保后面没有引号,或者直接匹配没有引号的情况
|
||||
re := regexp.MustCompile(`(?m)^(icon:\s+)(\\U[0-9A-F]{8})(\s*)$`)
|
||||
roleDataStr = re.ReplaceAllString(roleDataStr, `${1}"${2}"${3}`)
|
||||
roleData = []byte(roleDataStr)
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if err := os.WriteFile(roleFile, roleData, 0644); err != nil {
|
||||
h.logger.Error("保存角色配置文件失败", zap.String("role", roleName), zap.String("file", roleFile), zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
h.logger.Info("角色配置已保存到文件", zap.String("role", roleName), zap.String("file", roleFile))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// sanitizeFileName 将角色名称转换为安全的文件名
|
||||
func sanitizeFileName(name string) string {
|
||||
// 替换可能不安全的字符
|
||||
replacer := map[rune]string{
|
||||
'/': "_",
|
||||
'\\': "_",
|
||||
':': "_",
|
||||
'*': "_",
|
||||
'?': "_",
|
||||
'"': "_",
|
||||
'<': "_",
|
||||
'>': "_",
|
||||
'|': "_",
|
||||
' ': "_",
|
||||
}
|
||||
|
||||
var result []rune
|
||||
for _, r := range name {
|
||||
if replacement, ok := replacer[r]; ok {
|
||||
result = append(result, []rune(replacement)...)
|
||||
} else {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
|
||||
fileName := string(result)
|
||||
// 如果文件名为空,使用默认名称
|
||||
if fileName == "" {
|
||||
fileName = "role"
|
||||
}
|
||||
|
||||
return fileName
|
||||
}
|
||||
|
||||
// updateRolesConfig 更新角色配置
|
||||
func updateRolesConfig(doc *yaml.Node, cfg config.RolesConfig) {
|
||||
root := doc.Content[0]
|
||||
rolesNode := ensureMap(root, "roles")
|
||||
|
||||
// 清空现有角色
|
||||
if rolesNode.Kind == yaml.MappingNode {
|
||||
rolesNode.Content = nil
|
||||
}
|
||||
|
||||
// 添加新角色(使用name作为key,确保唯一性)
|
||||
if cfg.Roles != nil {
|
||||
// 先建立一个以name为key的map,去重(保留最后一个)
|
||||
rolesByName := make(map[string]config.RoleConfig)
|
||||
for roleKey, role := range cfg.Roles {
|
||||
// 确保角色的name字段正确设置
|
||||
if role.Name == "" {
|
||||
role.Name = roleKey
|
||||
}
|
||||
// 使用name作为最终key,如果有多个key对应相同的name,只保留最后一个
|
||||
rolesByName[role.Name] = role
|
||||
}
|
||||
|
||||
// 将去重后的角色写入YAML
|
||||
for roleName, role := range rolesByName {
|
||||
roleNode := ensureMap(rolesNode, roleName)
|
||||
setStringInMap(roleNode, "name", role.Name)
|
||||
setStringInMap(roleNode, "description", role.Description)
|
||||
setStringInMap(roleNode, "user_prompt", role.UserPrompt)
|
||||
if role.Icon != "" {
|
||||
setStringInMap(roleNode, "icon", role.Icon)
|
||||
}
|
||||
setBoolInMap(roleNode, "enabled", role.Enabled)
|
||||
|
||||
// 添加工具列表(优先使用tools字段)
|
||||
if len(role.Tools) > 0 {
|
||||
toolsNode := ensureArray(roleNode, "tools")
|
||||
toolsNode.Content = nil
|
||||
for _, toolKey := range role.Tools {
|
||||
toolNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: toolKey}
|
||||
toolsNode.Content = append(toolsNode.Content, toolNode)
|
||||
}
|
||||
} else if len(role.MCPs) > 0 {
|
||||
// 向后兼容:如果没有tools但有mcps,保存mcps
|
||||
mcpsNode := ensureArray(roleNode, "mcps")
|
||||
mcpsNode.Content = nil
|
||||
for _, mcpName := range role.MCPs {
|
||||
mcpNode := &yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: mcpName}
|
||||
mcpsNode.Content = append(mcpsNode.Content, mcpNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensureArray 确保数组中存在指定key的数组节点
|
||||
func ensureArray(parent *yaml.Node, key string) *yaml.Node {
|
||||
_, valueNode := ensureKeyValue(parent, key)
|
||||
if valueNode.Kind != yaml.SequenceNode {
|
||||
valueNode.Kind = yaml.SequenceNode
|
||||
valueNode.Tag = "!!seq"
|
||||
valueNode.Content = nil
|
||||
}
|
||||
return valueNode
|
||||
}
|
||||
@@ -161,14 +161,14 @@ func (r *Retriever) Search(ctx context.Context, req *SearchRequest) ([]*Retrieva
|
||||
|
||||
// 查询所有向量(或按风险类型过滤)
|
||||
// 使用精确匹配(=)以提高性能和准确性
|
||||
// 由于系统提供了 list_knowledge_risk_types 工具,用户应该使用准确的category名称
|
||||
// 同时,向量嵌入中已包含category信息,即使SQL过滤不完全匹配,向量相似度也能帮助匹配
|
||||
var rows *sql.Rows
|
||||
if req.RiskType != "" {
|
||||
// 使用精确匹配(=),性能更好且更准确
|
||||
// 使用 COLLATE NOCASE 实现大小写不敏感匹配,提高容错性
|
||||
// 注意:如果用户输入的risk_type与category不完全一致,可能匹配不到
|
||||
// 建议用户先调用 list_knowledge_risk_types 获取准确的category名称
|
||||
// 由于系统提供了内置工具来获取风险类型列表,用户应该使用准确的category名称
|
||||
// 同时,向量嵌入中已包含category信息,即使SQL过滤不完全匹配,向量相似度也能帮助匹配
|
||||
var rows *sql.Rows
|
||||
if req.RiskType != "" {
|
||||
// 使用精确匹配(=),性能更好且更准确
|
||||
// 使用 COLLATE NOCASE 实现大小写不敏感匹配,提高容错性
|
||||
// 注意:如果用户输入的risk_type与category不完全一致,可能匹配不到
|
||||
// 建议用户先调用相应的内置工具获取准确的category名称
|
||||
rows, err = r.db.Query(`
|
||||
SELECT e.id, e.item_id, e.chunk_index, e.chunk_text, e.embedding, i.category, i.title
|
||||
FROM knowledge_embeddings e
|
||||
|
||||
+12
-11
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
"cyberstrike-ai/internal/mcp/builtin"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
@@ -21,7 +22,7 @@ func RegisterKnowledgeTool(
|
||||
) {
|
||||
// 注册第一个工具:获取所有可用的风险类型列表
|
||||
listRiskTypesTool := mcp.Tool{
|
||||
Name: "list_knowledge_risk_types",
|
||||
Name: builtin.ToolListKnowledgeRiskTypes,
|
||||
Description: "获取知识库中所有可用的风险类型(risk_type)列表。在搜索知识库之前,可以先调用此工具获取可用的风险类型,然后使用正确的风险类型进行精确搜索,这样可以大幅减少检索时间并提高检索准确性。",
|
||||
ShortDescription: "获取知识库中所有可用的风险类型列表",
|
||||
InputSchema: map[string]interface{}{
|
||||
@@ -62,7 +63,7 @@ func RegisterKnowledgeTool(
|
||||
for i, category := range categories {
|
||||
resultText.WriteString(fmt.Sprintf("%d. %s\n", i+1, category))
|
||||
}
|
||||
resultText.WriteString("\n提示:在调用 search_knowledge_base 工具时,可以使用上述风险类型之一作为 risk_type 参数,以缩小搜索范围并提高检索效率。")
|
||||
resultText.WriteString("\n提示:在调用 " + builtin.ToolSearchKnowledgeBase + " 工具时,可以使用上述风险类型之一作为 risk_type 参数,以缩小搜索范围并提高检索效率。")
|
||||
|
||||
return &mcp.ToolResult{
|
||||
Content: []mcp.Content{
|
||||
@@ -79,8 +80,8 @@ func RegisterKnowledgeTool(
|
||||
|
||||
// 注册第二个工具:搜索知识库(保持原有功能)
|
||||
searchTool := mcp.Tool{
|
||||
Name: "search_knowledge_base",
|
||||
Description: "在知识库中搜索相关的安全知识。当你需要了解特定漏洞类型、攻击技术、检测方法等安全知识时,可以使用此工具进行检索。工具使用向量检索和混合搜索技术,能够根据查询内容的语义相似度和关键词匹配,自动找到最相关的知识片段。建议:在搜索前可以先调用 list_knowledge_risk_types 工具获取可用的风险类型,然后使用正确的 risk_type 参数进行精确搜索,这样可以大幅减少检索时间。",
|
||||
Name: builtin.ToolSearchKnowledgeBase,
|
||||
Description: "在知识库中搜索相关的安全知识。当你需要了解特定漏洞类型、攻击技术、检测方法等安全知识时,可以使用此工具进行检索。工具使用向量检索和混合搜索技术,能够根据查询内容的语义相似度和关键词匹配,自动找到最相关的知识片段。建议:在搜索前可以先调用 " + builtin.ToolListKnowledgeRiskTypes + " 工具获取可用的风险类型,然后使用正确的 risk_type 参数进行精确搜索,这样可以大幅减少检索时间。",
|
||||
ShortDescription: "搜索知识库中的安全知识(支持向量检索和混合搜索)",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
@@ -91,7 +92,7 @@ func RegisterKnowledgeTool(
|
||||
},
|
||||
"risk_type": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "可选:指定风险类型(如:SQL注入、XSS、文件上传等)。建议先调用 list_knowledge_risk_types 工具获取可用的风险类型列表,然后使用正确的风险类型进行精确搜索,这样可以大幅减少检索时间。如果不指定则搜索所有类型。",
|
||||
"description": "可选:指定风险类型(如:SQL注入、XSS、文件上传等)。建议先调用 " + builtin.ToolListKnowledgeRiskTypes + " 工具获取可用的风险类型列表,然后使用正确的风险类型进行精确搜索,这样可以大幅减少检索时间。如果不指定则搜索所有类型。",
|
||||
},
|
||||
},
|
||||
"required": []string{"query"},
|
||||
@@ -165,9 +166,9 @@ func RegisterKnowledgeTool(
|
||||
// 按文档分组结果,以便更好地展示上下文
|
||||
// 使用有序的slice来保持文档顺序(按最高混合分数)
|
||||
type itemGroup struct {
|
||||
itemID string
|
||||
results []*RetrievalResult
|
||||
maxScore float64 // 该文档的最高混合分数
|
||||
itemID string
|
||||
results []*RetrievalResult
|
||||
maxScore float64 // 该文档的最高混合分数
|
||||
}
|
||||
itemGroups := make([]*itemGroup, 0)
|
||||
itemMap := make(map[string]*itemGroup)
|
||||
@@ -177,8 +178,8 @@ func RegisterKnowledgeTool(
|
||||
group, exists := itemMap[itemID]
|
||||
if !exists {
|
||||
group = &itemGroup{
|
||||
itemID: itemID,
|
||||
results: make([]*RetrievalResult, 0),
|
||||
itemID: itemID,
|
||||
results: make([]*RetrievalResult, 0),
|
||||
maxScore: result.Score,
|
||||
}
|
||||
itemMap[itemID] = group
|
||||
@@ -219,7 +220,7 @@ func RegisterKnowledgeTool(
|
||||
})
|
||||
|
||||
// 显示主结果(混合分数最高的,同时显示相似度和混合分数)
|
||||
resultText.WriteString(fmt.Sprintf("--- 结果 %d (相似度: %.2f%%, 混合分数: %.2f%%) ---\n",
|
||||
resultText.WriteString(fmt.Sprintf("--- 结果 %d (相似度: %.2f%%, 混合分数: %.2f%%) ---\n",
|
||||
resultIndex, mainResult.Similarity*100, mainResult.Score*100))
|
||||
resultText.WriteString(fmt.Sprintf("来源: [%s] %s (ID: %s)\n", mainResult.Item.Category, mainResult.Item.Title, mainResult.Item.ID))
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package builtin
|
||||
|
||||
// 内置工具名称常量
|
||||
// 所有代码中使用内置工具名称的地方都应该使用这些常量,而不是硬编码字符串
|
||||
const (
|
||||
// 漏洞管理工具
|
||||
ToolRecordVulnerability = "record_vulnerability"
|
||||
|
||||
// 知识库工具
|
||||
ToolListKnowledgeRiskTypes = "list_knowledge_risk_types"
|
||||
ToolSearchKnowledgeBase = "search_knowledge_base"
|
||||
)
|
||||
|
||||
// IsBuiltinTool 检查工具名称是否是内置工具
|
||||
func IsBuiltinTool(toolName string) bool {
|
||||
switch toolName {
|
||||
case ToolRecordVulnerability,
|
||||
ToolListKnowledgeRiskTypes,
|
||||
ToolSearchKnowledgeBase:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// GetAllBuiltinTools 返回所有内置工具名称列表
|
||||
func GetAllBuiltinTools() []string {
|
||||
return []string{
|
||||
ToolRecordVulnerability,
|
||||
ToolListKnowledgeRiskTypes,
|
||||
ToolSearchKnowledgeBase,
|
||||
}
|
||||
}
|
||||
+25
-1
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -246,6 +247,7 @@ func (c *HTTPMCPClient) Close() error {
|
||||
type StdioMCPClient struct {
|
||||
command string
|
||||
args []string
|
||||
env map[string]string
|
||||
timeout time.Duration
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
@@ -263,7 +265,7 @@ type StdioMCPClient struct {
|
||||
}
|
||||
|
||||
// NewStdioMCPClient 创建stdio模式的MCP客户端
|
||||
func NewStdioMCPClient(command string, args []string, timeout time.Duration, logger *zap.Logger) *StdioMCPClient {
|
||||
func NewStdioMCPClient(command string, args []string, env map[string]string, timeout time.Duration, logger *zap.Logger) *StdioMCPClient {
|
||||
if timeout <= 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
@@ -271,6 +273,7 @@ func NewStdioMCPClient(command string, args []string, timeout time.Duration, log
|
||||
return &StdioMCPClient{
|
||||
command: command,
|
||||
args: args,
|
||||
env: env,
|
||||
timeout: timeout,
|
||||
logger: logger,
|
||||
status: "disconnected",
|
||||
@@ -354,6 +357,27 @@ func (c *StdioMCPClient) Initialize(ctx context.Context) error {
|
||||
func (c *StdioMCPClient) startProcess() error {
|
||||
cmd := exec.CommandContext(c.ctx, c.command, c.args...)
|
||||
|
||||
// 设置环境变量
|
||||
if c.env != nil && len(c.env) > 0 {
|
||||
// 获取当前环境变量
|
||||
cmd.Env = os.Environ()
|
||||
// 添加或覆盖配置的环境变量
|
||||
for key, value := range c.env {
|
||||
// 检查是否已存在该环境变量
|
||||
found := false
|
||||
for i, envVar := range cmd.Env {
|
||||
if strings.HasPrefix(envVar, key+"=") {
|
||||
cmd.Env[i] = key + "=" + value
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
cmd.Env = append(cmd.Env, key+"="+value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -738,7 +738,7 @@ func (m *ExternalMCPManager) createClient(serverCfg config.ExternalMCPServerConf
|
||||
if serverCfg.Command == "" {
|
||||
return nil
|
||||
}
|
||||
return NewStdioMCPClient(serverCfg.Command, serverCfg.Args, timeout, m.logger)
|
||||
return NewStdioMCPClient(serverCfg.Command, serverCfg.Args, serverCfg.Env, timeout, m.logger)
|
||||
case "sse":
|
||||
if serverCfg.URL == "" {
|
||||
return nil
|
||||
|
||||
@@ -180,7 +180,7 @@ func TestStdioMCPClient_Initialize(t *testing.T) {
|
||||
// 注意:这个测试需要一个真实的stdio MCP服务器
|
||||
// 如果没有服务器,这个测试会失败
|
||||
logger := zap.NewNop()
|
||||
client := NewStdioMCPClient("echo", []string{"test"}, 5*time.Second, logger)
|
||||
client := NewStdioMCPClient("echo", []string{"test"}, nil, 5*time.Second, logger)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -3,6 +3,7 @@ package security
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os/exec"
|
||||
@@ -616,6 +617,13 @@ func (e *Executor) formatParamValue(param config.ParameterConfig, value interfac
|
||||
return strings.Join(strs, ",")
|
||||
}
|
||||
return fmt.Sprintf("%v", value)
|
||||
case "object":
|
||||
// 对象/字典:序列化为 JSON 字符串
|
||||
if jsonBytes, err := json.Marshal(value); err == nil {
|
||||
return string(jsonBytes)
|
||||
}
|
||||
// 如果 JSON 序列化失败,回退到默认格式化
|
||||
return fmt.Sprintf("%v", value)
|
||||
default:
|
||||
formattedValue := fmt.Sprintf("%v", value)
|
||||
// 特殊处理:对于 ports 参数(通常是 nmap 等工具的端口参数),清理空格
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
name: API安全测试
|
||||
description: API安全测试专家,专注于API接口安全检测
|
||||
user_prompt: 你是一个专业的API安全测试专家。请使用专业的API测试工具对目标API接口进行全面的安全检测,包括GraphQL安全、API参数fuzzing、JWT分析、API架构分析等工作。
|
||||
icon: "\U0001F4E1"
|
||||
tools:
|
||||
- api-fuzzer
|
||||
- api-schema-analyzer
|
||||
- graphql-scanner
|
||||
- arjun
|
||||
- jwt-analyzer
|
||||
- http-intruder
|
||||
- http-framework-test
|
||||
- burpsuite
|
||||
- httpx
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,33 @@
|
||||
name: CTF
|
||||
description: CTF竞赛专家,擅长解题和漏洞利用
|
||||
user_prompt: 你是一个CTF竞赛专家。请使用CTF解题思维和方法,快速定位和利用漏洞,解决各类CTF题目。
|
||||
icon: "\U0001F3C6"
|
||||
tools:
|
||||
- amass
|
||||
- anew
|
||||
- angr
|
||||
- api-fuzzer
|
||||
- api-schema-analyzer
|
||||
- arjun
|
||||
- arp-scan
|
||||
- autorecon
|
||||
- binwalk
|
||||
- bloodhound
|
||||
- burpsuite
|
||||
- cat
|
||||
- checkov
|
||||
- checksec
|
||||
- cloudmapper
|
||||
- create-file
|
||||
- cyberchef
|
||||
- dalfox
|
||||
- delete-file
|
||||
- httpx
|
||||
- http-framework-test
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,25 @@
|
||||
name: Web应用扫描
|
||||
description: Web应用漏洞扫描专家,全面的Web安全检测
|
||||
user_prompt: 你是一个专业的Web应用漏洞扫描专家。请使用各种Web扫描工具对目标Web应用进行全面的安全检测,包括目录枚举、文件扫描、漏洞识别等工作。
|
||||
icon: "\U0001F310"
|
||||
tools:
|
||||
- dirsearch
|
||||
- dirb
|
||||
- gobuster
|
||||
- feroxbuster
|
||||
- ffuf
|
||||
- wfuzz
|
||||
- sqlmap
|
||||
- dalfox
|
||||
- xsser
|
||||
- nikto
|
||||
- nuclei
|
||||
- wpscan
|
||||
- httpx
|
||||
- http-framework-test
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Web框架测试
|
||||
description: Web框架安全测试专家,专注于Web应用框架漏洞检测
|
||||
user_prompt: 你是一个专业的Web框架安全测试专家。请使用专业的工具对Web应用框架进行安全测试,识别框架相关的安全漏洞和配置问题。
|
||||
icon: "\U0001F310"
|
||||
tools:
|
||||
- http-framework-test
|
||||
- nikto
|
||||
- nuclei
|
||||
- wafw00f
|
||||
- wpscan
|
||||
- httpx
|
||||
- burpsuite
|
||||
- zap
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,31 @@
|
||||
name: 二进制分析
|
||||
description: 二进制分析与利用专家,擅长逆向工程和密码破解
|
||||
user_prompt: 你是一个专业的二进制分析与利用专家。请使用逆向工程工具分析二进制文件,识别漏洞,进行利用开发。同时擅长密码破解、哈希分析等技术。
|
||||
icon: "\U0001F52C"
|
||||
tools:
|
||||
- dirsearch
|
||||
- docker-bench-security
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- ghidra
|
||||
- graphql-scanner
|
||||
- hakrawler
|
||||
- hash-identifier
|
||||
- hashcat
|
||||
- hashpump
|
||||
- http-framework-test
|
||||
- httpx
|
||||
- gdb
|
||||
- radare2
|
||||
- objdump
|
||||
- strings
|
||||
- binwalk
|
||||
- ropper
|
||||
- ropgadget
|
||||
- john
|
||||
- cyberchef
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,17 @@
|
||||
name: 云安全审计
|
||||
description: 云安全审计专家,多云环境安全检测
|
||||
user_prompt: 你是一个专业的云安全审计专家。请使用专业的云安全工具对AWS、Azure、GCP等云环境进行全面的安全审计,包括配置检查、合规性评估、权限审计、安全最佳实践验证等工作。
|
||||
icon: "\U00002601"
|
||||
tools:
|
||||
- prowler
|
||||
- scout-suite
|
||||
- cloudmapper
|
||||
- pacu
|
||||
- terrascan
|
||||
- checkov
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,31 @@
|
||||
name: 信息收集
|
||||
description: 资产发现与信息搜集专家
|
||||
user_prompt: 你是一个专业的信息收集专家。请使用各种信息收集技术和工具,对目标进行全面的资产发现、子域名枚举、端口扫描、服务识别等信息收集工作。
|
||||
icon: "\U0001F50D"
|
||||
tools:
|
||||
- amass
|
||||
- subfinder
|
||||
- dnsenum
|
||||
- fierce
|
||||
- fofa_search
|
||||
- zoomeye_search
|
||||
- nmap
|
||||
- masscan
|
||||
- rustscan
|
||||
- arp-scan
|
||||
- nbtscan
|
||||
- httpx
|
||||
- http-framework-test
|
||||
- katana
|
||||
- hakrawler
|
||||
- waybackurls
|
||||
- paramspider
|
||||
- gau
|
||||
- uro
|
||||
- qsreplace
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,23 @@
|
||||
name: 后渗透测试
|
||||
description: 后渗透测试专家,权限维持与横向移动
|
||||
user_prompt: 你是一个专业的后渗透测试专家。请使用专业的后渗透工具在获得初始访问权限后进行权限提升、横向移动、权限维持、数据收集等后渗透测试工作。
|
||||
icon: "\U0001F575"
|
||||
tools:
|
||||
- linpeas
|
||||
- winpeas
|
||||
- mimikatz
|
||||
- bloodhound
|
||||
- impacket
|
||||
- responder
|
||||
- netexec
|
||||
- rpcclient
|
||||
- smbmap
|
||||
- enum4linux
|
||||
- enum4linux-ng
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,18 @@
|
||||
name: 容器安全
|
||||
description: 容器与Kubernetes安全专家,容器环境安全检测
|
||||
user_prompt: 你是一个专业的容器与Kubernetes安全专家。请使用专业的容器安全工具对Docker容器和Kubernetes集群进行全面的安全检测,包括镜像漏洞扫描、配置检查、运行时安全等工作。
|
||||
icon: "\U0001F6E1"
|
||||
tools:
|
||||
- trivy
|
||||
- clair
|
||||
- docker-bench-security
|
||||
- kube-bench
|
||||
- kube-hunter
|
||||
- falco
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,24 @@
|
||||
name: 数字取证
|
||||
description: 数字取证与隐写分析专家,文件与内存取证
|
||||
user_prompt: 你是一个专业的数字取证与隐写分析专家。请使用专业的取证工具对文件、磁盘镜像、内存转储进行分析,提取证据信息。同时擅长隐写分析、数据恢复、元数据提取等技术。
|
||||
icon: "\U0001F50E"
|
||||
tools:
|
||||
- volatility
|
||||
- volatility3
|
||||
- foremost
|
||||
- steghide
|
||||
- stegsolve
|
||||
- zsteg
|
||||
- exiftool
|
||||
- binwalk
|
||||
- strings
|
||||
- xxd
|
||||
- fcrackzip
|
||||
- pdfcrack
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,33 @@
|
||||
name: 渗透测试
|
||||
description: 专业渗透测试专家,全面深入的漏洞检测
|
||||
user_prompt: 你是一个专业的网络安全渗透测试专家。请使用专业的渗透测试方法和工具,对目标进行全面的安全测试,包括但不限于SQL注入、XSS、CSRF、文件包含、命令执行等常见漏洞。
|
||||
icon: "\U0001F3AF"
|
||||
tools:
|
||||
- http-framework-test
|
||||
- httpx
|
||||
- amass
|
||||
- anew
|
||||
- angr
|
||||
- api-fuzzer
|
||||
- api-schema-analyzer
|
||||
- arjun
|
||||
- arp-scan
|
||||
- autorecon
|
||||
- binwalk
|
||||
- bloodhound
|
||||
- burpsuite
|
||||
- cat
|
||||
- checkov
|
||||
- checksec
|
||||
- cloudmapper
|
||||
- create-file
|
||||
- cyberchef
|
||||
- dalfox
|
||||
- delete-file
|
||||
- exec
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,23 @@
|
||||
name: 综合漏洞扫描
|
||||
description: 综合漏洞扫描专家,多类型漏洞检测
|
||||
user_prompt: 你是一个专业的综合漏洞扫描专家。请使用各种漏洞扫描工具对目标进行全面的安全检测,包括Web漏洞、网络服务漏洞、配置缺陷等多种类型的漏洞识别和分析。
|
||||
icon: "\U000026A0"
|
||||
tools:
|
||||
- nuclei
|
||||
- nikto
|
||||
- sqlmap
|
||||
- nmap
|
||||
- masscan
|
||||
- rustscan
|
||||
- wafw00f
|
||||
- dalfox
|
||||
- xsser
|
||||
- jaeles
|
||||
- httpx
|
||||
- http-framework-test
|
||||
- execute-python-script
|
||||
- install-python-package
|
||||
- record_vulnerability
|
||||
- list_knowledge_risk_types
|
||||
- search_knowledge_base
|
||||
enabled: true
|
||||
@@ -0,0 +1,5 @@
|
||||
name: 默认
|
||||
description: 默认角色,不额外携带用户提示词,使用默认MCP
|
||||
user_prompt: ""
|
||||
icon: "\U0001F535"
|
||||
enabled: true
|
||||
@@ -445,7 +445,7 @@ args:
|
||||
parser.add_argument("--url", required=True)
|
||||
parser.add_argument("--method", default="GET")
|
||||
parser.add_argument("--data", default="")
|
||||
parser.add_argument("--headers", default="")
|
||||
parser.add_argument("--headers", default="", type=str)
|
||||
parser.add_argument("--cookies", default="")
|
||||
parser.add_argument("--user-agent", dest="user_agent", default="")
|
||||
parser.add_argument("--proxy", default="")
|
||||
@@ -489,7 +489,30 @@ args:
|
||||
prepared_url = smart_encode_url(args.url) if args.auto_encode_url else args.url
|
||||
method = (args.method or "GET").upper()
|
||||
|
||||
headers = httpx.Headers(parse_headers(args.headers))
|
||||
# 处理 headers:支持字典(JSON字符串)和字符串格式
|
||||
# 框架会将 object 类型序列化为 JSON 字符串传递
|
||||
headers_list = []
|
||||
if args.headers:
|
||||
headers_str = args.headers.strip()
|
||||
# 优先尝试解析为 JSON(框架传递的字典会被序列化为 JSON)
|
||||
if headers_str.startswith("{") or headers_str.startswith("["):
|
||||
try:
|
||||
parsed = json.loads(headers_str)
|
||||
if isinstance(parsed, dict):
|
||||
# 字典格式:直接转换为 (key, value) 元组列表
|
||||
headers_list = [(str(k).strip(), str(v).strip()) for k, v in parsed.items()]
|
||||
elif isinstance(parsed, list):
|
||||
# 数组格式:使用原有的 parse_headers 函数处理
|
||||
headers_list = parse_headers(headers_str)
|
||||
else:
|
||||
headers_list = parse_headers(headers_str)
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
# JSON 解析失败,回退到原有的字符串解析逻辑
|
||||
headers_list = parse_headers(headers_str)
|
||||
else:
|
||||
# 非 JSON 格式,使用原有的字符串解析逻辑(向后兼容)
|
||||
headers_list = parse_headers(headers_str)
|
||||
headers = httpx.Headers(headers_list)
|
||||
if args.user_agent:
|
||||
headers["User-Agent"] = args.user_agent
|
||||
|
||||
@@ -724,8 +747,8 @@ parameters:
|
||||
required: false
|
||||
flag: "--data"
|
||||
- name: "headers"
|
||||
type: "string"
|
||||
description: "自定义请求头(JSON字典、行分隔或分号分隔的 Header: Value 格式)"
|
||||
type: "object"
|
||||
description: "自定义请求头(字典格式,如 {\"X-Custom\": \"value\"})"
|
||||
required: false
|
||||
flag: "--headers"
|
||||
- name: "cookies"
|
||||
|
||||
+36
-10
@@ -17,20 +17,46 @@ args:
|
||||
url = sys.argv[1]
|
||||
method = (sys.argv[2] or "GET").upper()
|
||||
location = (sys.argv[3] or "query").lower()
|
||||
params_json = sys.argv[4] if len(sys.argv) > 4 else "{}"
|
||||
params_input = sys.argv[4] if len(sys.argv) > 4 else "{}"
|
||||
payloads_json = sys.argv[5] if len(sys.argv) > 5 else "[]"
|
||||
max_requests = int(sys.argv[6]) if len(sys.argv) > 6 and sys.argv[6] else 0
|
||||
|
||||
try:
|
||||
params_template = json.loads(params_json) if params_json else {}
|
||||
# 框架会将 object 类型序列化为 JSON 字符串传递
|
||||
# sys.argv 中的参数都是字符串,需要解析 JSON
|
||||
if params_input and params_input.strip():
|
||||
params_template = json.loads(params_input)
|
||||
if not isinstance(params_template, dict):
|
||||
sys.stderr.write("参数模板必须是字典格式\n")
|
||||
sys.exit(1)
|
||||
else:
|
||||
params_template = {}
|
||||
except json.JSONDecodeError as exc:
|
||||
sys.stderr.write(f"参数模板解析失败: {exc}\n")
|
||||
sys.stderr.write(f"参数模板解析失败(需要 JSON 字典格式): {exc}\n")
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
payloads = json.loads(payloads_json)
|
||||
except json.JSONDecodeError as exc:
|
||||
sys.stderr.write(f"载荷解析失败: {exc}\n")
|
||||
# 框架会将 array 类型转换为逗号分隔的字符串(见 formatParamValue)
|
||||
# 但为了兼容性,也支持 JSON 数组格式
|
||||
if payloads_json and payloads_json.strip():
|
||||
payloads_str = payloads_json.strip()
|
||||
# 优先尝试解析为 JSON 数组
|
||||
if payloads_str.startswith("["):
|
||||
try:
|
||||
payloads = json.loads(payloads_str)
|
||||
except json.JSONDecodeError:
|
||||
# JSON 解析失败,尝试逗号分隔格式
|
||||
payloads = [item.strip() for item in payloads_str.split(",") if item.strip()]
|
||||
else:
|
||||
# 逗号分隔的字符串(框架的 array 类型默认格式)
|
||||
payloads = [item.strip() for item in payloads_str.split(",") if item.strip()]
|
||||
if not isinstance(payloads, list):
|
||||
sys.stderr.write("载荷必须是数组格式\n")
|
||||
sys.exit(1)
|
||||
else:
|
||||
payloads = []
|
||||
except (json.JSONDecodeError, ValueError) as exc:
|
||||
sys.stderr.write(f"载荷解析失败(需要 JSON 数组或逗号分隔格式): {exc}\n")
|
||||
sys.exit(1)
|
||||
|
||||
if not isinstance(payloads, list) or not payloads:
|
||||
@@ -110,14 +136,14 @@ parameters:
|
||||
position: 2
|
||||
format: "positional"
|
||||
- name: "params"
|
||||
type: "string"
|
||||
description: "参数模板(JSON字典),指定要模糊的键及默认值"
|
||||
type: "object"
|
||||
description: "参数模板(字典格式),指定要模糊的键及默认值,如 {\"id\": \"1\", \"name\": \"test\"}"
|
||||
required: true
|
||||
position: 3
|
||||
format: "positional"
|
||||
- name: "payloads"
|
||||
type: "string"
|
||||
description: "载荷列表(JSON数组)"
|
||||
type: "array"
|
||||
description: "载荷列表(数组格式),如 [\"test1\", \"test2\", \"test3\"]"
|
||||
required: true
|
||||
position: 4
|
||||
format: "positional"
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
name: "httpx"
|
||||
command: "httpx"
|
||||
enabled: true
|
||||
short_description: "基于Python httpx库的HTTP客户端"
|
||||
description: |
|
||||
该工具包装的是 Python 社区版 httpx CLI(`pip install httpx` 提供),可用于快速向 Web 目标发起请求、调试接口。
|
||||
|
||||
**提示:**
|
||||
- 官方 CLI 的调用方式为 `httpx <URL> [OPTIONS]`
|
||||
- 不支持 ProjectDiscovery 版本的 `-u/-l/-td` 等参数,请使用下方列出的原生选项或 additional_args 自行扩展
|
||||
parameters:
|
||||
- name: "url"
|
||||
type: "string"
|
||||
description: "目标URL(必填,作为位置参数传入)"
|
||||
required: true
|
||||
format: "positional"
|
||||
- name: "method"
|
||||
type: "string"
|
||||
description: "HTTP方法,默认GET"
|
||||
required: false
|
||||
flag: "-m"
|
||||
format: "flag"
|
||||
- name: "content"
|
||||
type: "string"
|
||||
description: "原始请求体内容(对应 httpx CLI 的 --content)"
|
||||
required: false
|
||||
flag: "-c"
|
||||
format: "flag"
|
||||
- name: "json"
|
||||
type: "string"
|
||||
description: "JSON 请求体(字符串形式)"
|
||||
required: false
|
||||
flag: "-j"
|
||||
format: "flag"
|
||||
- name: "proxy"
|
||||
type: "string"
|
||||
description: "代理地址(http(s):// 或 socks5://)"
|
||||
required: false
|
||||
flag: "--proxy"
|
||||
format: "flag"
|
||||
- name: "timeout"
|
||||
type: "string"
|
||||
description: "网络超时时间(秒,可为小数)"
|
||||
required: false
|
||||
flag: "--timeout"
|
||||
format: "flag"
|
||||
- name: "follow_redirects"
|
||||
type: "bool"
|
||||
description: "是否自动跟随重定向"
|
||||
required: false
|
||||
flag: "--follow-redirects"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "no_verify"
|
||||
type: "bool"
|
||||
description: "关闭TLS证书校验(对应 --no-verify)"
|
||||
required: false
|
||||
flag: "--no-verify"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "http2"
|
||||
type: "bool"
|
||||
description: "启用HTTP/2"
|
||||
required: false
|
||||
flag: "--http2"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "download"
|
||||
type: "string"
|
||||
description: "将响应内容保存至文件"
|
||||
required: false
|
||||
flag: "--download"
|
||||
format: "flag"
|
||||
- name: "verbose"
|
||||
type: "bool"
|
||||
description: "显示请求与响应的详细信息"
|
||||
required: false
|
||||
flag: "-v"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: |
|
||||
额外 httpx CLI 选项,格式直接与官方命令保持一致。
|
||||
|
||||
**示例:**
|
||||
- "--headers 'X-Test 1' 'X-Token secret'"
|
||||
- "--cookies 'session abc123'"
|
||||
- "--auth user pass"
|
||||
required: false
|
||||
format: "positional"
|
||||
+1375
-19
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 内置工具名称常量
|
||||
* 所有前端代码中使用内置工具名称的地方都应该使用这些常量,而不是硬编码字符串
|
||||
*
|
||||
* 注意:这些常量必须与后端的 internal/mcp/builtin/constants.go 中的常量保持一致
|
||||
*/
|
||||
|
||||
// 内置工具名称常量
|
||||
const BuiltinTools = {
|
||||
// 漏洞管理工具
|
||||
RECORD_VULNERABILITY: 'record_vulnerability',
|
||||
|
||||
// 知识库工具
|
||||
LIST_KNOWLEDGE_RISK_TYPES: 'list_knowledge_risk_types',
|
||||
SEARCH_KNOWLEDGE_BASE: 'search_knowledge_base'
|
||||
};
|
||||
|
||||
// 检查是否是内置工具
|
||||
function isBuiltinTool(toolName) {
|
||||
return Object.values(BuiltinTools).includes(toolName);
|
||||
}
|
||||
|
||||
// 获取所有内置工具名称列表
|
||||
function getAllBuiltinTools() {
|
||||
return Object.values(BuiltinTools);
|
||||
}
|
||||
|
||||
+412
-27
@@ -14,6 +14,9 @@ const mentionState = {
|
||||
selectedIndex: 0,
|
||||
};
|
||||
|
||||
// IME输入法状态跟踪
|
||||
let isComposing = false;
|
||||
|
||||
// 输入框草稿保存相关
|
||||
const DRAFT_STORAGE_KEY = 'cyberstrike-chat-draft';
|
||||
let draftSaveTimer = null;
|
||||
@@ -85,13 +88,20 @@ function clearChatDraft() {
|
||||
function adjustTextareaHeight(textarea) {
|
||||
if (!textarea) return;
|
||||
|
||||
// 重置高度以获取准确的scrollHeight
|
||||
textarea.style.height = '44px';
|
||||
// 先重置高度为auto,然后立即设置为固定值,确保能准确获取scrollHeight
|
||||
textarea.style.height = 'auto';
|
||||
// 强制浏览器重新计算布局
|
||||
void textarea.offsetHeight;
|
||||
|
||||
// 计算新高度(最小44px,最大不超过300px)
|
||||
const scrollHeight = textarea.scrollHeight;
|
||||
const newHeight = Math.min(Math.max(scrollHeight, 44), 300);
|
||||
textarea.style.height = newHeight + 'px';
|
||||
|
||||
// 如果内容为空或只有很少内容,立即重置到最小高度
|
||||
if (!textarea.value || textarea.value.trim().length === 0) {
|
||||
textarea.style.height = '44px';
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
@@ -135,6 +145,9 @@ async function sendMessage() {
|
||||
let mcpExecutionIds = [];
|
||||
|
||||
try {
|
||||
// 获取当前选中的角色(从 roles.js 的函数获取)
|
||||
const roleName = typeof getCurrentRole === 'function' ? getCurrentRole() : '';
|
||||
|
||||
const response = await apiFetch('/api/agent-loop/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@@ -142,7 +155,8 @@ async function sendMessage() {
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
conversationId: currentConversationId
|
||||
conversationId: currentConversationId,
|
||||
role: roleName || undefined
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -242,6 +256,13 @@ if (typeof window !== 'undefined') {
|
||||
}
|
||||
|
||||
function ensureMentionToolsLoaded() {
|
||||
// 检查角色是否改变,如果改变则强制重新加载
|
||||
if (typeof window !== 'undefined' && window._mentionToolsRoleChanged) {
|
||||
mentionToolsLoaded = false;
|
||||
mentionTools = [];
|
||||
delete window._mentionToolsRoleChanged;
|
||||
}
|
||||
|
||||
if (mentionToolsLoaded) {
|
||||
return Promise.resolve(mentionTools);
|
||||
}
|
||||
@@ -254,6 +275,16 @@ function ensureMentionToolsLoaded() {
|
||||
return mentionToolsLoadingPromise;
|
||||
}
|
||||
|
||||
// 生成工具的唯一标识符,用于区分同名但来源不同的工具
|
||||
function getToolKeyForMention(tool) {
|
||||
// 如果是外部工具,使用 external_mcp::tool.name 作为唯一标识
|
||||
// 如果是内部工具,使用 tool.name 作为标识
|
||||
if (tool.is_external && tool.external_mcp) {
|
||||
return `${tool.external_mcp}::${tool.name}`;
|
||||
}
|
||||
return tool.name;
|
||||
}
|
||||
|
||||
async function fetchMentionTools() {
|
||||
const pageSize = 100;
|
||||
let page = 1;
|
||||
@@ -262,6 +293,9 @@ async function fetchMentionTools() {
|
||||
const collected = [];
|
||||
|
||||
try {
|
||||
// 获取当前选中的角色(从 roles.js 的函数获取)
|
||||
const roleName = typeof getCurrentRole === 'function' ? getCurrentRole() : '';
|
||||
|
||||
// 同时获取外部MCP列表
|
||||
try {
|
||||
const mcpResponse = await apiFetch('/api/external-mcp');
|
||||
@@ -280,23 +314,45 @@ async function fetchMentionTools() {
|
||||
}
|
||||
|
||||
while (page <= totalPages && page <= 20) {
|
||||
const response = await apiFetch(`/api/config/tools?page=${page}&page_size=${pageSize}`);
|
||||
// 构建API URL,如果指定了角色,添加role查询参数
|
||||
let url = `/api/config/tools?page=${page}&page_size=${pageSize}`;
|
||||
if (roleName && roleName !== '默认') {
|
||||
url += `&role=${encodeURIComponent(roleName)}`;
|
||||
}
|
||||
|
||||
const response = await apiFetch(url);
|
||||
if (!response.ok) {
|
||||
break;
|
||||
}
|
||||
const result = await response.json();
|
||||
const tools = Array.isArray(result.tools) ? result.tools : [];
|
||||
tools.forEach(tool => {
|
||||
if (!tool || !tool.name || seen.has(tool.name)) {
|
||||
if (!tool || !tool.name) {
|
||||
return;
|
||||
}
|
||||
seen.add(tool.name);
|
||||
// 使用唯一标识符来去重,而不是只使用工具名称
|
||||
const toolKey = getToolKeyForMention(tool);
|
||||
if (seen.has(toolKey)) {
|
||||
return;
|
||||
}
|
||||
seen.add(toolKey);
|
||||
|
||||
// 确定工具在当前角色中的启用状态
|
||||
// 如果有 role_enabled 字段,使用它(表示指定了角色)
|
||||
// 否则使用 enabled 字段(表示未指定角色或使用所有工具)
|
||||
let roleEnabled = tool.enabled !== false;
|
||||
if (tool.role_enabled !== undefined && tool.role_enabled !== null) {
|
||||
roleEnabled = tool.role_enabled;
|
||||
}
|
||||
|
||||
collected.push({
|
||||
name: tool.name,
|
||||
description: tool.description || '',
|
||||
enabled: tool.enabled !== false,
|
||||
enabled: tool.enabled !== false, // 工具本身的启用状态
|
||||
roleEnabled: roleEnabled, // 在当前角色中的启用状态
|
||||
isExternal: !!tool.is_external,
|
||||
externalMcp: tool.external_mcp || '',
|
||||
toolKey: toolKey, // 保存唯一标识符
|
||||
});
|
||||
});
|
||||
totalPages = result.total_pages || 1;
|
||||
@@ -317,7 +373,10 @@ function handleChatInputInput(event) {
|
||||
const textarea = event.target;
|
||||
updateMentionStateFromInput(textarea);
|
||||
// 自动调整输入框高度
|
||||
adjustTextareaHeight(textarea);
|
||||
// 使用requestAnimationFrame确保在DOM更新后立即调整,特别是在删除内容时
|
||||
requestAnimationFrame(() => {
|
||||
adjustTextareaHeight(textarea);
|
||||
});
|
||||
// 保存输入内容到localStorage(防抖)
|
||||
saveChatDraftDebounced(textarea.value);
|
||||
}
|
||||
@@ -327,6 +386,12 @@ function handleChatInputClick(event) {
|
||||
}
|
||||
|
||||
function handleChatInputKeydown(event) {
|
||||
// 如果正在使用输入法输入(IME),回车键应该用于确认候选词,而不是发送消息
|
||||
// 使用 event.isComposing 或 isComposing 标志来判断
|
||||
if (event.isComposing || isComposing) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mentionState.active && mentionSuggestionsEl && mentionSuggestionsEl.style.display !== 'none') {
|
||||
if (event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
@@ -453,6 +518,15 @@ function updateMentionCandidates() {
|
||||
}
|
||||
|
||||
filtered = filtered.slice().sort((a, b) => {
|
||||
// 如果指定了角色,优先显示在当前角色中启用的工具
|
||||
if (a.roleEnabled !== undefined || b.roleEnabled !== undefined) {
|
||||
const aRoleEnabled = a.roleEnabled !== undefined ? a.roleEnabled : a.enabled;
|
||||
const bRoleEnabled = b.roleEnabled !== undefined ? b.roleEnabled : b.enabled;
|
||||
if (aRoleEnabled !== bRoleEnabled) {
|
||||
return aRoleEnabled ? -1 : 1; // 启用的工具排在前面
|
||||
}
|
||||
}
|
||||
|
||||
if (normalizedQuery) {
|
||||
// 精确匹配MCP名称的工具优先显示
|
||||
const aMcpExact = a.externalMcp && a.externalMcp.toLowerCase() === normalizedQuery;
|
||||
@@ -467,8 +541,11 @@ function updateMentionCandidates() {
|
||||
return aStarts ? -1 : 1;
|
||||
}
|
||||
}
|
||||
if (a.enabled !== b.enabled) {
|
||||
return a.enabled ? -1 : 1;
|
||||
// 如果指定了角色,使用 roleEnabled;否则使用 enabled
|
||||
const aEnabled = a.roleEnabled !== undefined ? a.roleEnabled : a.enabled;
|
||||
const bEnabled = b.roleEnabled !== undefined ? b.roleEnabled : b.enabled;
|
||||
if (aEnabled !== bEnabled) {
|
||||
return aEnabled ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name, 'zh-CN');
|
||||
});
|
||||
@@ -510,13 +587,16 @@ function renderMentionSuggestions({ showLoading = false } = {}) {
|
||||
|
||||
const itemsHtml = mentionFilteredTools.map((tool, index) => {
|
||||
const activeClass = index === mentionState.selectedIndex ? 'active' : '';
|
||||
const disabledClass = tool.enabled ? '' : 'disabled';
|
||||
// 如果工具有 roleEnabled 字段(指定了角色),使用它;否则使用 enabled
|
||||
const toolEnabled = tool.roleEnabled !== undefined ? tool.roleEnabled : tool.enabled;
|
||||
const disabledClass = toolEnabled ? '' : 'disabled';
|
||||
const badge = tool.isExternal ? '<span class="mention-item-badge">外部</span>' : '<span class="mention-item-badge internal">内置</span>';
|
||||
const nameHtml = escapeHtml(tool.name);
|
||||
const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : '暂无描述';
|
||||
const descHtml = `<div class="mention-item-desc">${description}</div>`;
|
||||
const statusLabel = tool.enabled ? '可用' : '已禁用';
|
||||
const statusClass = tool.enabled ? 'enabled' : 'disabled';
|
||||
// 根据工具在当前角色中的启用状态显示状态标签
|
||||
const statusLabel = toolEnabled ? '可用' : (tool.roleEnabled !== undefined ? '已禁用(当前角色)' : '已禁用');
|
||||
const statusClass = toolEnabled ? 'enabled' : 'disabled';
|
||||
const originLabel = tool.isExternal
|
||||
? (tool.externalMcp ? `来源:${escapeHtml(tool.externalMcp)}` : '来源:外部MCP')
|
||||
: '来源:内置工具';
|
||||
@@ -723,6 +803,25 @@ function initializeChatUI() {
|
||||
// 消息计数器,确保ID唯一
|
||||
let messageCounter = 0;
|
||||
|
||||
// 为消息气泡中的表格添加独立的滚动容器
|
||||
function wrapTablesInBubble(bubble) {
|
||||
const tables = bubble.querySelectorAll('table');
|
||||
tables.forEach(table => {
|
||||
// 检查表格是否已经有包装容器
|
||||
if (table.parentElement && table.parentElement.classList.contains('table-wrapper')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建表格包装容器
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'table-wrapper';
|
||||
|
||||
// 将表格移动到包装容器中
|
||||
table.parentNode.insertBefore(wrapper, table);
|
||||
wrapper.appendChild(table);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
function addMessage(role, content, mcpExecutionIds = null, progressId = null, createdAt = null) {
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
@@ -798,8 +897,30 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
}
|
||||
|
||||
bubble.innerHTML = formattedContent;
|
||||
|
||||
// 为每个表格添加独立的滚动容器
|
||||
wrapTablesInBubble(bubble);
|
||||
|
||||
contentWrapper.appendChild(bubble);
|
||||
|
||||
// 保存原始内容到消息元素,用于复制功能
|
||||
if (role === 'assistant') {
|
||||
messageDiv.dataset.originalContent = content;
|
||||
}
|
||||
|
||||
// 为助手消息添加复制按钮(复制整个回复内容)- 放在消息气泡右下角
|
||||
if (role === 'assistant') {
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'message-copy-btn';
|
||||
copyBtn.innerHTML = '<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" ry="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><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="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>复制</span>';
|
||||
copyBtn.title = '复制消息内容';
|
||||
copyBtn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
copyMessageToClipboard(messageDiv, this);
|
||||
};
|
||||
bubble.appendChild(copyBtn);
|
||||
}
|
||||
|
||||
// 添加时间戳
|
||||
const timeDiv = document.createElement('div');
|
||||
timeDiv.className = 'message-time';
|
||||
@@ -845,6 +966,8 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
detailBtn.innerHTML = `<span>调用 #${index + 1}</span>`;
|
||||
detailBtn.onclick = () => showMCPDetail(execId);
|
||||
buttonsContainer.appendChild(detailBtn);
|
||||
// 异步获取工具名称并更新按钮文本
|
||||
updateButtonWithToolName(detailBtn, execId, index + 1);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -869,6 +992,69 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
return id;
|
||||
}
|
||||
|
||||
// 复制消息内容到剪贴板(使用原始Markdown格式)
|
||||
function copyMessageToClipboard(messageDiv, button) {
|
||||
try {
|
||||
// 获取保存的原始Markdown内容
|
||||
const originalContent = messageDiv.dataset.originalContent;
|
||||
|
||||
if (!originalContent) {
|
||||
// 如果没有保存原始内容,尝试从渲染后的HTML提取(降级方案)
|
||||
const bubble = messageDiv.querySelector('.message-bubble');
|
||||
if (bubble) {
|
||||
const tempDiv = document.createElement('div');
|
||||
tempDiv.innerHTML = bubble.innerHTML;
|
||||
|
||||
// 移除复制按钮本身(避免复制按钮文本)
|
||||
const copyBtnInTemp = tempDiv.querySelector('.message-copy-btn');
|
||||
if (copyBtnInTemp) {
|
||||
copyBtnInTemp.remove();
|
||||
}
|
||||
|
||||
// 提取纯文本内容
|
||||
let textContent = tempDiv.textContent || tempDiv.innerText || '';
|
||||
textContent = textContent.replace(/\n{3,}/g, '\n\n').trim();
|
||||
|
||||
navigator.clipboard.writeText(textContent).then(() => {
|
||||
showCopySuccess(button);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
alert('复制失败,请手动选择内容复制');
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 使用原始Markdown内容
|
||||
navigator.clipboard.writeText(originalContent).then(() => {
|
||||
showCopySuccess(button);
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
alert('复制失败,请手动选择内容复制');
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('复制消息时出错:', error);
|
||||
alert('复制失败,请手动选择内容复制');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示复制成功提示
|
||||
function showCopySuccess(button) {
|
||||
if (button) {
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>已复制</span>';
|
||||
button.style.color = '#10b981';
|
||||
button.style.background = 'rgba(16, 185, 129, 0.1)';
|
||||
button.style.borderColor = 'rgba(16, 185, 129, 0.3)';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.style.color = '';
|
||||
button.style.background = '';
|
||||
button.style.borderColor = '';
|
||||
}, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染过程详情
|
||||
function renderProcessDetails(messageId, processDetails) {
|
||||
const messageElement = document.getElementById(messageId);
|
||||
@@ -993,7 +1179,7 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
itemTitle = `${statusIcon} 工具 ${escapeHtml(toolName)} 执行${success ? '完成' : '失败'}`;
|
||||
|
||||
// 如果是知识检索工具,添加特殊标记
|
||||
if (toolName === 'search_knowledge_base' && success) {
|
||||
if (toolName === BuiltinTools.SEARCH_KNOWLEDGE_BASE && success) {
|
||||
itemTitle = `📚 ${itemTitle} - 知识检索`;
|
||||
}
|
||||
} else if (eventType === 'knowledge_retrieval') {
|
||||
@@ -1041,6 +1227,13 @@ if (chatInput) {
|
||||
chatInput.addEventListener('input', handleChatInputInput);
|
||||
chatInput.addEventListener('click', handleChatInputClick);
|
||||
chatInput.addEventListener('focus', handleChatInputClick);
|
||||
// IME输入法事件监听,用于跟踪输入法状态
|
||||
chatInput.addEventListener('compositionstart', () => {
|
||||
isComposing = true;
|
||||
});
|
||||
chatInput.addEventListener('compositionend', () => {
|
||||
isComposing = false;
|
||||
});
|
||||
chatInput.addEventListener('blur', () => {
|
||||
setTimeout(() => {
|
||||
if (!chatInput.matches(':focus')) {
|
||||
@@ -1063,6 +1256,23 @@ window.addEventListener('beforeunload', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// 异步获取工具名称并更新按钮文本
|
||||
async function updateButtonWithToolName(button, executionId, index) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/monitor/execution/${executionId}`);
|
||||
if (response.ok) {
|
||||
const exec = await response.json();
|
||||
const toolName = exec.toolName || '未知工具';
|
||||
// 格式化工具名称(如果是 name::toolName 格式,只显示 toolName 部分)
|
||||
const displayToolName = toolName.includes('::') ? toolName.split('::')[1] : toolName;
|
||||
button.querySelector('span').textContent = `${displayToolName} #${index}`;
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果获取失败,保持原有文本不变
|
||||
console.error('获取工具名称失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示MCP调用详情
|
||||
async function showMCPDetail(executionId) {
|
||||
try {
|
||||
@@ -1281,16 +1491,27 @@ async function loadConversations(searchQuery = '') {
|
||||
url += '&search=' + encodeURIComponent(searchQuery.trim());
|
||||
}
|
||||
const response = await apiFetch(url);
|
||||
const conversations = await response.json();
|
||||
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (!listContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存滚动位置
|
||||
const sidebarContent = listContainer.closest('.sidebar-content');
|
||||
const savedScrollTop = sidebarContent ? sidebarContent.scrollTop : 0;
|
||||
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
// 如果响应不是200,显示空状态(友好处理,不显示错误)
|
||||
if (!response.ok) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
return;
|
||||
}
|
||||
|
||||
const conversations = await response.json();
|
||||
|
||||
if (!Array.isArray(conversations) || conversations.length === 0) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
return;
|
||||
@@ -1363,8 +1584,22 @@ async function loadConversations(searchQuery = '') {
|
||||
|
||||
listContainer.appendChild(fragment);
|
||||
updateActiveConversation();
|
||||
|
||||
// 恢复滚动位置
|
||||
if (sidebarContent) {
|
||||
// 使用 requestAnimationFrame 确保 DOM 已经更新
|
||||
requestAnimationFrame(() => {
|
||||
sidebarContent.scrollTop = savedScrollTop;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载对话列表失败:', error);
|
||||
// 错误时显示空状态,而不是错误提示(更友好的用户体验)
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (listContainer) {
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3888,16 +4123,27 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
url += '&search=' + encodeURIComponent(searchQuery.trim());
|
||||
}
|
||||
const response = await apiFetch(url);
|
||||
const conversations = await response.json();
|
||||
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (!listContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存滚动位置
|
||||
const sidebarContent = listContainer.closest('.sidebar-content');
|
||||
const savedScrollTop = sidebarContent ? sidebarContent.scrollTop : 0;
|
||||
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
// 如果响应不是200,显示空状态(友好处理,不显示错误)
|
||||
if (!response.ok) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
return;
|
||||
}
|
||||
|
||||
const conversations = await response.json();
|
||||
|
||||
if (!Array.isArray(conversations) || conversations.length === 0) {
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
return;
|
||||
@@ -3966,8 +4212,22 @@ async function loadConversationsWithGroups(searchQuery = '') {
|
||||
|
||||
listContainer.appendChild(fragment);
|
||||
updateActiveConversation();
|
||||
|
||||
// 恢复滚动位置
|
||||
if (sidebarContent) {
|
||||
// 使用 requestAnimationFrame 确保 DOM 已经更新
|
||||
requestAnimationFrame(() => {
|
||||
sidebarContent.scrollTop = savedScrollTop;
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载对话列表失败:', error);
|
||||
// 错误时显示空状态,而不是错误提示(更友好的用户体验)
|
||||
const listContainer = document.getElementById('conversations-list');
|
||||
if (listContainer) {
|
||||
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
|
||||
listContainer.innerHTML = emptyStateHtml;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4820,7 +5080,14 @@ let allConversationsForBatch = [];
|
||||
async function showBatchManageModal() {
|
||||
try {
|
||||
const response = await apiFetch('/api/conversations?limit=1000');
|
||||
allConversationsForBatch = await response.json();
|
||||
|
||||
// 如果响应不是200,使用空数组(友好处理,不显示错误)
|
||||
if (!response.ok) {
|
||||
allConversationsForBatch = [];
|
||||
} else {
|
||||
const data = await response.json();
|
||||
allConversationsForBatch = Array.isArray(data) ? data : [];
|
||||
}
|
||||
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
const countEl = document.getElementById('batch-manage-count');
|
||||
@@ -4834,7 +5101,17 @@ async function showBatchManageModal() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载对话列表失败:', error);
|
||||
alert('加载对话列表失败');
|
||||
// 错误时使用空数组,不显示错误提示(更友好的用户体验)
|
||||
allConversationsForBatch = [];
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
const countEl = document.getElementById('batch-manage-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = 0;
|
||||
}
|
||||
if (modal) {
|
||||
renderBatchConversations();
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5148,7 +5425,8 @@ async function enterGroupDetail(groupId) {
|
||||
// 刷新分组列表,确保当前分组高亮显示
|
||||
await loadGroups();
|
||||
|
||||
loadGroupConversations(groupId);
|
||||
// 加载分组对话(如果有搜索查询则使用搜索查询)
|
||||
loadGroupConversations(groupId, currentGroupSearchQuery);
|
||||
} catch (error) {
|
||||
console.error('加载分组失败:', error);
|
||||
currentGroupId = null;
|
||||
@@ -5158,6 +5436,14 @@ async function enterGroupDetail(groupId) {
|
||||
// 退出分组详情
|
||||
function exitGroupDetail() {
|
||||
currentGroupId = null;
|
||||
currentGroupSearchQuery = ''; // 清除搜索状态
|
||||
|
||||
// 隐藏搜索框并清除搜索内容
|
||||
const searchContainer = document.getElementById('group-search-container');
|
||||
const searchInput = document.getElementById('group-search-input');
|
||||
if (searchContainer) searchContainer.style.display = 'none';
|
||||
if (searchInput) searchInput.value = '';
|
||||
|
||||
const sidebar = document.querySelector('.conversation-sidebar');
|
||||
const groupDetailPage = document.getElementById('group-detail-page');
|
||||
const chatContainer = document.querySelector('.chat-container');
|
||||
@@ -5172,7 +5458,7 @@ function exitGroupDetail() {
|
||||
}
|
||||
|
||||
// 加载分组中的对话
|
||||
async function loadGroupConversations(groupId) {
|
||||
async function loadGroupConversations(groupId, searchQuery = '') {
|
||||
try {
|
||||
if (!groupId) {
|
||||
console.error('loadGroupConversations: groupId is null or undefined');
|
||||
@@ -5190,10 +5476,20 @@ async function loadGroupConversations(groupId) {
|
||||
console.error('group-conversations-list element not found');
|
||||
return;
|
||||
}
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载中...</div>';
|
||||
|
||||
// 显示加载状态
|
||||
if (searchQuery) {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">搜索中...</div>';
|
||||
} else {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载中...</div>';
|
||||
}
|
||||
|
||||
// 确保使用正确的 groupId
|
||||
const url = `/api/groups/${groupId}/conversations`;
|
||||
// 构建URL,如果有搜索关键词则添加search参数
|
||||
let url = `/api/groups/${groupId}/conversations`;
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
url += '?search=' + encodeURIComponent(searchQuery.trim());
|
||||
}
|
||||
|
||||
const response = await apiFetch(url);
|
||||
if (!response.ok) {
|
||||
console.error(`Failed to load conversations for group ${groupId}:`, response.statusText);
|
||||
@@ -5235,7 +5531,11 @@ async function loadGroupConversations(groupId) {
|
||||
list.innerHTML = '';
|
||||
|
||||
if (groupConvs.length === 0) {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">该分组暂无对话</div>';
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">未找到匹配的对话</div>';
|
||||
} else {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">该分组暂无对话</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5631,9 +5931,94 @@ function closeGroupContextMenu() {
|
||||
}
|
||||
|
||||
|
||||
// 在分组中搜索(占位函数)
|
||||
function searchInGroup() {
|
||||
alert('搜索功能待实现');
|
||||
// 分组搜索相关变量
|
||||
let groupSearchTimer = null;
|
||||
let currentGroupSearchQuery = '';
|
||||
|
||||
// 切换分组搜索框显示/隐藏
|
||||
function toggleGroupSearch() {
|
||||
const searchContainer = document.getElementById('group-search-container');
|
||||
const searchInput = document.getElementById('group-search-input');
|
||||
|
||||
if (!searchContainer || !searchInput) return;
|
||||
|
||||
if (searchContainer.style.display === 'none') {
|
||||
searchContainer.style.display = 'block';
|
||||
searchInput.focus();
|
||||
} else {
|
||||
searchContainer.style.display = 'none';
|
||||
clearGroupSearch();
|
||||
}
|
||||
}
|
||||
|
||||
// 处理分组搜索输入
|
||||
function handleGroupSearchInput(event) {
|
||||
// 支持回车键搜索
|
||||
if (event.key === 'Enter') {
|
||||
event.preventDefault();
|
||||
performGroupSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
// 支持ESC键关闭搜索
|
||||
if (event.key === 'Escape') {
|
||||
clearGroupSearch();
|
||||
toggleGroupSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('group-search-input');
|
||||
const clearBtn = document.getElementById('group-search-clear-btn');
|
||||
|
||||
if (!searchInput) return;
|
||||
|
||||
const query = searchInput.value.trim();
|
||||
|
||||
// 显示/隐藏清除按钮
|
||||
if (clearBtn) {
|
||||
clearBtn.style.display = query ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// 防抖搜索
|
||||
if (groupSearchTimer) {
|
||||
clearTimeout(groupSearchTimer);
|
||||
}
|
||||
|
||||
groupSearchTimer = setTimeout(() => {
|
||||
performGroupSearch();
|
||||
}, 300); // 300ms 防抖
|
||||
}
|
||||
|
||||
// 执行分组搜索
|
||||
async function performGroupSearch() {
|
||||
const searchInput = document.getElementById('group-search-input');
|
||||
if (!searchInput || !currentGroupId) return;
|
||||
|
||||
const query = searchInput.value.trim();
|
||||
currentGroupSearchQuery = query;
|
||||
|
||||
// 加载搜索结果
|
||||
await loadGroupConversations(currentGroupId, query);
|
||||
}
|
||||
|
||||
// 清除分组搜索
|
||||
function clearGroupSearch() {
|
||||
const searchInput = document.getElementById('group-search-input');
|
||||
const clearBtn = document.getElementById('group-search-clear-btn');
|
||||
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
if (clearBtn) {
|
||||
clearBtn.style.display = 'none';
|
||||
}
|
||||
|
||||
currentGroupSearchQuery = '';
|
||||
|
||||
// 重新加载分组对话(不搜索)
|
||||
if (currentGroupId) {
|
||||
loadGroupConversations(currentGroupId, '');
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化时加载分组
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+24
-1
@@ -8,7 +8,7 @@ function initRouter() {
|
||||
if (hash) {
|
||||
const hashParts = hash.split('?');
|
||||
const pageId = hashParts[0];
|
||||
if (pageId && ['chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'settings', 'tasks'].includes(pageId)) {
|
||||
if (pageId && ['chat', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'settings', 'tasks'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
|
||||
// 如果是chat页面且带有conversation参数,加载对应对话
|
||||
@@ -94,6 +94,19 @@ function updateNavState(pageId) {
|
||||
knowledgeItem.classList.add('expanded');
|
||||
}
|
||||
|
||||
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
|
||||
if (submenuItem) {
|
||||
submenuItem.classList.add('active');
|
||||
}
|
||||
} else if (pageId === 'roles-management') {
|
||||
// 角色子菜单项
|
||||
const rolesItem = document.querySelector('.nav-item[data-page="roles"]');
|
||||
if (rolesItem) {
|
||||
rolesItem.classList.add('active');
|
||||
// 展开角色子菜单
|
||||
rolesItem.classList.add('expanded');
|
||||
}
|
||||
|
||||
const submenuItem = document.querySelector(`.nav-submenu-item[data-page="${pageId}"]`);
|
||||
if (submenuItem) {
|
||||
submenuItem.classList.add('active');
|
||||
@@ -239,6 +252,16 @@ function initPage(pageId) {
|
||||
loadConfig(false);
|
||||
}
|
||||
break;
|
||||
case 'roles-management':
|
||||
// 初始化角色管理页面
|
||||
if (typeof loadRoles === 'function') {
|
||||
loadRoles().then(() => {
|
||||
if (typeof renderRolesList === 'function') {
|
||||
renderRolesList();
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// 清理其他页面的定时器
|
||||
|
||||
+75
-39
@@ -2,8 +2,18 @@
|
||||
let currentConfig = null;
|
||||
let allTools = [];
|
||||
// 全局工具状态映射,用于保存用户在所有页面的修改
|
||||
// key: tool.name, value: { enabled: boolean, is_external: boolean, external_mcp: string }
|
||||
// key: 唯一工具标识符(toolKey),value: { enabled: boolean, is_external: boolean, external_mcp: string }
|
||||
let toolStateMap = new Map();
|
||||
|
||||
// 生成工具的唯一标识符,用于区分同名但来源不同的工具
|
||||
function getToolKey(tool) {
|
||||
// 如果是外部工具,使用 external_mcp::tool.name 作为唯一标识
|
||||
// 如果是内部工具,使用 tool.name 作为标识
|
||||
if (tool.is_external && tool.external_mcp) {
|
||||
return `${tool.external_mcp}::${tool.name}`;
|
||||
}
|
||||
return tool.name;
|
||||
}
|
||||
// 从localStorage读取每页显示数量,默认为20
|
||||
const getToolsPageSize = () => {
|
||||
const saved = localStorage.getItem('toolsPageSize');
|
||||
@@ -199,11 +209,13 @@ async function loadToolsList(page = 1, searchKeyword = '') {
|
||||
|
||||
// 初始化工具状态映射(如果工具不在映射中,使用服务器返回的状态)
|
||||
allTools.forEach(tool => {
|
||||
if (!toolStateMap.has(tool.name)) {
|
||||
toolStateMap.set(tool.name, {
|
||||
const toolKey = getToolKey(tool);
|
||||
if (!toolStateMap.has(toolKey)) {
|
||||
toolStateMap.set(toolKey, {
|
||||
enabled: tool.enabled,
|
||||
is_external: tool.is_external || false,
|
||||
external_mcp: tool.external_mcp || ''
|
||||
external_mcp: tool.external_mcp || '',
|
||||
name: tool.name // 保存原始工具名称
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -223,14 +235,16 @@ async function loadToolsList(page = 1, searchKeyword = '') {
|
||||
function saveCurrentPageToolStates() {
|
||||
document.querySelectorAll('#tools-list .tool-item').forEach(item => {
|
||||
const checkbox = item.querySelector('input[type="checkbox"]');
|
||||
const toolKey = item.dataset.toolKey; // 使用唯一标识符
|
||||
const toolName = item.dataset.toolName;
|
||||
const isExternal = item.dataset.isExternal === 'true';
|
||||
const externalMcp = item.dataset.externalMcp || '';
|
||||
if (toolName && checkbox) {
|
||||
toolStateMap.set(toolName, {
|
||||
if (toolKey && checkbox) {
|
||||
toolStateMap.set(toolKey, {
|
||||
enabled: checkbox.checked,
|
||||
is_external: isExternal,
|
||||
external_mcp: externalMcp
|
||||
external_mcp: externalMcp,
|
||||
name: toolName // 保存原始工具名称
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -283,14 +297,16 @@ function renderToolsList() {
|
||||
}
|
||||
|
||||
allTools.forEach(tool => {
|
||||
const toolKey = getToolKey(tool); // 生成唯一标识符
|
||||
const toolItem = document.createElement('div');
|
||||
toolItem.className = 'tool-item';
|
||||
toolItem.dataset.toolKey = toolKey; // 保存唯一标识符
|
||||
toolItem.dataset.toolName = tool.name; // 保存原始工具名称
|
||||
toolItem.dataset.isExternal = tool.is_external ? 'true' : 'false';
|
||||
toolItem.dataset.externalMcp = tool.external_mcp || '';
|
||||
|
||||
// 从全局状态映射获取工具状态,如果不存在则使用服务器返回的状态
|
||||
const toolState = toolStateMap.get(tool.name) || {
|
||||
const toolState = toolStateMap.get(toolKey) || {
|
||||
enabled: tool.enabled,
|
||||
is_external: tool.is_external || false,
|
||||
external_mcp: tool.external_mcp || ''
|
||||
@@ -298,15 +314,18 @@ function renderToolsList() {
|
||||
|
||||
// 外部工具标签,显示来源信息
|
||||
let externalBadge = '';
|
||||
if (toolState.is_external) {
|
||||
const externalMcpName = toolState.external_mcp || '';
|
||||
if (toolState.is_external || tool.is_external) {
|
||||
const externalMcpName = toolState.external_mcp || tool.external_mcp || '';
|
||||
const badgeText = externalMcpName ? `外部 (${escapeHtml(externalMcpName)})` : '外部';
|
||||
const badgeTitle = externalMcpName ? `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}` : '外部MCP工具';
|
||||
externalBadge = `<span class="external-tool-badge" title="${badgeTitle}">${badgeText}</span>`;
|
||||
}
|
||||
|
||||
// 生成唯一的checkbox id,使用工具唯一标识符
|
||||
const checkboxId = `tool-${escapeHtml(toolKey).replace(/::/g, '--')}`;
|
||||
|
||||
toolItem.innerHTML = `
|
||||
<input type="checkbox" id="tool-${tool.name}" ${toolState.enabled ? 'checked' : ''} ${toolState.is_external ? 'data-external="true"' : ''} onchange="handleToolCheckboxChange('${tool.name}', this.checked)" />
|
||||
<input type="checkbox" id="${checkboxId}" ${toolState.enabled ? 'checked' : ''} ${toolState.is_external || tool.is_external ? 'data-external="true"' : ''} onchange="handleToolCheckboxChange('${escapeHtml(toolKey)}', this.checked)" />
|
||||
<div class="tool-item-info">
|
||||
<div class="tool-item-name">
|
||||
${escapeHtml(tool.name)}
|
||||
@@ -376,16 +395,18 @@ function renderToolsPagination() {
|
||||
}
|
||||
|
||||
// 处理工具checkbox状态变化
|
||||
function handleToolCheckboxChange(toolName, enabled) {
|
||||
function handleToolCheckboxChange(toolKey, enabled) {
|
||||
// 更新全局状态映射
|
||||
const toolItem = document.querySelector(`.tool-item[data-tool-name="${toolName}"]`);
|
||||
const toolItem = document.querySelector(`.tool-item[data-tool-key="${toolKey}"]`);
|
||||
if (toolItem) {
|
||||
const toolName = toolItem.dataset.toolName;
|
||||
const isExternal = toolItem.dataset.isExternal === 'true';
|
||||
const externalMcp = toolItem.dataset.externalMcp || '';
|
||||
toolStateMap.set(toolName, {
|
||||
toolStateMap.set(toolKey, {
|
||||
enabled: enabled,
|
||||
is_external: isExternal,
|
||||
external_mcp: externalMcp
|
||||
external_mcp: externalMcp,
|
||||
name: toolName // 保存原始工具名称
|
||||
});
|
||||
}
|
||||
updateToolsStats();
|
||||
@@ -398,14 +419,16 @@ function selectAllTools() {
|
||||
// 更新全局状态映射
|
||||
const toolItem = checkbox.closest('.tool-item');
|
||||
if (toolItem) {
|
||||
const toolKey = toolItem.dataset.toolKey;
|
||||
const toolName = toolItem.dataset.toolName;
|
||||
const isExternal = toolItem.dataset.isExternal === 'true';
|
||||
const externalMcp = toolItem.dataset.externalMcp || '';
|
||||
if (toolName) {
|
||||
toolStateMap.set(toolName, {
|
||||
if (toolKey) {
|
||||
toolStateMap.set(toolKey, {
|
||||
enabled: true,
|
||||
is_external: isExternal,
|
||||
external_mcp: externalMcp
|
||||
external_mcp: externalMcp,
|
||||
name: toolName // 保存原始工具名称
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -420,14 +443,16 @@ function deselectAllTools() {
|
||||
// 更新全局状态映射
|
||||
const toolItem = checkbox.closest('.tool-item');
|
||||
if (toolItem) {
|
||||
const toolKey = toolItem.dataset.toolKey;
|
||||
const toolName = toolItem.dataset.toolName;
|
||||
const isExternal = toolItem.dataset.isExternal === 'true';
|
||||
const externalMcp = toolItem.dataset.externalMcp || '';
|
||||
if (toolName) {
|
||||
toolStateMap.set(toolName, {
|
||||
if (toolKey) {
|
||||
toolStateMap.set(toolKey, {
|
||||
enabled: false,
|
||||
is_external: isExternal,
|
||||
external_mcp: externalMcp
|
||||
external_mcp: externalMcp,
|
||||
name: toolName // 保存原始工具名称
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -484,11 +509,13 @@ async function updateToolsStats() {
|
||||
totalTools = allTools.length;
|
||||
totalEnabled = allTools.filter(tool => {
|
||||
// 优先使用全局状态映射,否则使用checkbox状态,最后使用服务器返回的状态
|
||||
const savedState = toolStateMap.get(tool.name);
|
||||
const toolKey = getToolKey(tool);
|
||||
const savedState = toolStateMap.get(toolKey);
|
||||
if (savedState !== undefined) {
|
||||
return savedState.enabled;
|
||||
}
|
||||
const checkbox = document.getElementById(`tool-${tool.name}`);
|
||||
const checkboxId = `tool-${toolKey.replace(/::/g, '--')}`;
|
||||
const checkbox = document.getElementById(checkboxId);
|
||||
return checkbox ? checkbox.checked : tool.enabled;
|
||||
}).length;
|
||||
} else {
|
||||
@@ -498,16 +525,18 @@ async function updateToolsStats() {
|
||||
|
||||
// 从当前页的checkbox获取状态(如果全局映射中没有)
|
||||
allTools.forEach(tool => {
|
||||
const savedState = toolStateMap.get(tool.name);
|
||||
const toolKey = getToolKey(tool);
|
||||
const savedState = toolStateMap.get(toolKey);
|
||||
if (savedState !== undefined) {
|
||||
localStateMap.set(tool.name, savedState.enabled);
|
||||
localStateMap.set(toolKey, savedState.enabled);
|
||||
} else {
|
||||
const checkbox = document.getElementById(`tool-${tool.name}`);
|
||||
const checkboxId = `tool-${toolKey.replace(/::/g, '--')}`;
|
||||
const checkbox = document.getElementById(checkboxId);
|
||||
if (checkbox) {
|
||||
localStateMap.set(tool.name, checkbox.checked);
|
||||
localStateMap.set(toolKey, checkbox.checked);
|
||||
} else {
|
||||
// 如果checkbox不存在(不在当前页),使用工具原始状态
|
||||
localStateMap.set(tool.name, tool.enabled);
|
||||
localStateMap.set(toolKey, tool.enabled);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -527,9 +556,10 @@ async function updateToolsStats() {
|
||||
const pageResult = await pageResponse.json();
|
||||
pageResult.tools.forEach(tool => {
|
||||
// 优先使用全局状态映射,否则使用服务器返回的状态
|
||||
if (!localStateMap.has(tool.name)) {
|
||||
const savedState = toolStateMap.get(tool.name);
|
||||
localStateMap.set(tool.name, savedState ? savedState.enabled : tool.enabled);
|
||||
const toolKey = getToolKey(tool);
|
||||
if (!localStateMap.has(toolKey)) {
|
||||
const savedState = toolStateMap.get(toolKey);
|
||||
localStateMap.set(toolKey, savedState ? savedState.enabled : tool.enabled);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -665,8 +695,9 @@ async function applySettings() {
|
||||
// 将工具添加到映射中
|
||||
// 优先使用全局状态映射中的状态(用户修改过的),否则使用服务器返回的状态
|
||||
pageResult.tools.forEach(tool => {
|
||||
const savedState = toolStateMap.get(tool.name);
|
||||
allToolsMap.set(tool.name, {
|
||||
const toolKey = getToolKey(tool);
|
||||
const savedState = toolStateMap.get(toolKey);
|
||||
allToolsMap.set(toolKey, {
|
||||
name: tool.name,
|
||||
enabled: savedState ? savedState.enabled : tool.enabled,
|
||||
is_external: savedState ? savedState.is_external : (tool.is_external || false),
|
||||
@@ -683,7 +714,7 @@ async function applySettings() {
|
||||
}
|
||||
|
||||
// 将所有工具添加到配置中
|
||||
allToolsMap.forEach(tool => {
|
||||
allToolsMap.forEach((tool, toolKey) => {
|
||||
config.tools.push({
|
||||
name: tool.name,
|
||||
enabled: tool.enabled,
|
||||
@@ -694,7 +725,9 @@ async function applySettings() {
|
||||
} catch (error) {
|
||||
console.warn('获取所有工具列表失败,仅使用全局状态映射', error);
|
||||
// 如果获取失败,使用全局状态映射
|
||||
toolStateMap.forEach((toolData, toolName) => {
|
||||
toolStateMap.forEach((toolData, toolKey) => {
|
||||
// toolData.name 保存了原始工具名称
|
||||
const toolName = toolData.name || toolKey.split('::').pop();
|
||||
config.tools.push({
|
||||
name: toolName,
|
||||
enabled: toolData.enabled,
|
||||
@@ -777,8 +810,9 @@ async function saveToolsConfig() {
|
||||
|
||||
// 将工具添加到映射中
|
||||
pageResult.tools.forEach(tool => {
|
||||
const savedState = toolStateMap.get(tool.name);
|
||||
allToolsMap.set(tool.name, {
|
||||
const toolKey = getToolKey(tool);
|
||||
const savedState = toolStateMap.get(toolKey);
|
||||
allToolsMap.set(toolKey, {
|
||||
name: tool.name,
|
||||
enabled: savedState ? savedState.enabled : tool.enabled,
|
||||
is_external: savedState ? savedState.is_external : (tool.is_external || false),
|
||||
@@ -795,7 +829,7 @@ async function saveToolsConfig() {
|
||||
}
|
||||
|
||||
// 将所有工具添加到配置中
|
||||
allToolsMap.forEach(tool => {
|
||||
allToolsMap.forEach((tool, toolKey) => {
|
||||
config.tools.push({
|
||||
name: tool.name,
|
||||
enabled: tool.enabled,
|
||||
@@ -806,7 +840,9 @@ async function saveToolsConfig() {
|
||||
} catch (error) {
|
||||
console.warn('获取所有工具列表失败,仅使用全局状态映射', error);
|
||||
// 如果获取失败,使用全局状态映射
|
||||
toolStateMap.forEach((toolData, toolName) => {
|
||||
toolStateMap.forEach((toolData, toolKey) => {
|
||||
// toolData.name 保存了原始工具名称
|
||||
const toolName = toolData.name || toolKey.split('::').pop();
|
||||
config.tools.push({
|
||||
name: toolName,
|
||||
enabled: toolData.enabled,
|
||||
|
||||
+134
-5
@@ -716,23 +716,56 @@ const batchQueuesState = {
|
||||
totalPages: 1
|
||||
};
|
||||
|
||||
// 显示批量导入模态框
|
||||
function showBatchImportModal() {
|
||||
// 显示新建任务模态框
|
||||
async function showBatchImportModal() {
|
||||
const modal = document.getElementById('batch-import-modal');
|
||||
const input = document.getElementById('batch-tasks-input');
|
||||
const titleInput = document.getElementById('batch-queue-title');
|
||||
const roleSelect = document.getElementById('batch-queue-role');
|
||||
if (modal && input) {
|
||||
input.value = '';
|
||||
if (titleInput) {
|
||||
titleInput.value = '';
|
||||
}
|
||||
// 重置角色选择为默认
|
||||
if (roleSelect) {
|
||||
roleSelect.value = '';
|
||||
}
|
||||
updateBatchImportStats('');
|
||||
|
||||
// 加载并填充角色列表
|
||||
if (roleSelect && typeof loadRoles === 'function') {
|
||||
try {
|
||||
const loadedRoles = await loadRoles();
|
||||
// 清空现有选项(除了默认选项)
|
||||
roleSelect.innerHTML = '<option value="">默认</option>';
|
||||
|
||||
// 添加已启用的角色
|
||||
const sortedRoles = loadedRoles.sort((a, b) => {
|
||||
if (a.name === '默认') return -1;
|
||||
if (b.name === '默认') return 1;
|
||||
return (a.name || '').localeCompare(b.name || '', 'zh-CN');
|
||||
});
|
||||
|
||||
sortedRoles.forEach(role => {
|
||||
if (role.name !== '默认' && role.enabled !== false) {
|
||||
const option = document.createElement('option');
|
||||
option.value = role.name;
|
||||
option.textContent = role.name;
|
||||
roleSelect.appendChild(option);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('加载角色列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
modal.style.display = 'block';
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭批量导入模态框
|
||||
// 关闭新建任务模态框
|
||||
function closeBatchImportModal() {
|
||||
const modal = document.getElementById('batch-import-modal');
|
||||
if (modal) {
|
||||
@@ -740,7 +773,7 @@ function closeBatchImportModal() {
|
||||
}
|
||||
}
|
||||
|
||||
// 更新批量导入统计
|
||||
// 更新新建任务统计
|
||||
function updateBatchImportStats(text) {
|
||||
const statsEl = document.getElementById('batch-import-stats');
|
||||
if (!statsEl) return;
|
||||
@@ -770,6 +803,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
async function createBatchQueue() {
|
||||
const input = document.getElementById('batch-tasks-input');
|
||||
const titleInput = document.getElementById('batch-queue-title');
|
||||
const roleSelect = document.getElementById('batch-queue-role');
|
||||
if (!input) return;
|
||||
|
||||
const text = input.value.trim();
|
||||
@@ -788,13 +822,16 @@ async function createBatchQueue() {
|
||||
// 获取标题(可选)
|
||||
const title = titleInput ? titleInput.value.trim() : '';
|
||||
|
||||
// 获取角色(可选,空字符串表示默认角色)
|
||||
const role = roleSelect ? roleSelect.value || '' : '';
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/api/batch-tasks', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ title, tasks }),
|
||||
body: JSON.stringify({ title, tasks, role }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -816,6 +853,34 @@ async function createBatchQueue() {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色图标(辅助函数)
|
||||
function getRoleIconForDisplay(roleName, rolesList) {
|
||||
if (!roleName || roleName === '') {
|
||||
return '🔵'; // 默认角色图标
|
||||
}
|
||||
|
||||
if (Array.isArray(rolesList) && rolesList.length > 0) {
|
||||
const role = rolesList.find(r => r.name === roleName);
|
||||
if (role && role.icon) {
|
||||
let icon = role.icon;
|
||||
// 检查是否是 Unicode 转义格式(可能包含引号)
|
||||
const unicodeMatch = icon.match(/^"?\\U([0-9A-F]{8})"?$/i);
|
||||
if (unicodeMatch) {
|
||||
try {
|
||||
const codePoint = parseInt(unicodeMatch[1], 16);
|
||||
icon = String.fromCodePoint(codePoint);
|
||||
} catch (e) {
|
||||
// 转换失败,使用默认图标
|
||||
console.warn('转换 icon Unicode 转义失败:', icon, e);
|
||||
return '👤';
|
||||
}
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
}
|
||||
return '👤'; // 默认图标
|
||||
}
|
||||
|
||||
// 加载批量任务队列列表
|
||||
async function loadBatchQueues(page) {
|
||||
const section = document.getElementById('batch-queues-section');
|
||||
@@ -826,6 +891,17 @@ async function loadBatchQueues(page) {
|
||||
batchQueuesState.currentPage = page;
|
||||
}
|
||||
|
||||
// 加载角色列表(用于显示正确的角色图标)
|
||||
let loadedRoles = [];
|
||||
if (typeof loadRoles === 'function') {
|
||||
try {
|
||||
loadedRoles = await loadRoles();
|
||||
} catch (error) {
|
||||
console.warn('加载角色列表失败,将使用默认图标:', error);
|
||||
}
|
||||
}
|
||||
batchQueuesState.loadedRoles = loadedRoles; // 保存到状态中供渲染使用
|
||||
|
||||
// 构建查询参数
|
||||
const params = new URLSearchParams();
|
||||
params.append('page', batchQueuesState.currentPage.toString());
|
||||
@@ -933,11 +1009,18 @@ function renderBatchQueues() {
|
||||
|
||||
const titleDisplay = queue.title ? `<span class="batch-queue-title" style="font-weight: 600; color: var(--text-primary); margin-right: 8px;">${escapeHtml(queue.title)}</span>` : '';
|
||||
|
||||
// 显示角色信息(使用正确的角色图标)
|
||||
const loadedRoles = batchQueuesState.loadedRoles || [];
|
||||
const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles);
|
||||
const roleName = queue.role && queue.role !== '' ? queue.role : '默认';
|
||||
const roleDisplay = `<span class="batch-queue-role" style="margin-right: 8px;" title="角色: ${escapeHtml(roleName)}">${roleIcon} ${escapeHtml(roleName)}</span>`;
|
||||
|
||||
return `
|
||||
<div class="batch-queue-item" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
|
||||
<div class="batch-queue-header">
|
||||
<div class="batch-queue-info" style="flex: 1;">
|
||||
${titleDisplay}
|
||||
${roleDisplay}
|
||||
<span class="batch-queue-status ${status.class}">${status.text}</span>
|
||||
<span class="batch-queue-id">队列ID: ${escapeHtml(queue.id)}</span>
|
||||
<span class="batch-queue-time">创建时间: ${new Date(queue.createdAt).toLocaleString('zh-CN')}</span>
|
||||
@@ -1110,6 +1193,16 @@ async function showBatchQueueDetail(queueId) {
|
||||
if (!modal || !content) return;
|
||||
|
||||
try {
|
||||
// 加载角色列表(如果还未加载)
|
||||
let loadedRoles = [];
|
||||
if (typeof loadRoles === 'function') {
|
||||
try {
|
||||
loadedRoles = await loadRoles();
|
||||
} catch (error) {
|
||||
console.warn('加载角色列表失败,将使用默认图标:', error);
|
||||
}
|
||||
}
|
||||
|
||||
const response = await apiFetch(`/api/batch-tasks/${queueId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取队列详情失败');
|
||||
@@ -1164,12 +1257,48 @@ async function showBatchQueueDetail(queueId) {
|
||||
'cancelled': { text: '已取消', class: 'batch-task-status-cancelled' }
|
||||
};
|
||||
|
||||
// 获取角色信息(如果队列有角色配置)
|
||||
let roleDisplay = '';
|
||||
if (queue.role && queue.role !== '') {
|
||||
// 如果有角色配置,尝试获取角色详细信息
|
||||
let roleName = queue.role;
|
||||
let roleIcon = '👤';
|
||||
// 从已加载的角色列表中查找角色图标
|
||||
if (Array.isArray(loadedRoles) && loadedRoles.length > 0) {
|
||||
const role = loadedRoles.find(r => r.name === roleName);
|
||||
if (role && role.icon) {
|
||||
let icon = role.icon;
|
||||
const unicodeMatch = icon.match(/^"?\\U([0-9A-F]{8})"?$/i);
|
||||
if (unicodeMatch) {
|
||||
try {
|
||||
const codePoint = parseInt(unicodeMatch[1], 16);
|
||||
icon = String.fromCodePoint(codePoint);
|
||||
} catch (e) {
|
||||
// 转换失败,使用默认图标
|
||||
}
|
||||
}
|
||||
roleIcon = icon;
|
||||
}
|
||||
}
|
||||
roleDisplay = `<div class="detail-item">
|
||||
<span class="detail-label">角色</span>
|
||||
<span class="detail-value">${roleIcon} ${escapeHtml(roleName)}</span>
|
||||
</div>`;
|
||||
} else {
|
||||
// 默认角色
|
||||
roleDisplay = `<div class="detail-item">
|
||||
<span class="detail-label">角色</span>
|
||||
<span class="detail-value">🔵 默认</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="batch-queue-detail-info">
|
||||
${queue.title ? `<div class="detail-item">
|
||||
<span class="detail-label">任务标题</span>
|
||||
<span class="detail-value">${escapeHtml(queue.title)}</span>
|
||||
</div>` : ''}
|
||||
${roleDisplay}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">队列ID</span>
|
||||
<span class="detail-value"><code>${escapeHtml(queue.id)}</code></span>
|
||||
|
||||
+210
-29
@@ -70,36 +70,34 @@
|
||||
<nav class="main-sidebar-nav">
|
||||
<div class="nav-item" data-page="chat">
|
||||
<div class="nav-item-content" data-title="对话" onclick="switchPage('chat')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
||||
</svg>
|
||||
<span>对话</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="tasks">
|
||||
<div class="nav-item-content" data-title="任务管理" onclick="switchPage('tasks')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<polyline points="13 2 13 9 20 9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="9" y1="13" x2="15" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="9" y1="17" x2="15" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 11l3 3L22 4"></path>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"></path>
|
||||
</svg>
|
||||
<span>任务管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="vulnerabilities">
|
||||
<div class="nav-item-content" data-title="漏洞管理" onclick="switchPage('vulnerabilities')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
|
||||
<path d="M9 12l2 2 4-4"></path>
|
||||
</svg>
|
||||
<span>漏洞管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item nav-item-has-submenu" data-page="mcp">
|
||||
<div class="nav-item-content" data-title="MCP" onclick="toggleSubmenu('mcp')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 12h4l3 8 4-16 3 8h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
|
||||
</svg>
|
||||
<span>MCP</span>
|
||||
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -117,10 +115,9 @@
|
||||
</div>
|
||||
<div class="nav-item nav-item-has-submenu" data-page="knowledge">
|
||||
<div class="nav-item-content" data-title="知识" onclick="toggleSubmenu('knowledge')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M10 7h6M10 11h6M10 15h4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
|
||||
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
|
||||
</svg>
|
||||
<span>知识</span>
|
||||
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
@@ -136,11 +133,30 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item nav-item-has-submenu" data-page="roles">
|
||||
<div class="nav-item-content" data-title="角色" onclick="toggleSubmenu('roles')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
|
||||
<circle cx="9" cy="7" r="4"></circle>
|
||||
<path d="M23 21v-2a4 4 0 0 0-3-3.87"></path>
|
||||
<path d="M16 3.13a4 4 0 0 1 0 7.75"></path>
|
||||
</svg>
|
||||
<span>角色</span>
|
||||
<svg class="submenu-arrow" width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="nav-submenu">
|
||||
<div class="nav-submenu-item" data-page="roles-management" onclick="switchPage('roles-management')">
|
||||
<span>角色管理</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="settings">
|
||||
<div class="nav-item-content" data-title="系统设置" onclick="switchPage('settings')">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"></circle>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||
</svg>
|
||||
<span>系统设置</span>
|
||||
</div>
|
||||
@@ -162,7 +178,7 @@
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<!-- 全局搜索 -->
|
||||
<div class="conversation-search-box" style="margin-bottom: 16px;">
|
||||
<div class="conversation-search-box" style="margin-bottom: 16px; margin-top: 16px;">
|
||||
<input type="text" id="conversation-search-input" placeholder="搜索历史记录..."
|
||||
oninput="handleConversationSearch(this.value)"
|
||||
onkeypress="if(event.key === 'Enter') handleConversationSearch(this.value)" />
|
||||
@@ -218,7 +234,7 @@
|
||||
</button>
|
||||
<h2 id="group-detail-title" class="group-detail-title"></h2>
|
||||
<div class="group-detail-actions">
|
||||
<button class="group-action-btn" onclick="searchInGroup()" title="搜索">
|
||||
<button class="group-action-btn" onclick="toggleGroupSearch()" title="搜索" id="group-search-toggle-btn">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
@@ -237,6 +253,17 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="group-search-container" class="group-search-container" style="display: none;">
|
||||
<div class="group-search-input-wrapper">
|
||||
<input type="text" id="group-search-input" class="group-search-input" placeholder="搜索分组中的对话..." onkeyup="handleGroupSearchInput(event)" oninput="handleGroupSearchInput(event)">
|
||||
<button class="group-search-clear-btn" onclick="clearGroupSearch()" title="清除搜索" id="group-search-clear-btn" style="display: none;">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="m8 8 8 8M16 8l-8 8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="group-detail-content">
|
||||
<div id="group-conversations-list" class="group-conversations-list"></div>
|
||||
</div>
|
||||
@@ -258,11 +285,37 @@
|
||||
<div id="active-tasks-bar" class="active-tasks-bar"></div>
|
||||
<div id="chat-messages" class="chat-messages"></div>
|
||||
<div class="chat-input-container">
|
||||
<div class="role-selector-wrapper">
|
||||
<button id="role-selector-btn" class="role-selector-btn" onclick="toggleRoleSelectionPanel()" title="选择角色">
|
||||
<span id="role-selector-icon" class="role-selector-icon">🔵</span>
|
||||
<span id="role-selector-text" class="role-selector-text">默认</span>
|
||||
<svg class="role-selector-arrow" width="10" height="10" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- 角色选择下拉面板 -->
|
||||
<div id="role-selection-panel" class="role-selection-panel" style="display: none;">
|
||||
<div class="role-selection-panel-header">
|
||||
<h3 class="role-selection-panel-title">选择角色</h3>
|
||||
<button class="role-selection-panel-close" onclick="closeRoleSelectionPanel()" 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" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="role-selection-list" class="role-selection-list-main"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-input-field">
|
||||
<textarea id="chat-input" placeholder="输入测试目标或命令... (输入 @ 选择工具 | Shift+Enter 换行,Enter 发送)" rows="1"></textarea>
|
||||
<div id="mention-suggestions" class="mention-suggestions" role="listbox" aria-label="工具提及候选"></div>
|
||||
</div>
|
||||
<button onclick="sendMessage()">发送</button>
|
||||
<button class="send-btn" onclick="sendMessage()">
|
||||
<span>发送</span>
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 12h14M12 5l7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -557,7 +610,7 @@
|
||||
<div class="page-header">
|
||||
<h2>任务管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" onclick="showBatchImportModal()">批量导入任务</button>
|
||||
<button class="btn-primary" onclick="showBatchImportModal()">新建任务</button>
|
||||
<label class="auto-refresh-toggle">
|
||||
<input type="checkbox" id="tasks-auto-refresh" checked onchange="toggleTasksAutoRefresh(this.checked)">
|
||||
<span>自动刷新</span>
|
||||
@@ -594,6 +647,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色管理页面 -->
|
||||
<div id="page-roles-management" class="page">
|
||||
<div class="page-header">
|
||||
<h2>角色管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-secondary" onclick="refreshRoles()">刷新</button>
|
||||
<button class="btn-primary" onclick="showAddRoleModal()">添加角色</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content">
|
||||
<div class="roles-controls">
|
||||
<div class="roles-stats-bar" id="roles-stats">
|
||||
<div class="role-stat-item">
|
||||
<span class="role-stat-label">总角色数</span>
|
||||
<span class="role-stat-value">-</span>
|
||||
</div>
|
||||
<div class="role-stat-item">
|
||||
<span class="role-stat-label">已启用</span>
|
||||
<span class="role-stat-value">-</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="roles-list" class="roles-list">
|
||||
<div class="loading-spinner">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统设置页面 -->
|
||||
<div id="page-settings" class="page">
|
||||
<div class="page-header">
|
||||
@@ -676,10 +757,6 @@
|
||||
<option value="openai">OpenAI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="knowledge-embedding-model">模型名称</label>
|
||||
<input type="text" id="knowledge-embedding-model" placeholder="text-embedding-v4" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="knowledge-embedding-base-url">Base URL</label>
|
||||
<input type="text" id="knowledge-embedding-base-url" placeholder="留空则使用OpenAI配置的base_url" />
|
||||
@@ -690,6 +767,10 @@
|
||||
<input type="password" id="knowledge-embedding-api-key" placeholder="留空则使用OpenAI配置的api_key" />
|
||||
<small class="form-hint">留空则使用OpenAI配置的api_key</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="knowledge-embedding-model">模型名称</label>
|
||||
<input type="text" id="knowledge-embedding-model" placeholder="text-embedding-v4" />
|
||||
</div>
|
||||
|
||||
<div class="settings-subsection-header">
|
||||
<h5>检索配置</h5>
|
||||
@@ -1156,11 +1237,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 批量导入任务模态框 -->
|
||||
<!-- 新建任务模态框 -->
|
||||
<div id="batch-import-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 800px;">
|
||||
<div class="modal-header">
|
||||
<h2>批量导入任务</h2>
|
||||
<h2>新建任务</h2>
|
||||
<span class="modal-close" onclick="closeBatchImportModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -1171,6 +1252,15 @@
|
||||
为批量任务队列设置一个标题,方便后续查找和管理。
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="batch-queue-role">角色</label>
|
||||
<select id="batch-queue-role" style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; font-size: 0.875rem;">
|
||||
<option value="">默认</option>
|
||||
</select>
|
||||
<div class="form-hint" style="margin-top: 4px;">
|
||||
选择一个角色,所有任务将使用该角色的配置(提示词和工具)执行。
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="batch-tasks-input">任务列表(每行一个任务)<span style="color: red;">*</span></label>
|
||||
<textarea id="batch-tasks-input" rows="15" placeholder="请输入任务列表,每行一个任务,例如: 扫描 192.168.1.1 的开放端口 检查 https://example.com 是否存在SQL注入 枚举 example.com 的子域名" style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea>
|
||||
@@ -1318,6 +1408,96 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色选择弹窗 -->
|
||||
<div id="role-select-modal" class="modal">
|
||||
<div class="modal-content role-select-modal-content">
|
||||
<div class="modal-header">
|
||||
<h2>选择角色</h2>
|
||||
<span class="modal-close" onclick="closeRoleSelectModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body role-select-body">
|
||||
<div id="role-select-list" class="role-select-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色编辑模态框 -->
|
||||
<div id="role-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 900px;">
|
||||
<div class="modal-header">
|
||||
<h2 id="role-modal-title">添加角色</h2>
|
||||
<span class="modal-close" onclick="closeRoleModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="role-name">角色名称 <span style="color: red;">*</span></label>
|
||||
<input type="text" id="role-name" placeholder="输入角色名称" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="role-description">角色描述</label>
|
||||
<input type="text" id="role-description" placeholder="输入角色描述" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="role-icon">角色图标</label>
|
||||
<input type="text" id="role-icon" placeholder="输入emoji图标,例如: 🏆" maxlength="10" />
|
||||
<small class="form-hint">输入一个emoji作为角色的图标,将显示在角色选择器中。</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="role-user-prompt">用户提示词</label>
|
||||
<textarea id="role-user-prompt" rows="10" placeholder="输入用户提示词,会在用户消息前追加此提示词..."></textarea>
|
||||
<small class="form-hint">此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。</small>
|
||||
</div>
|
||||
<div class="form-group" id="role-tools-section">
|
||||
<label>关联的工具(可选)</label>
|
||||
<div id="role-tools-default-hint" class="role-tools-default-hint" style="display: none;">
|
||||
<div class="role-tools-default-info">
|
||||
<span class="role-tools-default-icon">ℹ️</span>
|
||||
<div class="role-tools-default-content">
|
||||
<div class="role-tools-default-title">默认角色使用所有工具</div>
|
||||
<div class="role-tools-default-desc">默认角色会自动使用MCP管理中启用的所有工具,无需单独配置。</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="role-tools-controls">
|
||||
<div class="role-tools-actions">
|
||||
<button type="button" class="btn-secondary" onclick="selectAllRoleTools()">全选</button>
|
||||
<button type="button" class="btn-secondary" onclick="deselectAllRoleTools()">全不选</button>
|
||||
<div class="role-tools-search-box">
|
||||
<input type="text" id="role-tools-search" placeholder="搜索工具..."
|
||||
oninput="searchRoleTools(this.value)"
|
||||
onkeypress="if(event.key === 'Enter') searchRoleTools(this.value)" />
|
||||
<button class="role-tools-search-clear" id="role-tools-search-clear"
|
||||
onclick="clearRoleToolsSearch()" style="display: none;" title="清除搜索">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="role-tools-stats" class="role-tools-stats"></div>
|
||||
</div>
|
||||
<div id="role-tools-list" class="role-tools-list">
|
||||
<div class="tools-loading">正在加载工具列表...</div>
|
||||
</div>
|
||||
<small class="form-hint">勾选要关联的工具,留空则使用MCP管理中的全部工具配置。</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="role-enabled" class="modern-checkbox" checked />
|
||||
<span class="checkbox-custom"></span>
|
||||
<span class="checkbox-text">启用此角色</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeRoleModal()">取消</button>
|
||||
<button class="btn-primary" onclick="saveRole()">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/static/js/builtin-tools.js"></script>
|
||||
<script src="/static/js/auth.js"></script>
|
||||
<script src="/static/js/router.js"></script>
|
||||
<script src="/static/js/monitor.js"></script>
|
||||
@@ -1326,6 +1506,7 @@
|
||||
<script src="/static/js/knowledge.js"></script>
|
||||
<script src="/static/js/vulnerability.js?v=4"></script>
|
||||
<script src="/static/js/tasks.js"></script>
|
||||
<script src="/static/js/roles.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user