mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 13:43:31 +02:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 797b10b176 | |||
| 0809be60fa | |||
| 62a83f6271 | |||
| b4da3e5d33 | |||
| 4b1023ff6c | |||
| 82ca5225ae | |||
| 5e8fef0ad4 | |||
| 226f9b79e2 | |||
| 7222466cff | |||
| 1630c2b2c4 | |||
| f7ffa1d5d3 | |||
| e4cd68df41 | |||
| d24f797552 | |||
| 0a89ac31c3 | |||
| 379fc8767d | |||
| 8bdab678fa | |||
| cc555af8dd | |||
| 643e0e7adf | |||
| eb27eaff7d | |||
| fc542a48f3 | |||
| dd7d15845c | |||
| ee9559e074 | |||
| 872e570518 | |||
| a5ffafba77 | |||
| 3da7f77e1c | |||
| 26ad9646be | |||
| 959a97870b | |||
| c8bbfcd171 | |||
| 5f2862b629 | |||
| ee6c4b6f19 | |||
| 55b8decbaa | |||
| 1222adc485 | |||
| 38972bf93b | |||
| 127a5dd5c3 | |||
| f5f73d41c0 | |||
| 9811209002 |
@@ -1,78 +0,0 @@
|
||||
---
|
||||
name: 🐛 Bug / 异常问题反馈
|
||||
about: 报告一个 Bug 或异常问题
|
||||
title: '[BUG] '
|
||||
labels: ['bug', '待确认']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 📋 问题描述
|
||||
<!-- 请清晰、简洁地描述遇到的问题 -->
|
||||
|
||||
|
||||
## 🔄 复现步骤
|
||||
<!-- 请详细描述如何复现这个问题 -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## ✅ 期望行为
|
||||
<!-- 描述你期望的正确行为是什么 -->
|
||||
|
||||
|
||||
## ❌ 实际行为
|
||||
<!-- 描述实际发生了什么 -->
|
||||
|
||||
|
||||
## 📸 截图/录屏
|
||||
<!--
|
||||
⚠️ 重要:请提供完整的截图或录屏,确保包含:
|
||||
- 完整的错误信息
|
||||
- 相关的界面元素
|
||||
- 浏览器控制台错误(如有)
|
||||
- 终端输出(如有)
|
||||
|
||||
如果截图不完整,issue 可能会被关闭。
|
||||
-->
|
||||
|
||||
<!-- 请在此处拖拽或粘贴截图 -->
|
||||
|
||||
|
||||
## 📝 报错日志(脱敏后)
|
||||
<!--
|
||||
⚠️ 重要:请提供完整的、脱敏后的报错日志。
|
||||
|
||||
脱敏要求:
|
||||
- 移除所有敏感信息(API Key、密码、Token、真实IP地址、域名等)
|
||||
- 使用占位符替换,如:`sk-xxx`、`password: ***`、`192.168.x.x`、`example.com`
|
||||
- 保留完整的错误堆栈信息
|
||||
- 保留时间戳和日志级别
|
||||
|
||||
请从以下位置收集日志:
|
||||
1. MCP状态监控 页面
|
||||
2. 服务器终端输出
|
||||
3. 日志文件(如果配置了文件输出)
|
||||
4. 浏览器控制台(F12 → Console)
|
||||
-->
|
||||
|
||||
```
|
||||
请在此处粘贴脱敏后的完整报错日志
|
||||
```
|
||||
|
||||
|
||||
## ✅ 检查清单
|
||||
<!-- 提交前请确认以下项目 -->
|
||||
|
||||
- [ ] 我已阅读并理解项目的 Issue 规范
|
||||
- [ ] 我已提供完整的、脱敏后的报错日志
|
||||
- [ ] 我已提供完整的截图(如适用)
|
||||
- [ ] 我已提供详细的复现步骤
|
||||
- [ ] 我已填写所有必要的环境信息
|
||||
- [ ] 我已脱敏所有敏感信息(API Key、密码、IP 等)
|
||||
- [ ] 我已确认这不是重复的 issue
|
||||
|
||||
---
|
||||
|
||||
**注意**:如果缺少必要的日志或截图,此 issue 可能会被标记为 `需要更多信息` 或直接关闭。请确保提供完整的信息以便我们能够快速定位和解决问题。
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
name: ✨ 功能优化建议
|
||||
about: 提出新功能或优化建议
|
||||
title: '[FEATURE] '
|
||||
labels: ['enhancement', '待讨论']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 💡 功能描述
|
||||
<!-- 请清晰、简洁地描述你希望添加或优化的功能 -->
|
||||
|
||||
|
||||
## 🎯 使用场景
|
||||
<!-- 描述这个功能的使用场景,解决什么问题 -->
|
||||
<!-- 例如:在什么情况下会用到这个功能?它如何改善用户体验? -->
|
||||
|
||||
|
||||
## 🔄 当前行为
|
||||
<!-- 描述当前系统是如何处理相关需求的,或者为什么需要这个功能 -->
|
||||
|
||||
|
||||
## ✨ 期望行为
|
||||
<!-- 详细描述你期望的新功能或优化后的行为 -->
|
||||
|
||||
|
||||
## 📸 参考示例(如有)
|
||||
<!--
|
||||
如果有其他项目的类似功能实现,可以在此提供截图或链接作为参考
|
||||
⚠️ 请确保截图完整,包含所有相关界面元素
|
||||
-->
|
||||
|
||||
<!-- 请在此处拖拽或粘贴参考截图 -->
|
||||
|
||||
|
||||
## 🛠️ 实现建议(可选)
|
||||
<!-- 如果你有具体的实现思路或技术建议,可以在此描述 -->
|
||||
|
||||
|
||||
## 📊 优先级评估
|
||||
<!-- 请选择你认为的优先级 -->
|
||||
- [ ] 🔴 高优先级(严重影响使用体验或功能缺失)
|
||||
- [ ] 🟡 中优先级(能显著改善体验)
|
||||
- [ ] 🟢 低优先级(锦上添花的功能)
|
||||
|
||||
## 🔍 相关功能
|
||||
<!-- 这个功能是否与现有功能相关? -->
|
||||
<!-- 例如:是否与工具管理、攻击链分析、知识库等功能相关? -->
|
||||
|
||||
|
||||
## 📝 额外信息
|
||||
<!-- 任何其他有助于理解需求的信息 -->
|
||||
- 是否已有替代方案?
|
||||
- 这个功能是否会影响现有功能?
|
||||
- 是否有相关的其他 issue 或讨论?
|
||||
|
||||
## ✅ 检查清单
|
||||
<!-- 提交前请确认以下项目 -->
|
||||
|
||||
- [ ] 我已清晰描述了功能需求和使用场景
|
||||
- [ ] 我已提供完整的参考截图(如有)
|
||||
- [ ] 我已评估了功能的优先级
|
||||
- [ ] 我已确认这不是重复的 issue
|
||||
- [ ] 我已考虑了对现有功能的影响
|
||||
|
||||
---
|
||||
|
||||
**注意**:请提供尽可能详细的信息,包括使用场景、参考示例等,这将有助于我们更好地理解和实现你的需求。
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 Ed1s0nZ
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -65,6 +65,14 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
|
||||
<img src="./images/role-management.png" alt="Role Management" width="100%">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="33.33%" align="center">
|
||||
<strong>WebShell Management</strong><br/>
|
||||
<img src="./images/webshell-management.png" alt="WebShell Management" width="100%">
|
||||
</td>
|
||||
<td width="33.33%" align="center"></td>
|
||||
<td width="33.33%" align="center"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
@@ -84,6 +92,7 @@ CyberStrikeAI is an **AI-native security testing platform** built in Go. It inte
|
||||
- 🎭 Role-based testing: predefined security testing roles (Penetration Testing, CTF, Web App Scanning, etc.) with custom prompts and tool restrictions
|
||||
- 🎯 Skills system: 20+ predefined security testing skills (SQL injection, XSS, API security, etc.) that can be attached to roles or called on-demand by AI agents
|
||||
- 📱 **Chatbot**: DingTalk and Lark (Feishu) long-lived connections so you can talk to CyberStrikeAI from mobile (see [Robot / Chatbot guide](docs/robot_en.md) for setup and commands)
|
||||
- 🐚 **WebShell management**: Add and manage WebShell connections (e.g. IceSword/AntSword compatible), use a virtual terminal for command execution, a built-in file manager for file operations, and an AI assistant tab that orchestrates tests and keeps per-connection conversation history; supports PHP, ASP, ASPX, JSP and custom shell types with configurable request method and command parameter.
|
||||
|
||||
## Tool Overview
|
||||
|
||||
@@ -169,6 +178,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
- **Conversation groups** – Organize conversations into groups, pin important groups, rename or delete groups via context menu.
|
||||
- **Vulnerability management** – Create, update, and track vulnerabilities discovered during testing. Filter by severity (critical/high/medium/low/info), status (open/confirmed/fixed/false_positive), and conversation. View statistics and export findings.
|
||||
- **Batch task management** – Create task queues with multiple tasks, add or edit tasks before execution, and run them sequentially. Each task executes as a separate conversation, with status tracking (pending/running/completed/failed/cancelled) and full execution history.
|
||||
- **WebShell management** – Add and manage WebShell connections (PHP/ASP/ASPX/JSP or custom). Use the virtual terminal to run commands, the file manager to list, read, edit, upload, and delete files, and the AI assistant tab to drive scripted tests with per-connection conversation history. Connections are stored in SQLite; supports GET/POST and configurable command parameter (e.g. IceSword/AntSword style).
|
||||
- **Settings** – Tweak provider keys, MCP enablement, tool toggles, and agent iteration limits.
|
||||
|
||||
### Built-in Safeguards
|
||||
@@ -235,6 +245,14 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
- The web UI renders the chain as an interactive graph with severity scoring and step replay.
|
||||
- Export the chain or raw findings to external reporting pipelines.
|
||||
|
||||
### WebShell Management
|
||||
- **Connections** – From the Web UI, go to **WebShell Management** to add, edit, or delete WebShell connections. Each connection stores: Shell URL, password/key, shell type (PHP, ASP, ASPX, JSP, Custom), request method (GET/POST), command parameter name (default `cmd`), and an optional remark; all records persist in SQLite and are compatible with common clients such as IceSword and AntSword.
|
||||
- **Virtual terminal** – After selecting a connection, use the **Virtual terminal** tab to run arbitrary commands with history and quick commands (whoami/id/ls/pwd etc.). Output is streamed in the browser, and Ctrl+L clears the screen.
|
||||
- **File manager** – Use the **File manager** tab to list directories, read or edit files, delete files, create folders/files, upload files (including chunked uploads for large files), rename paths, and download selected files. Path navigation supports breadcrumbs, parent directory jumps, and name filtering.
|
||||
- **AI assistant** – Use the **AI assistant** tab to chat with an agent that understands the current WebShell connection, automatically runs tools and shell commands, and maintains per-connection conversation history with a sidebar of previous sessions.
|
||||
- **Connectivity test** – Use **Test connectivity** to verify that the shell URL, password, and command parameter are correct before running commands (sends a lightweight `echo 1` check).
|
||||
- **Persistence** – All WebShell connections and AI conversations are stored in SQLite (same database as conversations), so they persist across restarts.
|
||||
|
||||
### MCP Everywhere
|
||||
- **Web mode** – ships with HTTP MCP server automatically consumed by the UI.
|
||||
- **MCP stdio mode** – `go run cmd/mcp-stdio/main.go` exposes the agent to Cursor/CLI.
|
||||
@@ -389,6 +407,7 @@ A test SSE MCP server is available at `cmd/test-sse-mcp-server/` for validation
|
||||
- **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.
|
||||
- **WebShell APIs** – manage WebShell connections and execute commands via `/api/webshell/connections` (GET list, POST create, PUT update, DELETE delete) and `/api/webshell/exec` (command execution), `/api/webshell/fileop` (list/read/write/delete files).
|
||||
- **Task control** – pause/resume/stop long scans, re-run steps with new params, or stream transcripts.
|
||||
- **Audit & security** – rotate passwords via `/api/auth/change-password`, enforce short-lived sessions, and restrict MCP ports at the network layer when exposing the service.
|
||||
|
||||
@@ -532,6 +551,13 @@ CyberStrikeAI has joined [404Starlink](https://github.com/knownsec/404StarLink)
|
||||

|
||||
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
|
||||
CyberStrikeAI is licensed under the Apache License 2.0.
|
||||
See the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ Disclaimer
|
||||
|
||||
@@ -64,6 +64,14 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
||||
<img src="./images/role-management.png" alt="角色管理" width="100%">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="33.33%" align="center">
|
||||
<strong>WebShell 管理</strong><br/>
|
||||
<img src="./images/webshell-management.png" alt="WebShell 管理" width="100%">
|
||||
</td>
|
||||
<td width="33.33%" align="center"></td>
|
||||
<td width="33.33%" align="center"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
@@ -83,6 +91,7 @@ CyberStrikeAI 是一款 **AI 原生安全测试平台**,基于 Go 构建,集
|
||||
- 🎭 角色化测试:预设安全测试角色(渗透测试、CTF、Web 应用扫描等),支持自定义提示词和工具限制
|
||||
- 🎯 Skills 技能系统:20+ 预设安全测试技能(SQL 注入、XSS、API 安全等),可附加到角色或由 AI 按需调用
|
||||
- 📱 **机器人**:支持钉钉、飞书长连接,在手机端与 CyberStrikeAI 对话(配置与命令详见 [机器人使用说明](docs/robot.md))
|
||||
- 🐚 **WebShell 管理**:添加与管理 WebShell 连接(兼容冰蝎/蚁剑等),通过虚拟终端执行命令、内置文件管理进行文件操作,并提供按连接维度保存历史的 AI 助手标签页;支持 PHP/ASP/ASPX/JSP 及自定义类型,可配置请求方法与命令参数。
|
||||
|
||||
## 工具概览
|
||||
|
||||
@@ -168,6 +177,7 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
- **对话分组**:将对话按项目或主题组织到不同分组,支持置顶、重命名、删除等操作,所有数据持久化存储。
|
||||
- **漏洞管理**:在测试过程中创建、更新和跟踪发现的漏洞。支持按严重程度(严重/高/中/低/信息)、状态(待确认/已确认/已修复/误报)和对话进行过滤,查看统计信息并导出发现。
|
||||
- **批量任务管理**:创建任务队列,批量添加多个任务,执行前可编辑或删除任务,然后依次顺序执行。每个任务会作为独立对话执行,支持完整的状态跟踪(待执行/执行中/已完成/失败/已取消)和执行历史。
|
||||
- **WebShell 管理**:添加并管理 WebShell 连接(PHP/ASP/ASPX/JSP 或自定义类型)。使用虚拟终端执行命令(带命令历史与快捷命令),使用文件管理浏览、读取、编辑、上传与删除目标文件,并支持按路径导航和名称过滤。连接信息持久化存储于 SQLite,支持 GET/POST 及可配置命令参数(兼容冰蝎/蚁剑等)。
|
||||
- **可视化配置**:在界面中切换模型、启停工具、设置迭代次数等。
|
||||
|
||||
### 默认安全措施
|
||||
@@ -233,6 +243,14 @@ go build -o cyberstrike-ai cmd/server/main.go
|
||||
- 智能体解析每次对话,抽取目标、工具、漏洞与因果关系。
|
||||
- Web 端可交互式查看链路节点、风险级别及时间轴,支持导出报告。
|
||||
|
||||
### WebShell 管理
|
||||
- **连接管理**:在 Web 界面进入 **WebShell 管理**,可添加、编辑或删除 WebShell 连接。每条连接包含:Shell 地址、密码/密钥、Shell 类型(PHP/ASP/ASPX/JSP/自定义)、请求方式(GET/POST)、命令参数名(默认 `cmd`)、备注等信息,并持久化存储在 SQLite,兼容冰蝎、蚁剑等常见客户端。
|
||||
- **虚拟终端**:选择连接后,在 **虚拟终端** 标签页中执行任意命令,支持命令历史与常用快捷命令(whoami/id/ls/pwd 等),输出在浏览器中实时显示,支持 Ctrl+L 清屏。
|
||||
- **文件管理**:在 **文件管理** 标签页中可列出目录、读取/编辑文件、删除文件、新建文件/目录、上传文件(大文件分片上传)、重命名路径以及下载勾选文件,并支持面包屑导航与名称过滤。
|
||||
- **AI 助手**:在 **AI 助手** 标签页中与智能体对话,由系统自动结合当前 WebShell 连接执行工具与命令,侧边栏展示该连接下的所有历史会话,支持多轮追踪与查看。
|
||||
- **连通性测试**:使用 **测试连通性** 可在执行命令前通过一次 `echo 1` 调用校验 Shell 地址、密码与命令参数是否正确。
|
||||
- **持久化**:所有 WebShell 连接与相关 AI 会话均保存在 SQLite(与对话共用数据库),服务重启后仍可继续使用。
|
||||
|
||||
### MCP 全场景
|
||||
- **Web 模式**:自带 HTTP MCP 服务供前端调用。
|
||||
- **MCP stdio 模式**:`go run cmd/mcp-stdio/main.go` 可接入 Cursor/命令行。
|
||||
@@ -388,6 +406,7 @@ CyberStrikeAI 支持通过三种传输模式连接外部 MCP 服务器:
|
||||
- **角色管理 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`(删除任务)。任务依次顺序执行,每个任务创建独立对话,支持完整状态跟踪。
|
||||
- **WebShell API**:通过 `/api/webshell/connections`(GET 列表、POST 创建、PUT 更新、DELETE 删除)及 `/api/webshell/exec`(执行命令)、`/api/webshell/fileop`(列出/读取/写入/删除文件)管理 WebShell 连接与执行操作。
|
||||
- **任务控制**:支持暂停/终止长任务、修改参数后重跑、流式获取日志。
|
||||
- **安全管理**:`/api/auth/change-password` 可即时轮换口令;建议在暴露 MCP 端口时配合网络层 ACL。
|
||||
|
||||
@@ -531,6 +550,13 @@ CyberStrikeAI 现已加入 [404星链计划](https://github.com/knownsec/404Star
|
||||
|
||||
---
|
||||
|
||||
## 许可证
|
||||
|
||||
CyberStrikeAI 采用 **Apache License 2.0** 开源许可。
|
||||
完整条款见仓库根目录 [LICENSE](LICENSE) 文件。
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 免责声明
|
||||
|
||||
**本工具仅供教育和授权测试使用!**
|
||||
|
||||
+4
-3
@@ -10,7 +10,7 @@
|
||||
# ============================================
|
||||
|
||||
# 前端显示的版本号(可选,不填则显示默认版本)
|
||||
version: "v1.3.22"
|
||||
version: "v1.3.25"
|
||||
|
||||
# 服务器配置
|
||||
server:
|
||||
@@ -59,8 +59,9 @@ fofa:
|
||||
agent:
|
||||
max_iterations: 120 # 最大迭代次数,AI 代理最多执行多少轮工具调用
|
||||
large_result_threshold: 102400 # 大结果阈值(字节),默认50KB,超过此大小会自动保存到存储
|
||||
result_storage_dir: tmp # 结果存储目录,大结果会保存在此目录下
|
||||
|
||||
result_storage_dir: tmp # 结果存储目录,大结果会保存在此目录下
|
||||
tool_timeout_minutes: 30 # 单次工具执行最大时长(分钟),超时自动终止;0 表示不限制(不推荐,易出现长时间挂起)
|
||||
|
||||
# 数据库配置
|
||||
database:
|
||||
path: data/conversations.db # SQLite 数据库文件路径,用于存储对话历史和消息
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 451 KiB |
+28
-5
@@ -3,6 +3,7 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -1234,6 +1235,18 @@ func (a *Agent) executeToolViaMCP(ctx context.Context, toolName string, args map
|
||||
var executionID string
|
||||
var err error
|
||||
|
||||
// 单次工具执行超时:防止单个工具长时间挂起(如 30 分钟仍显示执行中)
|
||||
toolCtx := ctx
|
||||
var toolCancel context.CancelFunc
|
||||
if a.agentConfig != nil && a.agentConfig.ToolTimeoutMinutes > 0 {
|
||||
toolCtx, toolCancel = context.WithTimeout(ctx, time.Duration(a.agentConfig.ToolTimeoutMinutes)*time.Minute)
|
||||
defer func() {
|
||||
if toolCancel != nil {
|
||||
toolCancel()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// 检查是否是外部MCP工具(通过工具名称映射)
|
||||
a.mu.RLock()
|
||||
originalToolName, isExternalTool := a.toolNameMapping[toolName]
|
||||
@@ -1245,29 +1258,39 @@ func (a *Agent) executeToolViaMCP(ctx context.Context, toolName string, args map
|
||||
zap.String("openAIName", toolName),
|
||||
zap.String("originalName", originalToolName),
|
||||
)
|
||||
result, executionID, err = a.externalMCPMgr.CallTool(ctx, originalToolName, args)
|
||||
result, executionID, err = a.externalMCPMgr.CallTool(toolCtx, originalToolName, args)
|
||||
} else {
|
||||
// 调用内部MCP工具
|
||||
result, executionID, err = a.mcpServer.CallTool(ctx, toolName, args)
|
||||
result, executionID, err = a.mcpServer.CallTool(toolCtx, toolName, args)
|
||||
}
|
||||
|
||||
// 如果调用失败(如工具不存在),返回友好的错误信息而不是抛出异常
|
||||
// 如果调用失败(如工具不存在、超时),返回友好的错误信息而不是抛出异常
|
||||
if err != nil {
|
||||
detail := err.Error()
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
min := 10
|
||||
if a.agentConfig != nil && a.agentConfig.ToolTimeoutMinutes > 0 {
|
||||
min = a.agentConfig.ToolTimeoutMinutes
|
||||
}
|
||||
detail = fmt.Sprintf("工具执行超过 %d 分钟被自动终止(可在 config.yaml 的 agent.tool_timeout_minutes 中调整)", min)
|
||||
}
|
||||
errorMsg := fmt.Sprintf(`工具调用失败
|
||||
|
||||
工具名称: %s
|
||||
错误类型: 系统错误
|
||||
错误详情: %v
|
||||
错误详情: %s
|
||||
|
||||
可能的原因:
|
||||
- 工具 "%s" 不存在或未启用
|
||||
- 单次执行超时(agent.tool_timeout_minutes)
|
||||
- 系统配置问题
|
||||
- 网络或权限问题
|
||||
|
||||
建议:
|
||||
- 检查工具名称是否正确
|
||||
- 若需更长执行时间,可适当增大 agent.tool_timeout_minutes
|
||||
- 尝试使用其他替代工具
|
||||
- 如果这是必需的工具,请向用户说明情况`, toolName, err, toolName)
|
||||
- 如果这是必需的工具,请向用户说明情况`, toolName, detail, toolName)
|
||||
|
||||
return &ToolExecutionResult{
|
||||
Result: errorMsg,
|
||||
|
||||
+175
-2
@@ -15,11 +15,11 @@ import (
|
||||
"cyberstrike-ai/internal/database"
|
||||
"cyberstrike-ai/internal/handler"
|
||||
"cyberstrike-ai/internal/knowledge"
|
||||
"cyberstrike-ai/internal/robot"
|
||||
"cyberstrike-ai/internal/logger"
|
||||
"cyberstrike-ai/internal/mcp"
|
||||
"cyberstrike-ai/internal/mcp/builtin"
|
||||
"cyberstrike-ai/internal/openai"
|
||||
"cyberstrike-ai/internal/robot"
|
||||
"cyberstrike-ai/internal/security"
|
||||
"cyberstrike-ai/internal/skills"
|
||||
"cyberstrike-ai/internal/storage"
|
||||
@@ -46,7 +46,7 @@ type App struct {
|
||||
knowledgeHandler *handler.KnowledgeHandler // 知识库处理器(用于动态初始化)
|
||||
agentHandler *handler.AgentHandler // Agent处理器(用于更新知识库管理器)
|
||||
robotHandler *handler.RobotHandler // 机器人处理器(钉钉/飞书/企业微信)
|
||||
robotMu sync.Mutex // 保护钉钉/飞书长连接的 cancel
|
||||
robotMu sync.Mutex // 保护钉钉/飞书长连接的 cancel
|
||||
dingCancel context.CancelFunc // 钉钉 Stream 取消函数,用于配置变更时重启
|
||||
larkCancel context.CancelFunc // 飞书长连接取消函数,用于配置变更时重启
|
||||
}
|
||||
@@ -319,6 +319,8 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
authHandler := handler.NewAuthHandler(authManager, cfg, configPath, log.Logger)
|
||||
attackChainHandler := handler.NewAttackChainHandler(db, &cfg.OpenAI, log.Logger)
|
||||
vulnerabilityHandler := handler.NewVulnerabilityHandler(db, log.Logger)
|
||||
webshellHandler := handler.NewWebShellHandler(log.Logger, db)
|
||||
registerWebshellTools(mcpServer, db, webshellHandler, 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)
|
||||
@@ -364,6 +366,13 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
}
|
||||
configHandler.SetVulnerabilityToolRegistrar(vulnerabilityRegistrar)
|
||||
|
||||
// 设置 WebShell 工具注册器(ApplyConfig 时重新注册)
|
||||
webshellRegistrar := func() error {
|
||||
registerWebshellTools(mcpServer, db, webshellHandler, log.Logger)
|
||||
return nil
|
||||
}
|
||||
configHandler.SetWebshellToolRegistrar(webshellRegistrar)
|
||||
|
||||
// 设置Skills工具注册器(内置工具,必须设置)
|
||||
skillsRegistrar := func() error {
|
||||
// 创建一个适配器,将database.DB适配为SkillStatsStorage接口
|
||||
@@ -429,6 +438,7 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
|
||||
attackChainHandler,
|
||||
app, // 传递 App 实例以便动态获取 knowledgeHandler
|
||||
vulnerabilityHandler,
|
||||
webshellHandler,
|
||||
roleHandler,
|
||||
skillsHandler,
|
||||
fofaHandler,
|
||||
@@ -556,6 +566,7 @@ func setupRoutes(
|
||||
attackChainHandler *handler.AttackChainHandler,
|
||||
app *App, // 传递 App 实例以便动态获取 knowledgeHandler
|
||||
vulnerabilityHandler *handler.VulnerabilityHandler,
|
||||
webshellHandler *handler.WebShellHandler,
|
||||
roleHandler *handler.RoleHandler,
|
||||
skillsHandler *handler.SkillsHandler,
|
||||
fofaHandler *handler.FofaHandler,
|
||||
@@ -817,6 +828,16 @@ func setupRoutes(
|
||||
protected.PUT("/vulnerabilities/:id", vulnerabilityHandler.UpdateVulnerability)
|
||||
protected.DELETE("/vulnerabilities/:id", vulnerabilityHandler.DeleteVulnerability)
|
||||
|
||||
// WebShell 管理(代理执行 + 连接配置存 SQLite)
|
||||
protected.GET("/webshell/connections", webshellHandler.ListConnections)
|
||||
protected.POST("/webshell/connections", webshellHandler.CreateConnection)
|
||||
protected.GET("/webshell/connections/:id/ai-history", webshellHandler.GetAIHistory)
|
||||
protected.GET("/webshell/connections/:id/ai-conversations", webshellHandler.ListAIConversations)
|
||||
protected.PUT("/webshell/connections/:id", webshellHandler.UpdateConnection)
|
||||
protected.DELETE("/webshell/connections/:id", webshellHandler.DeleteConnection)
|
||||
protected.POST("/webshell/exec", webshellHandler.Exec)
|
||||
protected.POST("/webshell/file", webshellHandler.FileOp)
|
||||
|
||||
// 角色管理
|
||||
protected.GET("/roles", roleHandler.GetRoles)
|
||||
protected.GET("/roles/:name", roleHandler.GetRole)
|
||||
@@ -1056,6 +1077,158 @@ func registerVulnerabilityTool(mcpServer *mcp.Server, db *database.DB, logger *z
|
||||
logger.Info("漏洞记录工具注册成功")
|
||||
}
|
||||
|
||||
// registerWebshellTools 注册 WebShell 相关 MCP 工具,供 AI 助手在指定连接上执行命令与文件操作
|
||||
func registerWebshellTools(mcpServer *mcp.Server, db *database.DB, webshellHandler *handler.WebShellHandler, logger *zap.Logger) {
|
||||
if db == nil || webshellHandler == nil {
|
||||
logger.Warn("跳过 WebShell 工具注册:db 或 webshellHandler 为空")
|
||||
return
|
||||
}
|
||||
|
||||
// webshell_exec
|
||||
execTool := mcp.Tool{
|
||||
Name: builtin.ToolWebshellExec,
|
||||
Description: "在指定的 WebShell 连接上执行一条系统命令,返回命令的标准输出。connection_id 由用户在 AI 助手上下文中选定。",
|
||||
ShortDescription: "在 WebShell 连接上执行命令",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"connection_id": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "WebShell 连接 ID(如 ws_xxx)",
|
||||
},
|
||||
"command": map[string]interface{}{
|
||||
"type": "string",
|
||||
"description": "要执行的系统命令",
|
||||
},
|
||||
},
|
||||
"required": []string{"connection_id", "command"},
|
||||
},
|
||||
}
|
||||
execHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
|
||||
cid, _ := args["connection_id"].(string)
|
||||
cmd, _ := args["command"].(string)
|
||||
if cid == "" || cmd == "" {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "connection_id 和 command 均为必填"}}, IsError: true}, nil
|
||||
}
|
||||
conn, err := db.GetWebshellConnection(cid)
|
||||
if err != nil || conn == nil {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "未找到该 WebShell 连接或查询失败"}}, IsError: true}, nil
|
||||
}
|
||||
output, ok, errMsg := webshellHandler.ExecWithConnection(conn, cmd)
|
||||
if errMsg != "" {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: errMsg}}, IsError: true}, nil
|
||||
}
|
||||
if !ok {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "HTTP 非 200,输出:\n" + output}}, IsError: false}, nil
|
||||
}
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: output}}, IsError: false}, nil
|
||||
}
|
||||
mcpServer.RegisterTool(execTool, execHandler)
|
||||
|
||||
// webshell_file_list
|
||||
listTool := mcp.Tool{
|
||||
Name: builtin.ToolWebshellFileList,
|
||||
Description: "在指定 WebShell 连接上列出目录内容。path 默认为当前目录(.)。",
|
||||
ShortDescription: "在 WebShell 上列出目录",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"connection_id": map[string]interface{}{"type": "string", "description": "WebShell 连接 ID"},
|
||||
"path": map[string]interface{}{"type": "string", "description": "目录路径,默认 ."},
|
||||
},
|
||||
"required": []string{"connection_id"},
|
||||
},
|
||||
}
|
||||
listHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
|
||||
cid, _ := args["connection_id"].(string)
|
||||
path, _ := args["path"].(string)
|
||||
if cid == "" {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "connection_id 必填"}}, IsError: true}, nil
|
||||
}
|
||||
conn, err := db.GetWebshellConnection(cid)
|
||||
if err != nil || conn == nil {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "未找到该 WebShell 连接"}}, IsError: true}, nil
|
||||
}
|
||||
output, ok, errMsg := webshellHandler.FileOpWithConnection(conn, "list", path, "", "")
|
||||
if errMsg != "" {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: errMsg}}, IsError: true}, nil
|
||||
}
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: output}}, IsError: !ok}, nil
|
||||
}
|
||||
mcpServer.RegisterTool(listTool, listHandler)
|
||||
|
||||
// webshell_file_read
|
||||
readTool := mcp.Tool{
|
||||
Name: builtin.ToolWebshellFileRead,
|
||||
Description: "在指定 WebShell 连接上读取文件内容。",
|
||||
ShortDescription: "在 WebShell 上读取文件",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"connection_id": map[string]interface{}{"type": "string", "description": "WebShell 连接 ID"},
|
||||
"path": map[string]interface{}{"type": "string", "description": "文件路径"},
|
||||
},
|
||||
"required": []string{"connection_id", "path"},
|
||||
},
|
||||
}
|
||||
readHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
|
||||
cid, _ := args["connection_id"].(string)
|
||||
path, _ := args["path"].(string)
|
||||
if cid == "" || path == "" {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "connection_id 和 path 必填"}}, IsError: true}, nil
|
||||
}
|
||||
conn, err := db.GetWebshellConnection(cid)
|
||||
if err != nil || conn == nil {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "未找到该 WebShell 连接"}}, IsError: true}, nil
|
||||
}
|
||||
output, ok, errMsg := webshellHandler.FileOpWithConnection(conn, "read", path, "", "")
|
||||
if errMsg != "" {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: errMsg}}, IsError: true}, nil
|
||||
}
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: output}}, IsError: !ok}, nil
|
||||
}
|
||||
mcpServer.RegisterTool(readTool, readHandler)
|
||||
|
||||
// webshell_file_write
|
||||
writeTool := mcp.Tool{
|
||||
Name: builtin.ToolWebshellFileWrite,
|
||||
Description: "在指定 WebShell 连接上写入文件内容(会覆盖已有文件)。",
|
||||
ShortDescription: "在 WebShell 上写入文件",
|
||||
InputSchema: map[string]interface{}{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"connection_id": map[string]interface{}{"type": "string", "description": "WebShell 连接 ID"},
|
||||
"path": map[string]interface{}{"type": "string", "description": "文件路径"},
|
||||
"content": map[string]interface{}{"type": "string", "description": "要写入的内容"},
|
||||
},
|
||||
"required": []string{"connection_id", "path", "content"},
|
||||
},
|
||||
}
|
||||
writeHandler := func(ctx context.Context, args map[string]interface{}) (*mcp.ToolResult, error) {
|
||||
cid, _ := args["connection_id"].(string)
|
||||
path, _ := args["path"].(string)
|
||||
content, _ := args["content"].(string)
|
||||
if cid == "" || path == "" {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "connection_id 和 path 必填"}}, IsError: true}, nil
|
||||
}
|
||||
conn, err := db.GetWebshellConnection(cid)
|
||||
if err != nil || conn == nil {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "未找到该 WebShell 连接"}}, IsError: true}, nil
|
||||
}
|
||||
output, ok, errMsg := webshellHandler.FileOpWithConnection(conn, "write", path, content, "")
|
||||
if errMsg != "" {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: errMsg}}, IsError: true}, nil
|
||||
}
|
||||
if !ok {
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "写入可能失败,输出:\n" + output}}, IsError: false}, nil
|
||||
}
|
||||
return &mcp.ToolResult{Content: []mcp.Content{{Type: "text", Text: "写入成功\n" + output}}, IsError: false}, nil
|
||||
}
|
||||
mcpServer.RegisterTool(writeTool, writeHandler)
|
||||
|
||||
logger.Info("WebShell 工具注册成功")
|
||||
}
|
||||
|
||||
// initializeKnowledge 初始化知识库组件(用于动态初始化)
|
||||
func initializeKnowledge(
|
||||
cfg *config.Config,
|
||||
|
||||
+14
-11
@@ -112,6 +112,7 @@ type AgentConfig struct {
|
||||
MaxIterations int `yaml:"max_iterations" json:"max_iterations"`
|
||||
LargeResultThreshold int `yaml:"large_result_threshold" json:"large_result_threshold"` // 大结果阈值(字节),默认50KB
|
||||
ResultStorageDir string `yaml:"result_storage_dir" json:"result_storage_dir"` // 结果存储目录,默认tmp
|
||||
ToolTimeoutMinutes int `yaml:"tool_timeout_minutes" json:"tool_timeout_minutes"` // 单次工具执行最大时长(分钟),超时自动终止,防止长时间挂起;0 表示不限制(不推荐)
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
@@ -163,16 +164,17 @@ type ToolConfig struct {
|
||||
|
||||
// ParameterConfig 参数配置
|
||||
type ParameterConfig struct {
|
||||
Name string `yaml:"name"` // 参数名称
|
||||
Type string `yaml:"type"` // 参数类型: string, int, bool, array
|
||||
Description string `yaml:"description"` // 参数描述
|
||||
Required bool `yaml:"required,omitempty"` // 是否必需
|
||||
Default interface{} `yaml:"default,omitempty"` // 默认值
|
||||
Flag string `yaml:"flag,omitempty"` // 命令行标志,如 "-u", "--url", "-p"
|
||||
Position *int `yaml:"position,omitempty"` // 位置参数的位置(从0开始)
|
||||
Format string `yaml:"format,omitempty"` // 参数格式: "flag", "positional", "combined" (flag=value), "template"
|
||||
Template string `yaml:"template,omitempty"` // 模板字符串,如 "{flag} {value}" 或 "{value}"
|
||||
Options []string `yaml:"options,omitempty"` // 可选值列表(用于枚举)
|
||||
Name string `yaml:"name"` // 参数名称
|
||||
Type string `yaml:"type"` // 参数类型: string, int, bool, array
|
||||
Description string `yaml:"description"` // 参数描述
|
||||
Required bool `yaml:"required,omitempty"` // 是否必需
|
||||
Default interface{} `yaml:"default,omitempty"` // 默认值
|
||||
ItemType string `yaml:"item_type,omitempty"` // 当 type 为 array 时,数组元素类型,如 string, number, object
|
||||
Flag string `yaml:"flag,omitempty"` // 命令行标志,如 "-u", "--url", "-p"
|
||||
Position *int `yaml:"position,omitempty"` // 位置参数的位置(从0开始)
|
||||
Format string `yaml:"format,omitempty"` // 参数格式: "flag", "positional", "combined" (flag=value), "template"
|
||||
Template string `yaml:"template,omitempty"` // 模板字符串,如 "{flag} {value}" 或 "{value}"
|
||||
Options []string `yaml:"options,omitempty"` // 可选值列表(用于枚举)
|
||||
}
|
||||
|
||||
func Load(path string) (*Config, error) {
|
||||
@@ -681,7 +683,8 @@ func Default() *Config {
|
||||
MaxTotalTokens: 120000,
|
||||
},
|
||||
Agent: AgentConfig{
|
||||
MaxIterations: 30, // 默认最大迭代次数
|
||||
MaxIterations: 30, // 默认最大迭代次数
|
||||
ToolTimeoutMinutes: 10, // 单次工具执行默认最多 10 分钟,避免异常长时间占用
|
||||
},
|
||||
Security: SecurityConfig{
|
||||
Tools: []ToolConfig{}, // 工具配置应该从 config.yaml 或 tools/ 目录加载
|
||||
|
||||
@@ -33,13 +33,26 @@ type Message struct {
|
||||
|
||||
// CreateConversation 创建新对话
|
||||
func (db *DB) CreateConversation(title string) (*Conversation, error) {
|
||||
return db.CreateConversationWithWebshell("", title)
|
||||
}
|
||||
|
||||
// CreateConversationWithWebshell 创建新对话,可选绑定 WebShell 连接 ID(为空则普通对话)
|
||||
func (db *DB) CreateConversationWithWebshell(webshellConnectionID, title string) (*Conversation, error) {
|
||||
id := uuid.New().String()
|
||||
now := time.Now()
|
||||
|
||||
_, err := db.Exec(
|
||||
"INSERT INTO conversations (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)",
|
||||
id, title, now, now,
|
||||
)
|
||||
var err error
|
||||
if webshellConnectionID != "" {
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO conversations (id, title, created_at, updated_at, webshell_connection_id) VALUES (?, ?, ?, ?, ?)",
|
||||
id, title, now, now, webshellConnectionID,
|
||||
)
|
||||
} else {
|
||||
_, err = db.Exec(
|
||||
"INSERT INTO conversations (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)",
|
||||
id, title, now, now,
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建对话失败: %w", err)
|
||||
}
|
||||
@@ -52,6 +65,117 @@ func (db *DB) CreateConversation(title string) (*Conversation, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetConversationByWebshellConnectionID 根据 WebShell 连接 ID 获取该连接下最近一条对话(用于 AI 助手持久化)
|
||||
func (db *DB) GetConversationByWebshellConnectionID(connectionID string) (*Conversation, error) {
|
||||
if connectionID == "" {
|
||||
return nil, fmt.Errorf("connectionID is empty")
|
||||
}
|
||||
var conv Conversation
|
||||
var createdAt, updatedAt string
|
||||
var pinned int
|
||||
err := db.QueryRow(
|
||||
"SELECT id, title, pinned, created_at, updated_at FROM conversations WHERE webshell_connection_id = ? ORDER BY updated_at DESC LIMIT 1",
|
||||
connectionID,
|
||||
).Scan(&conv.ID, &conv.Title, &pinned, &createdAt, &updatedAt)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, fmt.Errorf("查询对话失败: %w", err)
|
||||
}
|
||||
conv.Pinned = pinned != 0
|
||||
if t, e := time.Parse("2006-01-02 15:04:05.999999999-07:00", createdAt); e == nil {
|
||||
conv.CreatedAt = t
|
||||
} else if t, e := time.Parse("2006-01-02 15:04:05", createdAt); e == nil {
|
||||
conv.CreatedAt = t
|
||||
} else {
|
||||
conv.CreatedAt, _ = time.Parse(time.RFC3339, createdAt)
|
||||
}
|
||||
if t, e := time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt); e == nil {
|
||||
conv.UpdatedAt = t
|
||||
} else if t, e := time.Parse("2006-01-02 15:04:05", updatedAt); e == nil {
|
||||
conv.UpdatedAt = t
|
||||
} else {
|
||||
conv.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
}
|
||||
messages, err := db.GetMessages(conv.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("加载消息失败: %w", err)
|
||||
}
|
||||
conv.Messages = messages
|
||||
|
||||
// 加载过程详情并附加到对应消息(与 GetConversation 一致,便于刷新后仍可查看执行过程)
|
||||
processDetailsMap, err := db.GetProcessDetailsByConversation(conv.ID)
|
||||
if err != nil {
|
||||
db.logger.Warn("加载过程详情失败", zap.Error(err))
|
||||
processDetailsMap = make(map[string][]ProcessDetail)
|
||||
}
|
||||
for i := range conv.Messages {
|
||||
if details, ok := processDetailsMap[conv.Messages[i].ID]; ok {
|
||||
detailsJSON := make([]map[string]interface{}, len(details))
|
||||
for j, detail := range details {
|
||||
var data interface{}
|
||||
if detail.Data != "" {
|
||||
if err := json.Unmarshal([]byte(detail.Data), &data); err != nil {
|
||||
db.logger.Warn("解析过程详情数据失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
detailsJSON[j] = map[string]interface{}{
|
||||
"id": detail.ID,
|
||||
"messageId": detail.MessageID,
|
||||
"conversationId": detail.ConversationID,
|
||||
"eventType": detail.EventType,
|
||||
"message": detail.Message,
|
||||
"data": data,
|
||||
"createdAt": detail.CreatedAt,
|
||||
}
|
||||
}
|
||||
conv.Messages[i].ProcessDetails = detailsJSON
|
||||
}
|
||||
}
|
||||
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
// WebShellConversationItem 用于侧边栏列表,不含消息
|
||||
type WebShellConversationItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// ListConversationsByWebshellConnectionID 列出该 WebShell 连接下的所有对话(按更新时间倒序),供侧边栏展示
|
||||
func (db *DB) ListConversationsByWebshellConnectionID(connectionID string) ([]WebShellConversationItem, error) {
|
||||
if connectionID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
rows, err := db.Query(
|
||||
"SELECT id, title, updated_at FROM conversations WHERE webshell_connection_id = ? ORDER BY updated_at DESC",
|
||||
connectionID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询对话列表失败: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var list []WebShellConversationItem
|
||||
for rows.Next() {
|
||||
var item WebShellConversationItem
|
||||
var updatedAt string
|
||||
if err := rows.Scan(&item.ID, &item.Title, &updatedAt); err != nil {
|
||||
continue
|
||||
}
|
||||
if t, e := time.Parse("2006-01-02 15:04:05.999999999-07:00", updatedAt); e == nil {
|
||||
item.UpdatedAt = t
|
||||
} else if t, e := time.Parse("2006-01-02 15:04:05", updatedAt); e == nil {
|
||||
item.UpdatedAt = t
|
||||
} else {
|
||||
item.UpdatedAt, _ = time.Parse(time.RFC3339, updatedAt)
|
||||
}
|
||||
list = append(list, item)
|
||||
}
|
||||
return list, rows.Err()
|
||||
}
|
||||
|
||||
// GetConversation 获取对话
|
||||
func (db *DB) GetConversation(id string) (*Conversation, error) {
|
||||
var conv Conversation
|
||||
|
||||
@@ -227,6 +227,19 @@ func (db *DB) initTables() error {
|
||||
FOREIGN KEY (queue_id) REFERENCES batch_task_queues(id) ON DELETE CASCADE
|
||||
);`
|
||||
|
||||
// 创建 WebShell 连接表
|
||||
createWebshellConnectionsTable := `
|
||||
CREATE TABLE IF NOT EXISTS webshell_connections (
|
||||
id TEXT PRIMARY KEY,
|
||||
url TEXT NOT NULL,
|
||||
password TEXT NOT NULL DEFAULT '',
|
||||
type TEXT NOT NULL DEFAULT 'php',
|
||||
method TEXT NOT NULL DEFAULT 'post',
|
||||
cmd_param TEXT NOT NULL DEFAULT '',
|
||||
remark TEXT NOT NULL DEFAULT '',
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
// 创建索引
|
||||
createIndexes := `
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_conversation_id ON messages(conversation_id);
|
||||
@@ -253,6 +266,7 @@ func (db *DB) initTables() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_batch_tasks_queue_id ON batch_tasks(queue_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_batch_task_queues_created_at ON batch_task_queues(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_batch_task_queues_title ON batch_task_queues(title);
|
||||
CREATE INDEX IF NOT EXISTS idx_webshell_connections_created_at ON webshell_connections(created_at);
|
||||
`
|
||||
|
||||
if _, err := db.Exec(createConversationsTable); err != nil {
|
||||
@@ -311,6 +325,10 @@ func (db *DB) initTables() error {
|
||||
return fmt.Errorf("创建batch_tasks表失败: %w", err)
|
||||
}
|
||||
|
||||
if _, err := db.Exec(createWebshellConnectionsTable); err != nil {
|
||||
return fmt.Errorf("创建webshell_connections表失败: %w", err)
|
||||
}
|
||||
|
||||
// 为已有表添加新字段(如果不存在)- 必须在创建索引之前
|
||||
if err := db.migrateConversationsTable(); err != nil {
|
||||
db.logger.Warn("迁移conversations表失败", zap.Error(err))
|
||||
@@ -397,6 +415,21 @@ func (db *DB) migrateConversationsTable() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查 webshell_connection_id 字段是否存在(WebShell AI 助手对话关联)
|
||||
err = db.QueryRow("SELECT COUNT(*) FROM pragma_table_info('conversations') WHERE name='webshell_connection_id'").Scan(&count)
|
||||
if err != nil {
|
||||
if _, addErr := db.Exec("ALTER TABLE conversations ADD COLUMN webshell_connection_id TEXT"); addErr != nil {
|
||||
errMsg := strings.ToLower(addErr.Error())
|
||||
if !strings.Contains(errMsg, "duplicate column") && !strings.Contains(errMsg, "already exists") {
|
||||
db.logger.Warn("添加webshell_connection_id字段失败", zap.Error(addErr))
|
||||
}
|
||||
}
|
||||
} else if count == 0 {
|
||||
if _, err := db.Exec("ALTER TABLE conversations ADD COLUMN webshell_connection_id TEXT"); err != nil {
|
||||
db.logger.Warn("添加webshell_connection_id字段失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// WebShellConnection WebShell 连接配置
|
||||
type WebShellConnection struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
Method string `json:"method"`
|
||||
CmdParam string `json:"cmdParam"`
|
||||
Remark string `json:"remark"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
// ListWebshellConnections 列出所有 WebShell 连接,按创建时间倒序
|
||||
func (db *DB) ListWebshellConnections() ([]WebShellConnection, error) {
|
||||
query := `
|
||||
SELECT id, url, password, type, method, cmd_param, remark, created_at
|
||||
FROM webshell_connections
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
rows, err := db.Query(query)
|
||||
if err != nil {
|
||||
db.logger.Error("查询 WebShell 连接列表失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var list []WebShellConnection
|
||||
for rows.Next() {
|
||||
var c WebShellConnection
|
||||
err := rows.Scan(&c.ID, &c.URL, &c.Password, &c.Type, &c.Method, &c.CmdParam, &c.Remark, &c.CreatedAt)
|
||||
if err != nil {
|
||||
db.logger.Warn("扫描 WebShell 连接行失败", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
list = append(list, c)
|
||||
}
|
||||
return list, rows.Err()
|
||||
}
|
||||
|
||||
// GetWebshellConnection 根据 ID 获取一条连接
|
||||
func (db *DB) GetWebshellConnection(id string) (*WebShellConnection, error) {
|
||||
query := `
|
||||
SELECT id, url, password, type, method, cmd_param, remark, created_at
|
||||
FROM webshell_connections WHERE id = ?
|
||||
`
|
||||
var c WebShellConnection
|
||||
err := db.QueryRow(query, id).Scan(&c.ID, &c.URL, &c.Password, &c.Type, &c.Method, &c.CmdParam, &c.Remark, &c.CreatedAt)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
db.logger.Error("查询 WebShell 连接失败", zap.Error(err), zap.String("id", id))
|
||||
return nil, err
|
||||
}
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
// CreateWebshellConnection 创建 WebShell 连接
|
||||
func (db *DB) CreateWebshellConnection(c *WebShellConnection) error {
|
||||
query := `
|
||||
INSERT INTO webshell_connections (id, url, password, type, method, cmd_param, remark, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`
|
||||
_, err := db.Exec(query, c.ID, c.URL, c.Password, c.Type, c.Method, c.CmdParam, c.Remark, c.CreatedAt)
|
||||
if err != nil {
|
||||
db.logger.Error("创建 WebShell 连接失败", zap.Error(err), zap.String("id", c.ID))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateWebshellConnection 更新 WebShell 连接
|
||||
func (db *DB) UpdateWebshellConnection(c *WebShellConnection) error {
|
||||
query := `
|
||||
UPDATE webshell_connections
|
||||
SET url = ?, password = ?, type = ?, method = ?, cmd_param = ?, remark = ?
|
||||
WHERE id = ?
|
||||
`
|
||||
result, err := db.Exec(query, c.URL, c.Password, c.Type, c.Method, c.CmdParam, c.Remark, c.ID)
|
||||
if err != nil {
|
||||
db.logger.Error("更新 WebShell 连接失败", zap.Error(err), zap.String("id", c.ID))
|
||||
return err
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteWebshellConnection 删除 WebShell 连接
|
||||
func (db *DB) DeleteWebshellConnection(id string) error {
|
||||
result, err := db.Exec(`DELETE FROM webshell_connections WHERE id = ?`, id)
|
||||
if err != nil {
|
||||
db.logger.Error("删除 WebShell 连接失败", zap.Error(err), zap.String("id", id))
|
||||
return err
|
||||
}
|
||||
affected, _ := result.RowsAffected()
|
||||
if affected == 0 {
|
||||
return sql.ErrNoRows
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+69
-19
@@ -121,10 +121,11 @@ type ChatAttachment struct {
|
||||
|
||||
// ChatRequest 聊天请求
|
||||
type ChatRequest struct {
|
||||
Message string `json:"message" binding:"required"`
|
||||
ConversationID string `json:"conversationId,omitempty"`
|
||||
Role string `json:"role,omitempty"` // 角色名称
|
||||
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
||||
Message string `json:"message" binding:"required"`
|
||||
ConversationID string `json:"conversationId,omitempty"`
|
||||
Role string `json:"role,omitempty"` // 角色名称
|
||||
Attachments []ChatAttachment `json:"attachments,omitempty"`
|
||||
WebShellConnectionID string `json:"webshellConnectionId,omitempty"` // WebShell 管理 - AI 助手:当前选中的连接 ID,仅使用 webshell_* 工具
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -316,7 +317,34 @@ func (h *AgentHandler) AgentLoop(c *gin.Context) {
|
||||
finalMessage := req.Message
|
||||
var roleTools []string // 角色配置的工具列表
|
||||
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
|
||||
if req.Role != "" && req.Role != "默认" {
|
||||
|
||||
// WebShell AI 助手模式:绑定当前连接,仅开放 webshell_* 工具并注入 connection_id
|
||||
if req.WebShellConnectionID != "" {
|
||||
conn, err := h.db.GetWebshellConnection(strings.TrimSpace(req.WebShellConnectionID))
|
||||
if err != nil || conn == nil {
|
||||
h.logger.Warn("WebShell AI 助手:未找到连接", zap.String("id", req.WebShellConnectionID), zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "未找到该 WebShell 连接"})
|
||||
return
|
||||
}
|
||||
remark := conn.Remark
|
||||
if remark == "" {
|
||||
remark = conn.URL
|
||||
}
|
||||
finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base、list_skills、read_skill。请根据用户输入决定下一步:若仅为问候、闲聊或简单问题,直接简短回复即可,不必调用工具;当用户明确需要执行命令、列目录、读写文件、记录漏洞或检索知识库/查看 Skills 等操作时再调用上述工具。\n\n用户请求:%s",
|
||||
conn.ID, remark, conn.ID, req.Message)
|
||||
roleTools = []string{
|
||||
builtin.ToolWebshellExec,
|
||||
builtin.ToolWebshellFileList,
|
||||
builtin.ToolWebshellFileRead,
|
||||
builtin.ToolWebshellFileWrite,
|
||||
builtin.ToolRecordVulnerability,
|
||||
builtin.ToolListKnowledgeRiskTypes,
|
||||
builtin.ToolSearchKnowledgeBase,
|
||||
builtin.ToolListSkills,
|
||||
builtin.ToolReadSkill,
|
||||
}
|
||||
roleSkills = nil
|
||||
} else if req.Role != "" && req.Role != "默认" {
|
||||
if h.config.Roles != nil {
|
||||
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
|
||||
// 应用用户提示词
|
||||
@@ -712,11 +740,17 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有对话ID,创建新对话
|
||||
// 如果没有对话ID,创建新对话(WebShell 助手模式下关联连接 ID 以便持久化展示)
|
||||
conversationID := req.ConversationID
|
||||
if conversationID == "" {
|
||||
title := safeTruncateString(req.Message, 50)
|
||||
conv, err := h.db.CreateConversation(title)
|
||||
var conv *database.Conversation
|
||||
var err error
|
||||
if req.WebShellConnectionID != "" {
|
||||
conv, err = h.db.CreateConversationWithWebshell(strings.TrimSpace(req.WebShellConnectionID), title)
|
||||
} else {
|
||||
conv, err = h.db.CreateConversation(title)
|
||||
}
|
||||
if err != nil {
|
||||
h.logger.Error("创建对话失败", zap.Error(err))
|
||||
sendEvent("error", "创建对话失败: "+err.Error(), nil)
|
||||
@@ -769,7 +803,32 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
// 应用角色用户提示词和工具配置
|
||||
finalMessage := req.Message
|
||||
var roleTools []string // 角色配置的工具列表
|
||||
if req.Role != "" && req.Role != "默认" {
|
||||
var roleSkills []string
|
||||
if req.WebShellConnectionID != "" {
|
||||
conn, errConn := h.db.GetWebshellConnection(strings.TrimSpace(req.WebShellConnectionID))
|
||||
if errConn != nil || conn == nil {
|
||||
h.logger.Warn("WebShell AI 助手:未找到连接", zap.String("id", req.WebShellConnectionID), zap.Error(errConn))
|
||||
sendEvent("error", "未找到该 WebShell 连接", nil)
|
||||
return
|
||||
}
|
||||
remark := conn.Remark
|
||||
if remark == "" {
|
||||
remark = conn.URL
|
||||
}
|
||||
finalMessage = fmt.Sprintf("[WebShell 助手上下文] 当前连接 ID:%s,备注:%s。可用工具(仅在该连接上操作时使用,connection_id 填 \"%s\"):webshell_exec、webshell_file_list、webshell_file_read、webshell_file_write、record_vulnerability、list_knowledge_risk_types、search_knowledge_base、list_skills、read_skill。请根据用户输入决定下一步:若仅为问候、闲聊或简单问题,直接简短回复即可,不必调用工具;当用户明确需要执行命令、列目录、读写文件、记录漏洞或检索知识库/查看 Skills 等操作时再调用上述工具。\n\n用户请求:%s",
|
||||
conn.ID, remark, conn.ID, req.Message)
|
||||
roleTools = []string{
|
||||
builtin.ToolWebshellExec,
|
||||
builtin.ToolWebshellFileList,
|
||||
builtin.ToolWebshellFileRead,
|
||||
builtin.ToolWebshellFileWrite,
|
||||
builtin.ToolRecordVulnerability,
|
||||
builtin.ToolListKnowledgeRiskTypes,
|
||||
builtin.ToolSearchKnowledgeBase,
|
||||
builtin.ToolListSkills,
|
||||
builtin.ToolReadSkill,
|
||||
}
|
||||
} else if req.Role != "" && req.Role != "默认" {
|
||||
if h.config.Roles != nil {
|
||||
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
|
||||
// 应用用户提示词
|
||||
@@ -788,6 +847,7 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
}
|
||||
// 注意:角色配置的skills不再硬编码注入,AI可以通过list_skills和read_skill工具按需调用
|
||||
if len(role.Skills) > 0 {
|
||||
roleSkills = role.Skills
|
||||
h.logger.Info("角色配置了skills,AI可通过工具按需调用", zap.String("role", req.Role), zap.Int("skillCount", len(role.Skills)), zap.Strings("skills", role.Skills))
|
||||
}
|
||||
}
|
||||
@@ -886,17 +946,7 @@ func (h *AgentHandler) AgentLoopStream(c *gin.Context) {
|
||||
|
||||
// 执行Agent Loop,传入独立的上下文,确保任务不会因客户端断开而中断(使用包含角色提示词的finalMessage和角色工具列表)
|
||||
sendEvent("progress", "正在分析您的请求...", nil)
|
||||
// 注意:skills不会硬编码注入,但会在系统提示词中提示AI这个角色推荐使用哪些skills
|
||||
var roleSkills []string // 角色配置的skills列表(用于提示AI,但不硬编码内容)
|
||||
if req.Role != "" && req.Role != "默认" {
|
||||
if h.config.Roles != nil {
|
||||
if role, exists := h.config.Roles[req.Role]; exists && role.Enabled {
|
||||
if len(role.Skills) > 0 {
|
||||
roleSkills = role.Skills
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 注意:roleSkills 已在上方根据 req.Role 或 WebShell 模式设置
|
||||
result, err := h.agent.AgentLoopWithProgress(taskCtx, finalMessage, agentHistoryMessages, conversationID, progressCallback, roleTools, roleSkills)
|
||||
if err != nil {
|
||||
h.logger.Error("Agent Loop执行失败", zap.Error(err))
|
||||
|
||||
@@ -28,6 +28,9 @@ type KnowledgeToolRegistrar func() error
|
||||
// VulnerabilityToolRegistrar 漏洞工具注册器接口
|
||||
type VulnerabilityToolRegistrar func() error
|
||||
|
||||
// WebshellToolRegistrar WebShell 工具注册器接口(ApplyConfig 时重新注册)
|
||||
type WebshellToolRegistrar func() error
|
||||
|
||||
// SkillsToolRegistrar Skills工具注册器接口
|
||||
type SkillsToolRegistrar func() error
|
||||
|
||||
@@ -60,6 +63,7 @@ type ConfigHandler struct {
|
||||
externalMCPMgr *mcp.ExternalMCPManager // 外部MCP管理器
|
||||
knowledgeToolRegistrar KnowledgeToolRegistrar // 知识库工具注册器(可选)
|
||||
vulnerabilityToolRegistrar VulnerabilityToolRegistrar // 漏洞工具注册器(可选)
|
||||
webshellToolRegistrar WebshellToolRegistrar // WebShell 工具注册器(可选)
|
||||
skillsToolRegistrar SkillsToolRegistrar // Skills工具注册器(可选)
|
||||
retrieverUpdater RetrieverUpdater // 检索器更新器(可选)
|
||||
knowledgeInitializer KnowledgeInitializer // 知识库初始化器(可选)
|
||||
@@ -120,6 +124,13 @@ func (h *ConfigHandler) SetVulnerabilityToolRegistrar(registrar VulnerabilityToo
|
||||
h.vulnerabilityToolRegistrar = registrar
|
||||
}
|
||||
|
||||
// SetWebshellToolRegistrar 设置 WebShell 工具注册器
|
||||
func (h *ConfigHandler) SetWebshellToolRegistrar(registrar WebshellToolRegistrar) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.webshellToolRegistrar = registrar
|
||||
}
|
||||
|
||||
// SetSkillsToolRegistrar 设置Skills工具注册器
|
||||
func (h *ConfigHandler) SetSkillsToolRegistrar(registrar SkillsToolRegistrar) {
|
||||
h.mu.Lock()
|
||||
@@ -792,6 +803,16 @@ func (h *ConfigHandler) ApplyConfig(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 重新注册 WebShell 工具(内置工具,必须注册)
|
||||
if h.webshellToolRegistrar != nil {
|
||||
h.logger.Info("重新注册 WebShell 工具")
|
||||
if err := h.webshellToolRegistrar(); err != nil {
|
||||
h.logger.Error("重新注册 WebShell 工具失败", zap.Error(err))
|
||||
} else {
|
||||
h.logger.Info("WebShell 工具已重新注册")
|
||||
}
|
||||
}
|
||||
|
||||
// 重新注册Skills工具(内置工具,必须注册)
|
||||
if h.skillsToolRegistrar != nil {
|
||||
h.logger.Info("重新注册Skills工具")
|
||||
|
||||
@@ -596,15 +596,25 @@ func (h *RobotHandler) HandleWecomPOST(c *gin.Context) {
|
||||
h.logger.Debug("企业微信 POST 收到请求", zap.String("body", string(bodyRaw)))
|
||||
|
||||
// 验证请求签名防止伪造。企业微信签名算法同 URL 验证,使用 token、timestamp、nonce、 Encrypt 四个字段
|
||||
if msgSignature != "" {
|
||||
// 若配置了 Token 则必须校验签名,避免未授权请求触发 Agent(防止平台被接管)
|
||||
token := h.config.Robots.Wecom.Token
|
||||
if token != "" {
|
||||
if msgSignature == "" {
|
||||
h.logger.Warn("企业微信 POST 缺少签名,已拒绝(需配置 token 并确保回调携带 msg_signature)")
|
||||
c.String(http.StatusOK, "")
|
||||
return
|
||||
}
|
||||
var tmp wecomXML
|
||||
if err := xml.Unmarshal(bodyRaw, &tmp); err == nil {
|
||||
expected := h.signWecomRequest(h.config.Robots.Wecom.Token, timestamp, nonce, tmp.Encrypt)
|
||||
if expected != msgSignature {
|
||||
h.logger.Warn("企业微信 POST 签名验证失败", zap.String("expected", expected), zap.String("got", msgSignature))
|
||||
c.String(http.StatusOK, "")
|
||||
return
|
||||
}
|
||||
if err := xml.Unmarshal(bodyRaw, &tmp); err != nil {
|
||||
h.logger.Warn("企业微信 POST 签名验证前解析 XML 失败", zap.Error(err))
|
||||
c.String(http.StatusOK, "")
|
||||
return
|
||||
}
|
||||
expected := h.signWecomRequest(token, timestamp, nonce, tmp.Encrypt)
|
||||
if expected != msgSignature {
|
||||
h.logger.Warn("企业微信 POST 签名验证失败", zap.String("expected", expected), zap.String("got", msgSignature))
|
||||
c.String(http.StatusOK, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,626 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cyberstrike-ai/internal/database"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// WebShellHandler 代理执行 WebShell 命令(类似冰蝎/蚁剑),避免前端跨域并统一构建请求
|
||||
type WebShellHandler struct {
|
||||
logger *zap.Logger
|
||||
client *http.Client
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
// NewWebShellHandler 创建 WebShell 处理器,db 可为 nil(连接配置接口将不可用)
|
||||
func NewWebShellHandler(logger *zap.Logger, db *database.DB) *WebShellHandler {
|
||||
return &WebShellHandler{
|
||||
logger: logger,
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &http.Transport{DisableKeepAlives: false},
|
||||
},
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateConnectionRequest 创建连接请求
|
||||
type CreateConnectionRequest struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
Method string `json:"method"`
|
||||
CmdParam string `json:"cmd_param"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// UpdateConnectionRequest 更新连接请求
|
||||
type UpdateConnectionRequest struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
Method string `json:"method"`
|
||||
CmdParam string `json:"cmd_param"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
// ListConnections 列出所有 WebShell 连接(GET /api/webshell/connections)
|
||||
func (h *WebShellHandler) ListConnections(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
list, err := h.db.ListWebshellConnections()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []database.WebShellConnection{}
|
||||
}
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
// CreateConnection 创建 WebShell 连接(POST /api/webshell/connections)
|
||||
func (h *WebShellHandler) CreateConnection(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
var req CreateConnectionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
if req.URL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"})
|
||||
return
|
||||
}
|
||||
if _, err := url.Parse(req.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url"})
|
||||
return
|
||||
}
|
||||
method := strings.ToLower(strings.TrimSpace(req.Method))
|
||||
if method != "get" && method != "post" {
|
||||
method = "post"
|
||||
}
|
||||
shellType := strings.ToLower(strings.TrimSpace(req.Type))
|
||||
if shellType == "" {
|
||||
shellType = "php"
|
||||
}
|
||||
conn := &database.WebShellConnection{
|
||||
ID: "ws_" + strings.ReplaceAll(uuid.New().String(), "-", "")[:12],
|
||||
URL: req.URL,
|
||||
Password: strings.TrimSpace(req.Password),
|
||||
Type: shellType,
|
||||
Method: method,
|
||||
CmdParam: strings.TrimSpace(req.CmdParam),
|
||||
Remark: strings.TrimSpace(req.Remark),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := h.db.CreateWebshellConnection(conn); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, conn)
|
||||
}
|
||||
|
||||
// UpdateConnection 更新 WebShell 连接(PUT /api/webshell/connections/:id)
|
||||
func (h *WebShellHandler) UpdateConnection(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
||||
return
|
||||
}
|
||||
var req UpdateConnectionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
if req.URL == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url is required"})
|
||||
return
|
||||
}
|
||||
if _, err := url.Parse(req.URL); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url"})
|
||||
return
|
||||
}
|
||||
method := strings.ToLower(strings.TrimSpace(req.Method))
|
||||
if method != "get" && method != "post" {
|
||||
method = "post"
|
||||
}
|
||||
shellType := strings.ToLower(strings.TrimSpace(req.Type))
|
||||
if shellType == "" {
|
||||
shellType = "php"
|
||||
}
|
||||
conn := &database.WebShellConnection{
|
||||
ID: id,
|
||||
URL: req.URL,
|
||||
Password: strings.TrimSpace(req.Password),
|
||||
Type: shellType,
|
||||
Method: method,
|
||||
CmdParam: strings.TrimSpace(req.CmdParam),
|
||||
Remark: strings.TrimSpace(req.Remark),
|
||||
}
|
||||
if err := h.db.UpdateWebshellConnection(conn); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "connection not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
updated, _ := h.db.GetWebshellConnection(id)
|
||||
if updated != nil {
|
||||
c.JSON(http.StatusOK, updated)
|
||||
} else {
|
||||
c.JSON(http.StatusOK, conn)
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteConnection 删除 WebShell 连接(DELETE /api/webshell/connections/:id)
|
||||
func (h *WebShellHandler) DeleteConnection(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
||||
return
|
||||
}
|
||||
if err := h.db.DeleteWebshellConnection(id); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "connection not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
// GetAIHistory 获取指定 WebShell 连接的 AI 助手对话历史(GET /api/webshell/connections/:id/ai-history)
|
||||
func (h *WebShellHandler) GetAIHistory(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
||||
return
|
||||
}
|
||||
conv, err := h.db.GetConversationByWebshellConnectionID(id)
|
||||
if err != nil {
|
||||
h.logger.Warn("获取 WebShell AI 对话失败", zap.String("connectionId", id), zap.Error(err))
|
||||
c.JSON(http.StatusOK, gin.H{"conversationId": nil, "messages": []database.Message{}})
|
||||
return
|
||||
}
|
||||
if conv == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"conversationId": nil, "messages": []database.Message{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"conversationId": conv.ID, "messages": conv.Messages})
|
||||
}
|
||||
|
||||
// ListAIConversations 列出该 WebShell 连接下的所有 AI 对话(供侧边栏)
|
||||
func (h *WebShellHandler) ListAIConversations(c *gin.Context) {
|
||||
if h.db == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "database not available"})
|
||||
return
|
||||
}
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "id is required"})
|
||||
return
|
||||
}
|
||||
list, err := h.db.ListConversationsByWebshellConnectionID(id)
|
||||
if err != nil {
|
||||
h.logger.Warn("列出 WebShell AI 对话失败", zap.String("connectionId", id), zap.Error(err))
|
||||
c.JSON(http.StatusOK, []database.WebShellConversationItem{})
|
||||
return
|
||||
}
|
||||
if list == nil {
|
||||
list = []database.WebShellConversationItem{}
|
||||
}
|
||||
c.JSON(http.StatusOK, list)
|
||||
}
|
||||
|
||||
// ExecRequest 执行命令请求(前端传入连接信息 + 命令)
|
||||
type ExecRequest struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"` // php, asp, aspx, jsp, custom
|
||||
Method string `json:"method"` // GET 或 POST,空则默认 POST
|
||||
CmdParam string `json:"cmd_param"` // 命令参数名,如 cmd/xxx,空则默认 cmd
|
||||
Command string `json:"command" binding:"required"`
|
||||
}
|
||||
|
||||
// ExecResponse 执行命令响应
|
||||
type ExecResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
HTTPCode int `json:"http_code,omitempty"`
|
||||
}
|
||||
|
||||
// FileOpRequest 文件操作请求
|
||||
type FileOpRequest struct {
|
||||
URL string `json:"url" binding:"required"`
|
||||
Password string `json:"password"`
|
||||
Type string `json:"type"`
|
||||
Method string `json:"method"` // GET 或 POST,空则默认 POST
|
||||
CmdParam string `json:"cmd_param"` // 命令参数名,如 cmd/xxx,空则默认 cmd
|
||||
Action string `json:"action" binding:"required"` // list, read, delete, write, mkdir, rename, upload, upload_chunk
|
||||
Path string `json:"path"`
|
||||
TargetPath string `json:"target_path"` // rename 时目标路径
|
||||
Content string `json:"content"` // write/upload 时使用
|
||||
ChunkIndex int `json:"chunk_index"` // upload_chunk 时,0 表示首块
|
||||
}
|
||||
|
||||
// FileOpResponse 文件操作响应
|
||||
type FileOpResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Output string `json:"output"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) Exec(c *gin.Context) {
|
||||
var req ExecRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
req.Command = strings.TrimSpace(req.Command)
|
||||
if req.URL == "" || req.Command == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url and command are required"})
|
||||
return
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(req.URL)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url: only http(s) allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
useGET := strings.ToUpper(strings.TrimSpace(req.Method)) == "GET"
|
||||
cmdParam := strings.TrimSpace(req.CmdParam)
|
||||
if cmdParam == "" {
|
||||
cmdParam = "cmd"
|
||||
}
|
||||
var httpReq *http.Request
|
||||
if useGET {
|
||||
targetURL := h.buildExecURL(req.URL, req.Type, req.Password, cmdParam, req.Command)
|
||||
httpReq, err = http.NewRequest(http.MethodGet, targetURL, nil)
|
||||
} else {
|
||||
body := h.buildExecBody(req.Type, req.Password, cmdParam, req.Command)
|
||||
httpReq, err = http.NewRequest(http.MethodPost, req.URL, bytes.NewReader(body))
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if err != nil {
|
||||
h.logger.Warn("webshell exec NewRequest", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, ExecResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
|
||||
|
||||
resp, err := h.client.Do(httpReq)
|
||||
if err != nil {
|
||||
h.logger.Warn("webshell exec Do", zap.String("url", req.URL), zap.Error(err))
|
||||
c.JSON(http.StatusOK, ExecResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
out, _ := io.ReadAll(resp.Body)
|
||||
output := string(out)
|
||||
httpCode := resp.StatusCode
|
||||
|
||||
c.JSON(http.StatusOK, ExecResponse{
|
||||
OK: resp.StatusCode == http.StatusOK,
|
||||
Output: output,
|
||||
HTTPCode: httpCode,
|
||||
})
|
||||
}
|
||||
|
||||
// buildExecBody 按常见 WebShell 约定构建 POST 体(多数使用 pass + cmd,可配置命令参数名)
|
||||
func (h *WebShellHandler) buildExecBody(shellType, password, cmdParam, command string) []byte {
|
||||
form := h.execParams(shellType, password, cmdParam, command)
|
||||
return []byte(form.Encode())
|
||||
}
|
||||
|
||||
// buildExecURL 构建 GET 请求的完整 URL(baseURL + ?pass=xxx&cmd=yyy,cmd 可配置)
|
||||
func (h *WebShellHandler) buildExecURL(baseURL, shellType, password, cmdParam, command string) string {
|
||||
form := h.execParams(shellType, password, cmdParam, command)
|
||||
if parsed, err := url.Parse(baseURL); err == nil {
|
||||
parsed.RawQuery = form.Encode()
|
||||
return parsed.String()
|
||||
}
|
||||
return baseURL + "?" + form.Encode()
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) execParams(shellType, password, cmdParam, command string) url.Values {
|
||||
shellType = strings.ToLower(strings.TrimSpace(shellType))
|
||||
if shellType == "" {
|
||||
shellType = "php"
|
||||
}
|
||||
if strings.TrimSpace(cmdParam) == "" {
|
||||
cmdParam = "cmd"
|
||||
}
|
||||
form := url.Values{}
|
||||
form.Set("pass", password)
|
||||
form.Set(cmdParam, command)
|
||||
return form
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) FileOp(c *gin.Context) {
|
||||
var req FileOpRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
req.URL = strings.TrimSpace(req.URL)
|
||||
req.Action = strings.ToLower(strings.TrimSpace(req.Action))
|
||||
if req.URL == "" || req.Action == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "url and action are required"})
|
||||
return
|
||||
}
|
||||
|
||||
parsed, err := url.Parse(req.URL)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid url: only http(s) allowed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 通过执行系统命令实现文件操作(与通用一句话兼容)
|
||||
var command string
|
||||
shellType := strings.ToLower(strings.TrimSpace(req.Type))
|
||||
switch req.Action {
|
||||
case "list":
|
||||
path := strings.TrimSpace(req.Path)
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "dir " + h.escapePath(path)
|
||||
} else {
|
||||
command = "ls -la " + h.escapePath(path)
|
||||
}
|
||||
case "read":
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "type " + h.escapePath(strings.TrimSpace(req.Path))
|
||||
} else {
|
||||
command = "cat " + h.escapePath(strings.TrimSpace(req.Path))
|
||||
}
|
||||
case "delete":
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "del " + h.escapePath(strings.TrimSpace(req.Path))
|
||||
} else {
|
||||
command = "rm -f " + h.escapePath(strings.TrimSpace(req.Path))
|
||||
}
|
||||
case "write":
|
||||
path := h.escapePath(strings.TrimSpace(req.Path))
|
||||
command = "echo " + h.escapeForEcho(req.Content) + " > " + path
|
||||
case "mkdir":
|
||||
path := strings.TrimSpace(req.Path)
|
||||
if path == "" {
|
||||
c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "path is required for mkdir"})
|
||||
return
|
||||
}
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "md " + h.escapePath(path)
|
||||
} else {
|
||||
command = "mkdir -p " + h.escapePath(path)
|
||||
}
|
||||
case "rename":
|
||||
oldPath := strings.TrimSpace(req.Path)
|
||||
newPath := strings.TrimSpace(req.TargetPath)
|
||||
if oldPath == "" || newPath == "" {
|
||||
c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "path and target_path are required for rename"})
|
||||
return
|
||||
}
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "move /y " + h.escapePath(oldPath) + " " + h.escapePath(newPath)
|
||||
} else {
|
||||
command = "mv " + h.escapePath(oldPath) + " " + h.escapePath(newPath)
|
||||
}
|
||||
case "upload":
|
||||
path := strings.TrimSpace(req.Path)
|
||||
if path == "" {
|
||||
c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "path is required for upload"})
|
||||
return
|
||||
}
|
||||
if len(req.Content) > 512*1024 {
|
||||
c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "upload content too large (max 512KB base64)"})
|
||||
return
|
||||
}
|
||||
// base64 仅含 A-Za-z0-9+/=,用单引号包裹安全
|
||||
command = "echo " + "'" + req.Content + "'" + " | base64 -d > " + h.escapePath(path)
|
||||
case "upload_chunk":
|
||||
path := strings.TrimSpace(req.Path)
|
||||
if path == "" {
|
||||
c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "path is required for upload_chunk"})
|
||||
return
|
||||
}
|
||||
redir := ">>"
|
||||
if req.ChunkIndex == 0 {
|
||||
redir = ">"
|
||||
}
|
||||
command = "echo " + "'" + req.Content + "'" + " | base64 -d " + redir + " " + h.escapePath(path)
|
||||
default:
|
||||
c.JSON(http.StatusBadRequest, FileOpResponse{OK: false, Error: "unsupported action: " + req.Action})
|
||||
return
|
||||
}
|
||||
|
||||
useGET := strings.ToUpper(strings.TrimSpace(req.Method)) == "GET"
|
||||
cmdParam := strings.TrimSpace(req.CmdParam)
|
||||
if cmdParam == "" {
|
||||
cmdParam = "cmd"
|
||||
}
|
||||
var httpReq *http.Request
|
||||
if useGET {
|
||||
targetURL := h.buildExecURL(req.URL, req.Type, req.Password, cmdParam, command)
|
||||
httpReq, err = http.NewRequest(http.MethodGet, targetURL, nil)
|
||||
} else {
|
||||
body := h.buildExecBody(req.Type, req.Password, cmdParam, command)
|
||||
httpReq, err = http.NewRequest(http.MethodPost, req.URL, bytes.NewReader(body))
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, FileOpResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
|
||||
|
||||
resp, err := h.client.Do(httpReq)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, FileOpResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
out, _ := io.ReadAll(resp.Body)
|
||||
output := string(out)
|
||||
|
||||
c.JSON(http.StatusOK, FileOpResponse{
|
||||
OK: resp.StatusCode == http.StatusOK,
|
||||
Output: output,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) escapePath(p string) string {
|
||||
if p == "" {
|
||||
return "."
|
||||
}
|
||||
// 简单转义空格与敏感字符,避免命令注入
|
||||
return "'" + strings.ReplaceAll(p, "'", "'\\''") + "'"
|
||||
}
|
||||
|
||||
func (h *WebShellHandler) escapeForEcho(s string) string {
|
||||
// 仅用于 write:base64 写入更安全,这里简单用单引号包裹
|
||||
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
|
||||
}
|
||||
|
||||
// ExecWithConnection 在指定 WebShell 连接上执行命令(供 MCP/Agent 等非 HTTP 调用)
|
||||
func (h *WebShellHandler) ExecWithConnection(conn *database.WebShellConnection, command string) (output string, ok bool, errMsg string) {
|
||||
if conn == nil {
|
||||
return "", false, "connection is nil"
|
||||
}
|
||||
command = strings.TrimSpace(command)
|
||||
if command == "" {
|
||||
return "", false, "command is required"
|
||||
}
|
||||
useGET := strings.ToUpper(strings.TrimSpace(conn.Method)) == "GET"
|
||||
cmdParam := strings.TrimSpace(conn.CmdParam)
|
||||
if cmdParam == "" {
|
||||
cmdParam = "cmd"
|
||||
}
|
||||
var httpReq *http.Request
|
||||
var err error
|
||||
if useGET {
|
||||
targetURL := h.buildExecURL(conn.URL, conn.Type, conn.Password, cmdParam, command)
|
||||
httpReq, err = http.NewRequest(http.MethodGet, targetURL, nil)
|
||||
} else {
|
||||
body := h.buildExecBody(conn.Type, conn.Password, cmdParam, command)
|
||||
httpReq, err = http.NewRequest(http.MethodPost, conn.URL, bytes.NewReader(body))
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err.Error()
|
||||
}
|
||||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
|
||||
resp, err := h.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", false, err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
out, _ := io.ReadAll(resp.Body)
|
||||
return string(out), resp.StatusCode == http.StatusOK, ""
|
||||
}
|
||||
|
||||
// FileOpWithConnection 在指定 WebShell 连接上执行文件操作(供 MCP/Agent 调用),支持 list / read / write
|
||||
func (h *WebShellHandler) FileOpWithConnection(conn *database.WebShellConnection, action, path, content, targetPath string) (output string, ok bool, errMsg string) {
|
||||
if conn == nil {
|
||||
return "", false, "connection is nil"
|
||||
}
|
||||
action = strings.ToLower(strings.TrimSpace(action))
|
||||
shellType := strings.ToLower(strings.TrimSpace(conn.Type))
|
||||
if shellType == "" {
|
||||
shellType = "php"
|
||||
}
|
||||
var command string
|
||||
switch action {
|
||||
case "list":
|
||||
if path == "" {
|
||||
path = "."
|
||||
}
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "dir " + h.escapePath(strings.TrimSpace(path))
|
||||
} else {
|
||||
command = "ls -la " + h.escapePath(strings.TrimSpace(path))
|
||||
}
|
||||
case "read":
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "", false, "path is required for read"
|
||||
}
|
||||
if shellType == "asp" || shellType == "aspx" {
|
||||
command = "type " + h.escapePath(path)
|
||||
} else {
|
||||
command = "cat " + h.escapePath(path)
|
||||
}
|
||||
case "write":
|
||||
path = strings.TrimSpace(path)
|
||||
if path == "" {
|
||||
return "", false, "path is required for write"
|
||||
}
|
||||
command = "echo " + h.escapeForEcho(content) + " > " + h.escapePath(path)
|
||||
default:
|
||||
return "", false, "unsupported action: " + action + " (supported: list, read, write)"
|
||||
}
|
||||
useGET := strings.ToUpper(strings.TrimSpace(conn.Method)) == "GET"
|
||||
cmdParam := strings.TrimSpace(conn.CmdParam)
|
||||
if cmdParam == "" {
|
||||
cmdParam = "cmd"
|
||||
}
|
||||
var httpReq *http.Request
|
||||
var err error
|
||||
if useGET {
|
||||
targetURL := h.buildExecURL(conn.URL, conn.Type, conn.Password, cmdParam, command)
|
||||
httpReq, err = http.NewRequest(http.MethodGet, targetURL, nil)
|
||||
} else {
|
||||
body := h.buildExecBody(conn.Type, conn.Password, cmdParam, command)
|
||||
httpReq, err = http.NewRequest(http.MethodPost, conn.URL, bytes.NewReader(body))
|
||||
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
if err != nil {
|
||||
return "", false, err.Error()
|
||||
}
|
||||
httpReq.Header.Set("User-Agent", "Mozilla/5.0 (compatible; CyberStrikeAI-WebShell/1.0)")
|
||||
resp, err := h.client.Do(httpReq)
|
||||
if err != nil {
|
||||
return "", false, err.Error()
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
out, _ := io.ReadAll(resp.Body)
|
||||
return string(out), resp.StatusCode == http.StatusOK, ""
|
||||
}
|
||||
@@ -13,6 +13,12 @@ const (
|
||||
// Skills工具
|
||||
ToolListSkills = "list_skills"
|
||||
ToolReadSkill = "read_skill"
|
||||
|
||||
// WebShell 助手工具(AI 在 WebShell 管理 - AI 助手 中使用)
|
||||
ToolWebshellExec = "webshell_exec"
|
||||
ToolWebshellFileList = "webshell_file_list"
|
||||
ToolWebshellFileRead = "webshell_file_read"
|
||||
ToolWebshellFileWrite = "webshell_file_write"
|
||||
)
|
||||
|
||||
// IsBuiltinTool 检查工具名称是否是内置工具
|
||||
@@ -22,7 +28,11 @@ func IsBuiltinTool(toolName string) bool {
|
||||
ToolListKnowledgeRiskTypes,
|
||||
ToolSearchKnowledgeBase,
|
||||
ToolListSkills,
|
||||
ToolReadSkill:
|
||||
ToolReadSkill,
|
||||
ToolWebshellExec,
|
||||
ToolWebshellFileList,
|
||||
ToolWebshellFileRead,
|
||||
ToolWebshellFileWrite:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -37,5 +47,9 @@ func GetAllBuiltinTools() []string {
|
||||
ToolSearchKnowledgeBase,
|
||||
ToolListSkills,
|
||||
ToolReadSkill,
|
||||
ToolWebshellExec,
|
||||
ToolWebshellFileList,
|
||||
ToolWebshellFileRead,
|
||||
ToolWebshellFileWrite,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1229,6 +1229,17 @@ func (e *Executor) buildInputSchema(toolConfig *config.ToolConfig) map[string]in
|
||||
"description": param.Description,
|
||||
}
|
||||
|
||||
// JSON Schema/OpenAI 要求 array 类型必须包含 items,否则 API 报 invalid_function_parameters
|
||||
if openAIType == "array" {
|
||||
itemType := strings.TrimSpace(param.ItemType)
|
||||
if itemType == "" {
|
||||
itemType = "string"
|
||||
}
|
||||
prop["items"] = map[string]interface{}{
|
||||
"type": e.convertToOpenAIType(itemType),
|
||||
}
|
||||
}
|
||||
|
||||
// 添加默认值
|
||||
if param.Default != nil {
|
||||
prop["default"] = param.Default
|
||||
|
||||
@@ -202,6 +202,7 @@ description: |
|
||||
```yaml
|
||||
- name: "ports"
|
||||
type: "array"
|
||||
item_type: "number"
|
||||
description: "端口列表"
|
||||
required: false
|
||||
# 输入: [80, 443, 8080]
|
||||
@@ -364,6 +365,13 @@ parameters:
|
||||
- 说明权限要求
|
||||
- 提醒仅在授权环境中使用
|
||||
|
||||
6. **单次执行时长与超时(最佳实践)**
|
||||
- 若某工具经常执行很久(如超过 10~30 分钟仍显示「执行中」),属于异常长时间挂起,建议:
|
||||
- 在 **config.yaml** 的 `agent.tool_timeout_minutes` 中设置单次工具最大执行时长(默认 10 分钟),超时后会自动终止并释放资源;
|
||||
- 需要更长扫描时再适当调大该值(如 20、30),不建议设为 0(不限制);
|
||||
- 在任务监控页可对整条任务使用「停止任务」中断当前对话与后续工具调用;
|
||||
- 工具实现上尽量支持「可中断」或内置超时(如脚本内设 timeout),以便与系统超时协同。
|
||||
|
||||
## 禁用工具
|
||||
|
||||
要禁用某个工具,只需将配置文件中的 `enabled` 字段设置为 `false`,或者直接删除/重命名配置文件。
|
||||
@@ -390,6 +398,13 @@ A: 对于数组类型参数,系统会自动转换为逗号分隔的字符串
|
||||
|
||||
A: 某些工具(如 `nmap`)支持 `scan_type` 参数来覆盖默认的扫描类型。对于其他情况,可以使用 `additional_args` 参数。
|
||||
|
||||
### Q: 工具执行超过 30 分钟一直显示「执行中」怎么办?
|
||||
|
||||
A: 属于异常长时间挂起,建议:
|
||||
1. 在 **config.yaml** 中配置 `agent.tool_timeout_minutes`(默认 10),单次工具超过该分钟数会自动终止;
|
||||
2. 在监控页对该任务使用「停止任务」立即中断;
|
||||
3. 若该工具确实需要更长时间,可适当增大 `tool_timeout_minutes`,但不建议设为 0。
|
||||
|
||||
### Q: 工具执行失败怎么办?
|
||||
|
||||
A: 检查以下几点:
|
||||
|
||||
+330
-151
@@ -2,121 +2,168 @@
|
||||
|
||||
## Overview
|
||||
|
||||
Each tool ships with its own YAML configuration placed in the `tools/` directory. This keeps definitions modular, easier to review, and simple to extend. The runtime automatically loads every `.yaml` / `.yml` file in that directory.
|
||||
Each tool has its own configuration file under the `tools/` directory. This keeps tool definitions clear, easy to maintain, and manageable. The system automatically loads all `.yaml` and `.yml` files in `tools/`.
|
||||
|
||||
## File Structure
|
||||
## Configuration File Format
|
||||
|
||||
The table below enumerates every supported top-level field. Double-check each entry before adding a new tool:
|
||||
Each tool configuration file is a YAML file. The table below lists supported top-level fields and whether they are required. Check each item before submitting:
|
||||
|
||||
| Field | Required | Type | Description |
|
||||
|-------|----------|------|-------------|
|
||||
| `name` | ✅ | string | Unique identifier. Prefer lowercase letters, digits, and hyphens. |
|
||||
| `command` | ✅ | string | Executable or script name. Must exist in `$PATH` or be an absolute path. |
|
||||
| `enabled` | ✅ | bool | Controls MCP registration. Disabled tools are ignored by the loader. |
|
||||
| `description` | ✅ | string | Full Markdown description for MCP `resources/read` and AI comprehension. |
|
||||
| `short_description` | Optional | string | 20–50 character summary shown in tool lists. When omitted, the loader extracts the start of `description`. |
|
||||
| `args` | Optional | string[] | Static arguments prepended to every invocation—useful for default scan profiles. |
|
||||
| `parameters` | Optional | array | Runtime parameter definitions. See **Parameter Definition** for details. |
|
||||
| `arg_mapping` | Optional | string | Mapping strategy (`auto`/`manual`/`template`). Defaults to `auto`; override only for legacy tooling. |
|
||||
| `name` | ✅ | string | Unique tool identifier; use lowercase letters, digits, and hyphens. |
|
||||
| `command` | ✅ | string | Command or script to run; must be on system PATH or an absolute path. |
|
||||
| `enabled` | ✅ | bool | Whether to register with MCP; set to `false` to skip the tool. |
|
||||
| `description` | ✅ | string | Full description, multi-line Markdown, for AI and `resources/read` queries. |
|
||||
| `short_description` | Optional | string | 20–50 character summary for tool lists and lower token usage; defaults to start of `description` if omitted. |
|
||||
| `args` | Optional | string[] | Fixed arguments prepended to the command line; often used for default scan modes. |
|
||||
| `parameters` | Optional | array | Runtime parameter list; see **Parameter Definition** below. |
|
||||
| `arg_mapping` | Optional | string | Parameter mapping mode (`auto`/`manual`/`template`); default `auto`; only set if needed. |
|
||||
|
||||
> If a required field is missing or malformed, the loader skips that tool and logs a warning without blocking the service.
|
||||
> If a field is wrong or a required field is missing, the loader skips that tool and logs a warning; other tools are unaffected.
|
||||
|
||||
## Tool Descriptions
|
||||
|
||||
### Short Description (`short_description`)
|
||||
|
||||
- **Purpose**: compact summary for tool listings and to minimise language model context usage.
|
||||
- **Guideline**: one concise sentence (20–50 Chinese characters or English equivalents).
|
||||
- **Purpose**: Used in tool lists to reduce tokens sent to the model.
|
||||
- **Guideline**: One sentence (20–50 characters) describing the tool’s main use.
|
||||
- **Example**: `"Network scanner for discovering hosts, open ports, and services"`
|
||||
|
||||
### Detailed Description (`description`)
|
||||
|
||||
Supports multi-line Markdown. Recommended contents:
|
||||
Use multi-line text and include:
|
||||
|
||||
1. **Capabilities** – what the tool does.
|
||||
2. **Usage scenarios** – when to prefer this tool.
|
||||
3. **Warnings** – permissions, runtime risks, side-effects.
|
||||
4. **Examples** – optional walkthroughs or sample commands.
|
||||
1. **Capabilities**: What the tool does.
|
||||
2. **Usage scenarios**: When to use it.
|
||||
3. **Warnings**: Caveats and safety notes.
|
||||
4. **Examples**: Optional usage examples.
|
||||
|
||||
**Important**:
|
||||
- Tool menus and MCP summaries use `short_description` when available.
|
||||
- Without `short_description`, the loader trims the first line or first 100 characters of `description`.
|
||||
- Full descriptions are accessible through the MCP `resources/read` endpoint (`tool://<tool_name>`).
|
||||
**Notes**:
|
||||
- Tool lists use `short_description` when present.
|
||||
- If `short_description` is missing, the system uses the first line or first 100 characters of `description`.
|
||||
- Full descriptions are available via MCP `resources/read` (URI: `tool://tool_name`).
|
||||
|
||||
This reduces token usage, especially with many tools (e.g. 100+).
|
||||
|
||||
## Parameter Definition
|
||||
|
||||
Each parameter object accepts the fields below:
|
||||
Each parameter can include:
|
||||
|
||||
- `name` *(required)* – parameter key used in CLI construction and MCP schema.
|
||||
- `type` *(required)* – `string`, `int`/`integer`, `bool`/`boolean`, `array`, etc.
|
||||
- `description` *(required)* – Markdown-friendly explanation including purpose, format rules, example values, and safety notes.
|
||||
- `required` – boolean; when `true`, missing values cause the executor to return an error.
|
||||
- `default` – fallback value applied if the caller omits the argument.
|
||||
- `flag` – CLI switch such as `-u` or `--url`.
|
||||
- `position` – zero-based index for positional arguments.
|
||||
- `format` – rendering strategy:
|
||||
- `flag` *(default)* → `--flag value` / `-f value`
|
||||
- `combined` → `--flag=value`
|
||||
- `positional` → appended according to `position`
|
||||
- `template` → uses the `template` string
|
||||
- `template` – placeholder string (supports `{flag}`, `{value}`, `{name}`) when `format: "template"`.
|
||||
- `options` – array of allowed values; surfaced as `enum` entries in the MCP schema.
|
||||
- `name`: Parameter name.
|
||||
- `type`: One of string, int, bool, array.
|
||||
- `description`: Full description (multi-line supported).
|
||||
- `required`: Whether it is required (true/false).
|
||||
- `default`: Default value.
|
||||
- `flag`: CLI flag (e.g. `-u`, `--url`, `-p`).
|
||||
- `position`: Zero-based index for positional arguments.
|
||||
- `format`: One of `"flag"`, `"positional"`, `"combined"`, `"template"`.
|
||||
- `template`: Template string when `format` is `"template"`.
|
||||
- `options`: Allowed values for enums.
|
||||
|
||||
### Format Reference
|
||||
### Parameter Formats
|
||||
|
||||
- **`flag`**: pass the flag and the value separately.
|
||||
Example: `flag: "-u"` → `-u https://example.com`
|
||||
- **`flag`**: Flag plus value, e.g. `--flag value` or `-f value`
|
||||
- Example: `flag: "-u"` → `-u http://example.com`
|
||||
|
||||
- **`positional`**: insert according to `position`.
|
||||
Example: `position: 0` → becomes the first positional argument.
|
||||
- **`positional`**: Added in order by position.
|
||||
- Example: `position: 0` → first positional argument.
|
||||
|
||||
- **`combined`**: join flag and value in one token.
|
||||
Example: `flag: "--level"`, `format: "combined"` → `--level=3`
|
||||
- **`combined`**: Single token `--flag=value`.
|
||||
- Example: `flag: "--level"`, `format: "combined"` → `--level=3`
|
||||
|
||||
- **`template`**: custom rendering.
|
||||
Example: `template: "{flag} {value}"` → fully manual control.
|
||||
- **`template`**: Custom template.
|
||||
- Example: `template: "{flag} {value}"` → custom format.
|
||||
|
||||
### Reserved Parameters
|
||||
### Special Parameters
|
||||
|
||||
- `additional_args` – allows users to append arbitrary CLI fragments. The executor tokenises the string (preserving quoted groups) and appends the resulting list to the command.
|
||||
- `scan_type` – for scanners like `nmap`, replacing default scan switches (e.g., `-sV -sC`).
|
||||
- `action` – consumed by server-side branching logic and intentionally not forwarded to the command line.
|
||||
#### `additional_args`
|
||||
|
||||
## Parameter Description Checklist
|
||||
Used to pass extra CLI options not defined in the parameter list. The value is split on spaces into multiple arguments.
|
||||
|
||||
When documenting a parameter, include:
|
||||
**Use cases:**
|
||||
- Advanced tool options.
|
||||
- Options not in the schema.
|
||||
- Complex argument combinations.
|
||||
|
||||
1. **Purpose** – what the value controls.
|
||||
2. **Format rules** – accepted patterns (URL, CIDR, path, etc.).
|
||||
3. **Example values** – list several realistic samples.
|
||||
4. **Notes** – permissions, performance impact, or other caveats.
|
||||
**Example:**
|
||||
```yaml
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: "Extra CLI arguments; separate multiple options with spaces"
|
||||
required: false
|
||||
format: "positional"
|
||||
```
|
||||
|
||||
Suggested style: Markdown lists, bold emphasis for key cautions, and code blocks for complex examples.
|
||||
**Usage:**
|
||||
- `additional_args: "--script vuln -O"` → `["--script", "vuln", "-O"]`
|
||||
- `additional_args: "-T4 --max-retries 3"` → `["-T4", "--max-retries", "3"]`
|
||||
|
||||
### Example
|
||||
**Notes:**
|
||||
- Split by spaces; quoted parts are preserved.
|
||||
- Ensure valid syntax to avoid command injection.
|
||||
- Appended at the end of the command.
|
||||
|
||||
#### `scan_type` (tool-specific)
|
||||
|
||||
Some tools (e.g. `nmap`) support `scan_type` to override the default scan arguments.
|
||||
|
||||
**Example (nmap):**
|
||||
```yaml
|
||||
- name: "scan_type"
|
||||
type: "string"
|
||||
description: "Scan type options; overrides default scan arguments"
|
||||
required: false
|
||||
format: "positional"
|
||||
```
|
||||
|
||||
**Usage:**
|
||||
- `scan_type: "-sV -sC"` → version and script scan.
|
||||
- `scan_type: "-A"` → aggressive scan.
|
||||
|
||||
**Notes:**
|
||||
- If set, it replaces the tool’s default scan arguments.
|
||||
- Multiple options separated by spaces.
|
||||
|
||||
### Parameter Description Guidelines
|
||||
|
||||
Parameter descriptions should include:
|
||||
|
||||
1. **Purpose**: What the parameter does.
|
||||
2. **Format**: Expected format (e.g. URL, port range).
|
||||
3. **Example values**: Concrete examples (list if several).
|
||||
4. **Notes**: Permissions, performance, safety, etc.
|
||||
|
||||
**Style:**
|
||||
- Use Markdown for readability.
|
||||
- Use **bold** for important points.
|
||||
- Use lists for multiple examples or options.
|
||||
- Use code blocks for complex formats.
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
description: |
|
||||
Target IP address or domain. Accepts single IPs, ranges, CIDR blocks, or hostnames.
|
||||
Target IP or domain. Can be a single IP, range, CIDR, or hostname.
|
||||
|
||||
**Example values**
|
||||
**Example values:**
|
||||
- Single IP: "192.168.1.1"
|
||||
- Range: "192.168.1.1-100"
|
||||
- CIDR: "192.168.1.0/24"
|
||||
- Domain: "example.com"
|
||||
|
||||
**Notes**
|
||||
**Notes:**
|
||||
- Format must be valid.
|
||||
- Required; cannot be empty.
|
||||
- Validate address format before running to avoid false positives.
|
||||
```
|
||||
|
||||
## Parameter Types
|
||||
|
||||
### Boolean
|
||||
- `true` → adds only the flag (no value).
|
||||
- `false` → suppresses the flag.
|
||||
- Accepts `true`/`false`, `1`/`0`, and `"true"`/`"false"`.
|
||||
### Boolean (`bool`)
|
||||
|
||||
- `true`: Add only the flag (e.g. `--flag`).
|
||||
- `false`: Do not add the argument.
|
||||
- Accepted: `true`/`false`, `1`/`0`, `"true"`/`"false"`.
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
- name: "verbose"
|
||||
type: "bool"
|
||||
@@ -127,91 +174,68 @@ description: |
|
||||
format: "flag"
|
||||
```
|
||||
|
||||
### String
|
||||
Most common parameter type; accepts any string value.
|
||||
### String (`string`)
|
||||
|
||||
### Integer
|
||||
Use for numeric inputs (ports, levels, limits).
|
||||
General-purpose; any string value.
|
||||
|
||||
### Integer (`int` / `integer`)
|
||||
|
||||
For numbers (ports, levels, etc.).
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
- name: "level"
|
||||
type: "int"
|
||||
description: "Level of detail, 1-5"
|
||||
description: "Test level, 1-5"
|
||||
required: false
|
||||
default: 3
|
||||
flag: "--level"
|
||||
format: "combined" # --level=3
|
||||
```
|
||||
|
||||
### Array
|
||||
Automatically converted to a comma-separated string.
|
||||
### Array (`array`)
|
||||
|
||||
Converted to a comma-separated string.
|
||||
|
||||
**Example:**
|
||||
```yaml
|
||||
- name: "ports"
|
||||
type: "array"
|
||||
description: "List of ports to scan"
|
||||
item_type: "number"
|
||||
description: "Port list"
|
||||
required: false
|
||||
# Input: [80, 443, 8080]
|
||||
# Output: "80,443,8080"
|
||||
```
|
||||
|
||||
## Special Parameters
|
||||
## Examples
|
||||
|
||||
### `additional_args`
|
||||
See existing configs under `tools/`:
|
||||
|
||||
```yaml
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: "Extra CLI arguments; separate multiple options with spaces"
|
||||
required: false
|
||||
format: "positional"
|
||||
```
|
||||
- `nmap.yaml`: Network scanner (`scan_type` and `additional_args`).
|
||||
- `sqlmap.yaml`: SQL injection (`additional_args`).
|
||||
- `nikto.yaml`: Web server scanner.
|
||||
- `dirb.yaml`: Directory scanner.
|
||||
- `exec.yaml`: System command execution.
|
||||
|
||||
Examples:
|
||||
- `additional_args: "--script vuln -O"` → `["--script", "vuln", "-O"]`
|
||||
- `additional_args: "-T4 --max-retries 3"` → `["-T4", "--max-retries", "3"]`
|
||||
|
||||
Notes:
|
||||
- Quoted strings are preserved.
|
||||
- Validate user input to avoid command injection.
|
||||
- Appended at the end of the final command.
|
||||
|
||||
### `scan_type`
|
||||
|
||||
```yaml
|
||||
- name: "scan_type"
|
||||
type: "string"
|
||||
description: "Overrides default scan switches"
|
||||
required: false
|
||||
format: "positional"
|
||||
```
|
||||
|
||||
Examples:
|
||||
- `scan_type: "-sV -sC"`
|
||||
- `scan_type: "-A"`
|
||||
|
||||
Notes:
|
||||
- Replaces default entries in the tool’s `args` list.
|
||||
- Separate multiple flags with spaces.
|
||||
|
||||
## Complete Example (`nmap`)
|
||||
### Full Example: nmap
|
||||
|
||||
```yaml
|
||||
name: "nmap"
|
||||
command: "nmap"
|
||||
args: ["-sT", "-sV", "-sC"]
|
||||
args: ["-sT", "-sV", "-sC"] # default scan type
|
||||
enabled: true
|
||||
|
||||
short_description: "Network scanner for discovering hosts, open ports, and services"
|
||||
|
||||
description: |
|
||||
Network mapping and port scanning utility.
|
||||
Network mapping and port scanning for hosts, services, and open ports.
|
||||
|
||||
**Highlights**
|
||||
**Capabilities:**
|
||||
- Host discovery
|
||||
- Port scanning
|
||||
- Service identification
|
||||
- OS fingerprinting
|
||||
- Service/version detection
|
||||
- OS detection
|
||||
- NSE-based vulnerability checks
|
||||
|
||||
parameters:
|
||||
@@ -224,62 +248,80 @@ parameters:
|
||||
|
||||
- name: "ports"
|
||||
type: "string"
|
||||
description: "Port range, e.g., 1-1000"
|
||||
description: "Port range, e.g. 1-1000"
|
||||
required: false
|
||||
flag: "-p"
|
||||
format: "flag"
|
||||
|
||||
- name: "scan_type"
|
||||
type: "string"
|
||||
description: "Override scan switches, e.g., '-sV -sC'"
|
||||
description: "Scan type options, e.g. '-sV -sC'"
|
||||
required: false
|
||||
format: "positional"
|
||||
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: "Extra nmap arguments, e.g., '--script vuln -O'"
|
||||
description: "Extra nmap arguments, e.g. '--script vuln -O'"
|
||||
required: false
|
||||
format: "positional"
|
||||
```
|
||||
|
||||
## Adding a New Tool
|
||||
|
||||
1. Create a YAML file in `tools/` (e.g., `tools/mytool.yaml`).
|
||||
2. Fill out the top-level fields and parameter list.
|
||||
3. Provide defaults and rich descriptions wherever possible.
|
||||
4. Run `go run cmd/test-config/main.go` to validate the configuration.
|
||||
5. Restart the service (or trigger a reload) so the UI and MCP registry pick up the change.
|
||||
|
||||
### Template
|
||||
Create a new YAML file under `tools/`, e.g. `my_tool.yaml`:
|
||||
|
||||
```yaml
|
||||
name: "tool_name"
|
||||
command: "command"
|
||||
name: "my_tool"
|
||||
command: "my-command"
|
||||
args: ["--default-arg"] # optional fixed args
|
||||
enabled: true
|
||||
|
||||
short_description: "One-line summary"
|
||||
# Short description (recommended) – for tool list, fewer tokens
|
||||
short_description: "One-line summary of what the tool does"
|
||||
|
||||
# Full description – for docs and AI
|
||||
description: |
|
||||
Detailed description with Markdown formatting.
|
||||
Full description; multi-line and Markdown supported.
|
||||
|
||||
**Capabilities:**
|
||||
- Feature 1
|
||||
- Feature 2
|
||||
|
||||
**Usage:**
|
||||
- Scenario 1
|
||||
- Scenario 2
|
||||
|
||||
**Notes:**
|
||||
- Caveats
|
||||
- Permissions
|
||||
- Performance
|
||||
|
||||
parameters:
|
||||
- name: "target"
|
||||
type: "string"
|
||||
description: "Explain the expected value, format, examples, and caveats"
|
||||
description: |
|
||||
Target parameter description.
|
||||
|
||||
**Example values:**
|
||||
- "value1"
|
||||
- "value2"
|
||||
|
||||
**Notes:**
|
||||
- Format and limits
|
||||
required: true
|
||||
position: 0
|
||||
format: "positional"
|
||||
|
||||
- name: "option"
|
||||
type: "string"
|
||||
description: "Optional flag parameter"
|
||||
description: "Option parameter"
|
||||
required: false
|
||||
flag: "--option"
|
||||
format: "flag"
|
||||
|
||||
- name: "verbose"
|
||||
type: "bool"
|
||||
description: "Enable verbose mode"
|
||||
description: "Verbose mode"
|
||||
required: false
|
||||
default: false
|
||||
flag: "-v"
|
||||
@@ -287,33 +329,170 @@ parameters:
|
||||
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: "Extra CLI options separated by spaces"
|
||||
description: "Extra arguments; separate with spaces"
|
||||
required: false
|
||||
format: "positional"
|
||||
```
|
||||
|
||||
## Validation & Troubleshooting
|
||||
Restart the service to load the new tool.
|
||||
|
||||
- ✅ Verify required fields: `name`, `command`, `enabled`, `description`.
|
||||
- ✅ Ensure parameter definitions use supported types and formats.
|
||||
- ✅ Watch server logs for warnings when a tool fails to load.
|
||||
- ✅ Use `go run cmd/test-config/main.go` to inspect parsed tool metadata.
|
||||
### Best Practices
|
||||
|
||||
## Best Practices
|
||||
1. **Parameter design**
|
||||
- Define common parameters explicitly so the AI can use them.
|
||||
- Use `additional_args` for advanced cases.
|
||||
- Provide clear descriptions and examples.
|
||||
|
||||
1. **Parameter design** – expose common flags individually; leverage `additional_args` for advanced scenarios.
|
||||
2. **Documentation** – combine `short_description` with thorough `description` to balance brevity and clarity.
|
||||
3. **Defaults** – provide sensible `default` values, especially for frequently used options.
|
||||
4. **Validation prompts** – describe expected formats and highlight constraints to help the AI and users avoid mistakes.
|
||||
5. **Safety** – warn about privileged commands, destructive actions, or high-impact scans.
|
||||
2. **Descriptions**
|
||||
- Use `short_description` to reduce tokens.
|
||||
- Keep `description` detailed for AI and docs.
|
||||
- Use Markdown for readability.
|
||||
|
||||
3. **Defaults**
|
||||
- Set sensible defaults for common parameters.
|
||||
- Booleans often default to `false`.
|
||||
- Numbers according to tool behavior.
|
||||
|
||||
4. **Validation**
|
||||
- Document format and constraints.
|
||||
- Give several example values.
|
||||
- Mention limits and caveats.
|
||||
|
||||
5. **Safety**
|
||||
- Add warnings for dangerous or privileged actions.
|
||||
- Document permission requirements.
|
||||
- Remind users to use only in authorized environments.
|
||||
|
||||
6. **Execution duration and timeout**
|
||||
- If a tool often runs very long (e.g. still “running” after 10–30 minutes), treat it as abnormal and:
|
||||
- Set **config.yaml** → `agent.tool_timeout_minutes` (default 10) so long runs are stopped and resources freed.
|
||||
- Increase it (e.g. 20, 30) only when longer runs are needed; avoid `0` (no limit).
|
||||
- Use “Stop task” on the task monitor to cancel the whole run.
|
||||
- Prefer tools that support cancellation or an internal timeout so they align with the global timeout.
|
||||
|
||||
## Disabling a Tool
|
||||
|
||||
Set `enabled: false` or remove/rename the YAML file. Disabled tools disappear from the UI and MCP inventory.
|
||||
Set `enabled: false` in the tool’s config, or remove/rename the file. Disabled tools are not listed and cannot be called by the AI.
|
||||
|
||||
## Tool Configuration Validation
|
||||
|
||||
On load, the system checks:
|
||||
|
||||
- ✅ Required fields: `name`, `command`, `enabled`.
|
||||
- ✅ Parameter structure and types.
|
||||
|
||||
Invalid configs produce startup warnings but do not prevent the server from starting. Invalid tools are skipped; others still load.
|
||||
|
||||
## FAQ
|
||||
|
||||
### Q: How do I pass multiple parameter values?
|
||||
|
||||
A: Array parameters are turned into comma-separated strings. For multiple separate arguments, use `additional_args`.
|
||||
|
||||
### Q: How do I override a tool’s default arguments?
|
||||
|
||||
A: Some tools (e.g. `nmap`) support a `scan_type` parameter. Otherwise use `additional_args`.
|
||||
|
||||
### Q: A tool has been “running” for over 30 minutes. What should I do?
|
||||
|
||||
A: That usually means it’s stuck. You can:
|
||||
1. Set `agent.tool_timeout_minutes` in **config.yaml** (default 10) so single tool runs are stopped after that many minutes.
|
||||
2. Use “Stop task” on the task monitor to stop the run immediately.
|
||||
3. If the tool legitimately needs more time, increase `tool_timeout_minutes` (avoid setting it to 0).
|
||||
|
||||
### Q: What if tool execution fails?
|
||||
|
||||
A: Check:
|
||||
1. The tool is installed and on PATH.
|
||||
2. The tool config is correct.
|
||||
3. Parameter formats match what the tool expects.
|
||||
4. Server logs for the exact error.
|
||||
|
||||
### Q: How can I test a tool configuration?
|
||||
|
||||
A: Use the config test utility:
|
||||
```bash
|
||||
go run cmd/test-config/main.go
|
||||
```
|
||||
|
||||
### Q: How is parameter order controlled?
|
||||
|
||||
A: Use the `position` field for positional arguments. **Position 0** (e.g. gobuster’s `dir` subcommand) is placed right after the command, before any flag arguments, so CLIs that expect “subcommand + options” work. Other flags are added in the order they appear in `parameters`, then position 1, 2, …; `additional_args` is appended last.
|
||||
|
||||
## Tool Configuration Templates
|
||||
|
||||
### Basic template
|
||||
|
||||
```yaml
|
||||
name: "tool_name"
|
||||
command: "command"
|
||||
enabled: true
|
||||
|
||||
short_description: "Short description (20–50 chars)"
|
||||
|
||||
description: |
|
||||
Full description: what it does, when to use it, and caveats.
|
||||
|
||||
parameters:
|
||||
- name: "target"
|
||||
type: "string"
|
||||
description: "Target parameter"
|
||||
required: true
|
||||
position: 0
|
||||
format: "positional"
|
||||
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: "Extra CLI arguments"
|
||||
required: false
|
||||
format: "positional"
|
||||
```
|
||||
|
||||
### Template with flag parameters
|
||||
|
||||
```yaml
|
||||
name: "tool_name"
|
||||
command: "command"
|
||||
enabled: true
|
||||
|
||||
short_description: "Short description"
|
||||
|
||||
description: |
|
||||
Full description.
|
||||
|
||||
parameters:
|
||||
- name: "target"
|
||||
type: "string"
|
||||
description: "Target"
|
||||
required: true
|
||||
flag: "-t"
|
||||
format: "flag"
|
||||
|
||||
- name: "option"
|
||||
type: "bool"
|
||||
description: "Option"
|
||||
required: false
|
||||
default: false
|
||||
flag: "--option"
|
||||
format: "flag"
|
||||
|
||||
- name: "level"
|
||||
type: "int"
|
||||
description: "Level"
|
||||
required: false
|
||||
default: 3
|
||||
flag: "--level"
|
||||
format: "combined"
|
||||
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: "Extra arguments"
|
||||
required: false
|
||||
format: "positional"
|
||||
```
|
||||
|
||||
## Related Documents
|
||||
|
||||
- Main project README: `../README.md`
|
||||
- Tool list samples: `tools/*.yaml`
|
||||
- API overview: see the main README
|
||||
|
||||
- Main project README: see `README.md` in the project root.
|
||||
- Tool list: all YAML configs under `tools/`.
|
||||
- API: see the main README for API details.
|
||||
|
||||
+8
-3
@@ -1,5 +1,8 @@
|
||||
name: "dalfox"
|
||||
command: "dalfox"
|
||||
# dalfox v2+ 使用子命令,单目标模式为 `dalfox url <target>`,不再支持根级的 -u
|
||||
args:
|
||||
- "url"
|
||||
enabled: true
|
||||
short_description: "高级XSS漏洞扫描器"
|
||||
description: |
|
||||
@@ -19,10 +22,12 @@ description: |
|
||||
parameters:
|
||||
- name: "url"
|
||||
type: "string"
|
||||
description: "目标URL"
|
||||
description: |
|
||||
目标URL。dalfox 单目标模式为子命令 url,此处作为 url 后的第一个位置参数传入。
|
||||
示例等价 CLI:dalfox url "http://target/page?q=test"
|
||||
required: true
|
||||
flag: "-u"
|
||||
format: "flag"
|
||||
position: 0
|
||||
format: "positional"
|
||||
- name: "pipe_mode"
|
||||
type: "bool"
|
||||
description: "使用管道模式输入"
|
||||
|
||||
@@ -143,6 +143,7 @@ parameters:
|
||||
format: "positional"
|
||||
- name: "payloads"
|
||||
type: "array"
|
||||
item_type: "string"
|
||||
description: "载荷列表(数组格式),如 [\"test1\", \"test2\", \"test3\"]"
|
||||
required: true
|
||||
position: 4
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
name: "nmap-advanced"
|
||||
command: "nmap"
|
||||
enabled: true
|
||||
short_description: "高级Nmap扫描,支持自定义NSE脚本和优化时序"
|
||||
description: |
|
||||
高级Nmap扫描工具,支持自定义NSE脚本、优化时序和多种扫描技术。
|
||||
|
||||
**主要功能:**
|
||||
- 多种扫描技术(SYN, TCP, UDP等)
|
||||
- 自定义NSE脚本
|
||||
- 时序优化
|
||||
- OS检测和版本检测
|
||||
|
||||
**使用场景:**
|
||||
- 高级网络扫描
|
||||
- 深度安全评估
|
||||
- 渗透测试
|
||||
- 网络侦察
|
||||
parameters:
|
||||
- name: "target"
|
||||
type: "string"
|
||||
description: "目标IP地址或主机名"
|
||||
required: true
|
||||
position: 0
|
||||
format: "positional"
|
||||
- name: "scan_type"
|
||||
type: "string"
|
||||
description: "扫描类型(-sS, -sT, -sU等)"
|
||||
required: false
|
||||
format: "template"
|
||||
template: "{value}"
|
||||
default: "-sS"
|
||||
- name: "ports"
|
||||
type: "string"
|
||||
description: "要扫描的端口"
|
||||
required: false
|
||||
flag: "-p"
|
||||
format: "flag"
|
||||
- name: "timing"
|
||||
type: "string"
|
||||
description: "时序模板(T0-T5)"
|
||||
required: false
|
||||
format: "template"
|
||||
template: "-T{value}"
|
||||
default: "4"
|
||||
- name: "nse_scripts"
|
||||
type: "string"
|
||||
description: "要运行的自定义NSE脚本"
|
||||
required: false
|
||||
flag: "--script"
|
||||
format: "flag"
|
||||
- name: "os_detection"
|
||||
type: "bool"
|
||||
description: "启用OS检测"
|
||||
required: false
|
||||
flag: "-O"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "version_detection"
|
||||
type: "bool"
|
||||
description: "启用版本检测"
|
||||
required: false
|
||||
flag: "-sV"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "aggressive"
|
||||
type: "bool"
|
||||
description: "启用激进扫描"
|
||||
required: false
|
||||
flag: "-A"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: |
|
||||
额外的nmap-advanced参数。用于传递未在参数列表中定义的nmap-advanced选项。
|
||||
|
||||
**示例值:**
|
||||
- 根据工具特性添加常用参数示例
|
||||
|
||||
**注意事项:**
|
||||
- 多个参数用空格分隔
|
||||
- 确保参数格式正确,避免命令注入
|
||||
- 此参数会直接追加到命令末尾
|
||||
required: false
|
||||
format: "positional"
|
||||
+53
-76
@@ -1,108 +1,85 @@
|
||||
name: "nmap"
|
||||
command: "nmap"
|
||||
args: ["-sT", "-sV", "-sC"] # 固定参数:TCP连接扫描、版本检测、默认脚本
|
||||
# 默认:TCP 连接扫描 + 版本检测 + 默认 NSE 脚本(无 root 也可用)
|
||||
args: ["-sT", "-sV", "-sC"]
|
||||
enabled: true
|
||||
# 简短描述(用于工具列表,减少token消耗)- 一句话说明工具用途
|
||||
short_description: "网络扫描工具,用于发现网络主机、开放端口和服务"
|
||||
# 工具详细描述 - 帮助大模型理解工具用途和使用场景
|
||||
short_description: "网络扫描:端口/服务/脚本;可选时序、自定义 NSE、OS 检测(需 root)"
|
||||
description: |
|
||||
网络映射和端口扫描工具,用于发现网络中的主机、服务和开放端口。
|
||||
网络映射与端口扫描。
|
||||
|
||||
**主要功能:**
|
||||
- 主机发现:检测网络中的活动主机
|
||||
- 端口扫描:识别目标主机上开放的端口
|
||||
- 服务识别:检测运行在端口上的服务类型和版本
|
||||
- 操作系统检测:识别目标主机的操作系统类型
|
||||
- 漏洞检测:使用NSE脚本检测常见漏洞
|
||||
**默认行为(只传 target/ports 即可):**
|
||||
- `-sT` TCP 连接扫描(无需 root)
|
||||
- `-sV` 版本检测
|
||||
- `-sC` 默认 NSE 脚本
|
||||
|
||||
**使用场景:**
|
||||
- 网络资产发现和枚举
|
||||
- 安全评估和渗透测试
|
||||
- 网络故障排查
|
||||
- 端口和服务审计
|
||||
**可选增强:**
|
||||
- `timing`:`-T0`~`-T5` 时序
|
||||
- `nse_scripts`:`--script` 自定义脚本(如 `vuln`、`http-*`)
|
||||
- `os_detection`:`-O` **必须 root**,否则 nmap 会 QUITTING
|
||||
- `aggressive`:`-A` **必须 root**(含 OS 检测)
|
||||
- `scan_type`:若传入则**整段替换**上述默认 `-sT -sV -sC`,需自行写上需要的选项(如 `-sT -sV`)
|
||||
|
||||
**注意事项:**
|
||||
- 使用 -sT (TCP连接扫描) 而不是 -sS (SYN扫描),因为 -sS 需要root权限
|
||||
- 扫描速度取决于网络延迟和目标响应
|
||||
- 某些扫描可能被防火墙或IDS检测到
|
||||
- 请确保有权限扫描目标网络
|
||||
# 参数定义
|
||||
- `-sS` SYN 扫描需要 root;无 root 请用默认或 `-sT`
|
||||
- 扫描全端口 `1-65535` 非常慢,建议先常用端口
|
||||
- 请确保有权限扫描目标
|
||||
parameters:
|
||||
- name: "target"
|
||||
type: "string"
|
||||
description: |
|
||||
目标IP地址或域名。可以是单个IP、IP范围、CIDR格式或域名。
|
||||
目标 IP、主机名、CIDR 或域名;URL 会自动提取主机部分。
|
||||
|
||||
**示例值:**
|
||||
- 单个IP: "192.168.1.1"
|
||||
- IP范围: "192.168.1.1-100"
|
||||
- CIDR: "192.168.1.0/24"
|
||||
- 域名: "example.com"
|
||||
- URL: "https://example.com" (会自动提取域名部分)
|
||||
|
||||
**注意事项:**
|
||||
- 如果提供URL,会自动提取域名部分
|
||||
- 确保目标地址格式正确
|
||||
- 必需参数,不能为空
|
||||
**示例:** `192.168.1.1`、`10.0.0.0/24`、`example.com`
|
||||
required: true
|
||||
position: 1 # 位置参数,必须放在命令最后(nmap [options] target),用 1 确保在 flag 之后、最后添加
|
||||
position: 1
|
||||
format: "positional"
|
||||
- name: "ports"
|
||||
type: "string"
|
||||
description: |
|
||||
要扫描的端口范围。可以是单个端口、端口范围、逗号分隔的端口列表,或特殊值。
|
||||
|
||||
**示例值:**
|
||||
- 单个端口: "80"
|
||||
- 端口范围: "1-1000"
|
||||
- 多个端口: "80,443,8080,8443"
|
||||
- 组合: "80,443,8000-9000"
|
||||
- 常用端口: "1-1024"
|
||||
- 所有端口: "1-65535"
|
||||
- 快速扫描: "80,443,22,21,25,53,110,143,993,995"
|
||||
|
||||
**注意事项:**
|
||||
- 如果不指定,将扫描默认的1000个常用端口
|
||||
- 扫描所有端口(1-65535)会非常耗时
|
||||
- 建议先扫描常用端口,再根据结果决定是否扫描全部端口
|
||||
端口范围。示例:`80`、`1-1000`、`80,443,8080`、`1-65535`(全端口很慢)
|
||||
required: false
|
||||
flag: "-p"
|
||||
format: "flag"
|
||||
- name: "timing"
|
||||
type: "string"
|
||||
description: "时序模板 T0–T5,数字越大越快。示例:`4` 生成 `-T4`"
|
||||
required: false
|
||||
format: "template"
|
||||
template: "-T{value}"
|
||||
- name: "nse_scripts"
|
||||
type: "string"
|
||||
description: "NSE 脚本,传给 `--script`。示例:`vuln`、`http-title,http-headers`"
|
||||
required: false
|
||||
flag: "--script"
|
||||
format: "flag"
|
||||
- name: "os_detection"
|
||||
type: "bool"
|
||||
description: |
|
||||
启用 `-O` OS 检测。**必须 root**;无 root 请保持 false。
|
||||
required: false
|
||||
flag: "-O"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "aggressive"
|
||||
type: "bool"
|
||||
description: |
|
||||
启用 `-A` 激进扫描(含 OS 检测)。**必须 root**;无 root 请保持 false。
|
||||
required: false
|
||||
flag: "-A"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "scan_type"
|
||||
type: "string"
|
||||
description: |
|
||||
扫描类型选项。可以覆盖默认的扫描类型。
|
||||
|
||||
**常用选项:**
|
||||
- "-sV": 版本检测
|
||||
- "-sC": 默认脚本扫描
|
||||
- "-sS": SYN扫描(需要root权限)
|
||||
- "-sT": TCP连接扫描(默认)
|
||||
- "-sU": UDP扫描
|
||||
- "-A": 全面扫描(OS检测、版本检测、脚本扫描、路由追踪)
|
||||
|
||||
**注意事项:**
|
||||
- 多个选项可以组合,用空格分隔,例如: "-sV -sC"
|
||||
- 默认已包含 "-sT -sV -sC"
|
||||
- 如果指定此参数,将替换默认的扫描类型
|
||||
扫描类型选项;**若填写则替换默认的 `-sT -sV -sC`**,只保留你写的选项。
|
||||
多选项用空格分隔,例如:`-sT -sV`、`-sU`(UDP)。
|
||||
required: false
|
||||
format: "template"
|
||||
template: "{value}"
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: |
|
||||
额外的Nmap参数。用于传递未在参数列表中定义的Nmap选项。
|
||||
|
||||
**示例值:**
|
||||
- "--script vuln": 运行漏洞检测脚本
|
||||
- "-O": 操作系统检测
|
||||
- "-T4": 时间模板(0-5,数字越大越快)
|
||||
- "--max-retries 3": 最大重试次数
|
||||
- "-v": 详细输出
|
||||
|
||||
**注意事项:**
|
||||
- 多个参数用空格分隔
|
||||
- 确保参数格式正确,避免命令注入
|
||||
- 此参数会直接追加到命令末尾
|
||||
额外参数,按空格追加到命令末尾。
|
||||
示例:`--max-retries 3`、`-v`、`-Pn`
|
||||
required: false
|
||||
format: "positional"
|
||||
|
||||
+119
-25
@@ -1,58 +1,152 @@
|
||||
name: "rustscan"
|
||||
command: "rustscan"
|
||||
enabled: true
|
||||
short_description: "超快速端口扫描工具,使用Rust编写"
|
||||
short_description: "超快速端口扫描(Rust);可选 greppable、批量与脚本级别"
|
||||
description: |
|
||||
Rustscan是一个用Rust编写的超快速端口扫描工具,可以快速扫描大量端口。
|
||||
RustScan 2.x:快速端口发现,可选再调 Nmap 脚本。
|
||||
|
||||
**主要功能:**
|
||||
- 超快速端口扫描
|
||||
- 可配置的扫描速度
|
||||
- 支持Nmap脚本集成
|
||||
- 批量扫描支持
|
||||
**与 `rustscan -h` 对应关系:**
|
||||
- `-a` / `--addresses`:扫描目标列表(逗号分隔或文件)
|
||||
- `-p`:逗号分隔端口;`-r`:端口范围 `start-end`(二选一或与 `-p` 配合以 CLI 为准)
|
||||
- `-g` / `--greppable`:只输出端口,便于 grep/管道
|
||||
- `--scripts`:**官方默认是 default**(会跑 Nmap);设为 **none** 可只做端口发现、更快
|
||||
- `-b` batch-size、`-t` timeout、`--scan-order` 等可微调速度与顺序
|
||||
|
||||
**使用场景:**
|
||||
- 快速端口扫描
|
||||
- 大规模网络扫描
|
||||
- 渗透测试信息收集
|
||||
**使用建议:**
|
||||
- 快速扫端口、不要 Nmap:`scripts` 用 `none`,必要时加 `-g`
|
||||
- 需要服务识别/脚本:用 `default` 或 `custom`,并确保本机有 nmap
|
||||
parameters:
|
||||
# -a, --addresses
|
||||
- name: "target"
|
||||
type: "string"
|
||||
description: "目标IP地址或主机名"
|
||||
description: |
|
||||
对应 `-a`:逗号分隔的 CIDR、IP 或主机名;也可为含换行/列表的文件路径。
|
||||
示例:`192.168.1.1`、`10.0.0.1,10.0.0.2`、`192.168.0.0/24`
|
||||
required: true
|
||||
flag: "-a"
|
||||
format: "flag"
|
||||
|
||||
# -p, --ports;范围请用 range (-r),勿把 1-1000 填进 ports
|
||||
- name: "ports"
|
||||
type: "string"
|
||||
description: "要扫描的端口(如:22,80,443或1-1000)"
|
||||
description: |
|
||||
要扫描的端口,**仅**逗号分隔列表,对应 `-p`。
|
||||
示例:`22,80,443`、`80,443,8080`。
|
||||
若要范围如 `1-1000`,请用参数 **range**(`-r`),不要写在本参数。
|
||||
required: false
|
||||
flag: "-p"
|
||||
format: "flag"
|
||||
|
||||
# -r, --range(与 ports 列表二选一或按官方说明组合)
|
||||
- name: "range"
|
||||
type: "string"
|
||||
description: |
|
||||
端口范围,格式 start-end,对应 `-r`。
|
||||
示例:`1-1000`、`1-65535`(全端口很慢)。
|
||||
离散端口如 `22,80,443` 请用 **ports**(`-p`),不要写在本参数。
|
||||
required: false
|
||||
flag: "-r"
|
||||
format: "flag"
|
||||
|
||||
# -u, --ulimit
|
||||
- name: "ulimit"
|
||||
type: "int"
|
||||
description: "文件描述符限制"
|
||||
description: "提升扫描用的 ulimit;依系统 fd 限制调整"
|
||||
required: false
|
||||
flag: "-u"
|
||||
format: "flag"
|
||||
default: 5000
|
||||
|
||||
# --scripts:none | default | custom(CLI 默认 default)
|
||||
- name: "scripts"
|
||||
type: "bool"
|
||||
description: "在发现的端口上运行Nmap脚本"
|
||||
type: "string"
|
||||
description: |
|
||||
脚本级别;**必须**传字符串,勿传 true/false。
|
||||
- **none**:不跑 Nmap,仅端口发现(最快)
|
||||
- **default**:与 rustscan 官方默认一致,会调 Nmap
|
||||
- **custom**:自定义脚本,常需配合 additional_args
|
||||
required: false
|
||||
flag: "--scripts"
|
||||
format: "flag"
|
||||
default: "none"
|
||||
options:
|
||||
- "none"
|
||||
- "default"
|
||||
- "custom"
|
||||
|
||||
# -g, --greppable:仅端口列表,无 Nmap 输出
|
||||
- name: "greppable"
|
||||
type: "bool"
|
||||
description: "Greppable 模式:只输出端口,适合脚本解析或写入文件"
|
||||
required: false
|
||||
flag: "-g"
|
||||
format: "flag"
|
||||
default: false
|
||||
|
||||
# -b, --batch-size [default: 4500]
|
||||
- name: "batch_size"
|
||||
type: "int"
|
||||
description: "每批并发端口数;越大越快,受 OS 打开文件数限制。官方默认 4500"
|
||||
required: false
|
||||
flag: "-b"
|
||||
format: "flag"
|
||||
|
||||
# -t, --timeout ms [default: 1500]
|
||||
- name: "timeout_ms"
|
||||
type: "int"
|
||||
description: "单端口超时(毫秒)。官方默认 1500"
|
||||
required: false
|
||||
flag: "-t"
|
||||
format: "flag"
|
||||
|
||||
# --scan-order serial | random [default: serial]
|
||||
- name: "scan_order"
|
||||
type: "string"
|
||||
description: "扫描顺序:serial 升序;random 随机"
|
||||
required: false
|
||||
flag: "--scan-order"
|
||||
format: "flag"
|
||||
options:
|
||||
- "serial"
|
||||
- "random"
|
||||
|
||||
# --top:top 1000 端口
|
||||
- name: "top_ports"
|
||||
type: "bool"
|
||||
description: "使用内置 top 1000 端口(等价于传 `--top`)"
|
||||
required: false
|
||||
flag: "--top"
|
||||
format: "flag"
|
||||
default: false
|
||||
|
||||
# -e, --exclude-ports
|
||||
- name: "exclude_ports"
|
||||
type: "string"
|
||||
description: "排除端口,逗号分隔。示例:`80,443`"
|
||||
required: false
|
||||
flag: "-e"
|
||||
format: "flag"
|
||||
|
||||
# -x, --exclude-addresses
|
||||
- name: "exclude_addresses"
|
||||
type: "string"
|
||||
description: "排除地址,逗号分隔 CIDR/IP/主机"
|
||||
required: false
|
||||
flag: "-x"
|
||||
format: "flag"
|
||||
|
||||
# --tries [default: 1]
|
||||
- name: "tries"
|
||||
type: "int"
|
||||
description: "判定关闭前的重试次数;0 会被纠正为 1"
|
||||
required: false
|
||||
flag: "--tries"
|
||||
format: "flag"
|
||||
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: |
|
||||
额外的rustscan参数。用于传递未在参数列表中定义的rustscan选项。
|
||||
|
||||
**示例值:**
|
||||
- 根据工具特性添加常用参数示例
|
||||
|
||||
**注意事项:**
|
||||
- 多个参数用空格分隔
|
||||
- 确保参数格式正确,避免命令注入
|
||||
- 此参数会直接追加到命令末尾
|
||||
未列在上面的选项可写在这里,空格分隔。
|
||||
示例:`--no-banner`、`--udp`、`-n`(忽略配置文件)、`-c /path/config.toml`、`--resolver 8.8.8.8`
|
||||
required: false
|
||||
format: "positional"
|
||||
|
||||
+1042
-2
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,7 @@
|
||||
"infoCollect": "Recon",
|
||||
"tasks": "Tasks",
|
||||
"vulnerabilities": "Vulnerabilities",
|
||||
"webshell": "WebShell Management",
|
||||
"mcp": "MCP",
|
||||
"mcpMonitor": "MCP Monitor",
|
||||
"mcpManagement": "MCP Management",
|
||||
@@ -104,6 +105,7 @@
|
||||
},
|
||||
"chat": {
|
||||
"newChat": "New chat",
|
||||
"toggleConversationPanel": "Collapse/expand conversation list",
|
||||
"searchHistory": "Search history...",
|
||||
"conversationGroups": "Conversation groups",
|
||||
"addGroup": "New group",
|
||||
@@ -169,7 +171,9 @@
|
||||
"lastIterSummary": "Last iteration: generating summary and next steps...",
|
||||
"summaryDone": "Summary complete",
|
||||
"generatingFinalReply": "Generating final reply...",
|
||||
"maxIterSummary": "Max iterations reached, generating summary..."
|
||||
"maxIterSummary": "Max iterations reached, generating summary...",
|
||||
"analyzingRequestShort": "Analyzing your request...",
|
||||
"analyzingRequestPlanning": "Analyzing your request and planning test strategy..."
|
||||
},
|
||||
"timeline": {
|
||||
"params": "Parameters:",
|
||||
@@ -325,6 +329,72 @@
|
||||
"loadFailed": "Failed to load vulnerabilities",
|
||||
"deleteConfirm": "Delete this vulnerability?"
|
||||
},
|
||||
"webshell": {
|
||||
"title": "WebShell Management",
|
||||
"addConnection": "Add connection",
|
||||
"connections": "Connections",
|
||||
"noConnections": "No connections. Click \"Add connection\" to add one.",
|
||||
"selectOrAdd": "Select a connection from the list or add a new WebShell connection.",
|
||||
"url": "Shell URL",
|
||||
"urlPlaceholder": "http(s)://target.com/shell.php",
|
||||
"password": "Password / Key",
|
||||
"passwordPlaceholder": "e.g. IceSword/AntSword connection password",
|
||||
"method": "Request method",
|
||||
"methodPost": "POST",
|
||||
"methodGet": "GET",
|
||||
"type": "Shell type",
|
||||
"typePhp": "PHP",
|
||||
"typeAsp": "ASP",
|
||||
"typeAspx": "ASPX",
|
||||
"typeJsp": "JSP",
|
||||
"typeCustom": "Custom",
|
||||
"cmdParam": "Command parameter name",
|
||||
"cmdParamPlaceholder": "Leave empty for cmd; e.g. xxx for xxx=command",
|
||||
"remark": "Remark",
|
||||
"remarkPlaceholder": "Friendly name for this connection",
|
||||
"deleteConfirm": "Delete this connection?",
|
||||
"editConnection": "Edit",
|
||||
"editConnectionTitle": "Edit connection",
|
||||
"tabTerminal": "Virtual terminal",
|
||||
"tabFileManager": "File manager",
|
||||
"tabAiAssistant": "AI Assistant",
|
||||
"aiSystemReadyMessage": "System is ready. Please enter your test requirements, and the system will automatically perform the corresponding security tests.",
|
||||
"aiNewConversation": "New conversation",
|
||||
"aiPreviousConversation": "Previous conversation",
|
||||
"aiDeleteConversation": "Delete conversation",
|
||||
"aiDeleteConversationConfirm": "Delete this conversation?",
|
||||
"aiPlaceholder": "e.g. List files in the current directory",
|
||||
"aiSend": "Send",
|
||||
"quickCommands": "Quick commands",
|
||||
"downloadFile": "Download",
|
||||
"terminalWelcome": "WebShell virtual terminal — type a command and press Enter (Ctrl+L clear)",
|
||||
"filePath": "Current path",
|
||||
"listDir": "List directory",
|
||||
"readFile": "Read",
|
||||
"editFile": "Edit",
|
||||
"deleteFile": "Delete",
|
||||
"saveFile": "Save",
|
||||
"cancelEdit": "Cancel",
|
||||
"parentDir": "Parent directory",
|
||||
"execError": "Execution failed",
|
||||
"testConnectivity": "Test connectivity",
|
||||
"testSuccess": "Connection OK, shell is reachable",
|
||||
"testFailed": "Connectivity test failed",
|
||||
"testNoExpectedOutput": "Shell responded but expected output was not found. Check password and command parameter name.",
|
||||
"clearScreen": "Clear",
|
||||
"running": "Running…",
|
||||
"waitFinish": "Please wait for the current command to finish",
|
||||
"newDir": "New directory",
|
||||
"rename": "Rename",
|
||||
"upload": "Upload",
|
||||
"newFile": "New file",
|
||||
"filterPlaceholder": "Filter by name",
|
||||
"batchDelete": "Batch delete",
|
||||
"batchDownload": "Batch download",
|
||||
"refresh": "Refresh",
|
||||
"selectAll": "Select all",
|
||||
"breadcrumbHome": "Root"
|
||||
},
|
||||
"mcp": {
|
||||
"monitorTitle": "MCP Status Monitor",
|
||||
"execStats": "Execution stats",
|
||||
@@ -404,7 +474,12 @@
|
||||
"totalCount": "Total",
|
||||
"enabledCount": "Enabled",
|
||||
"disabledCount": "Disabled",
|
||||
"connectedCount": "Connected"
|
||||
"connectedCount": "Connected",
|
||||
"toolsCountValue": "🔧 {{count}} tools",
|
||||
"connectionErrorLabel": "Connection error:",
|
||||
"secondsUnit": "s",
|
||||
"urlLabel": "URL",
|
||||
"loadExternalMCPFailed": "Load failed"
|
||||
},
|
||||
"settings": {
|
||||
"title": "System settings",
|
||||
@@ -1022,8 +1097,15 @@
|
||||
"title": "Terminal",
|
||||
"description": "Run commands on the server for ops and debugging. Commands run on the server; avoid sensitive or destructive operations.",
|
||||
"terminalTab": "Terminal {{n}}",
|
||||
"close": "Close",
|
||||
"newTerminal": "New terminal"
|
||||
"welcomeLine": "CyberStrikeAI Terminal — real shell session; type commands directly. Ctrl+L to clear screen",
|
||||
"sessionClosed": "[Session closed]",
|
||||
"connectionError": "[Terminal connection error]",
|
||||
"connectFailed": "[Cannot connect to terminal service: {{msg}}]",
|
||||
"closeTabTitle": "Close",
|
||||
"containerClickTitle": "Click here, then type commands",
|
||||
"xtermNotLoaded": "xterm.js failed to load. Refresh the page or check your network.",
|
||||
"close": "×",
|
||||
"newTerminal": "+"
|
||||
},
|
||||
"settingsSecurity": {
|
||||
"changePasswordTitle": "Change password",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"infoCollect": "信息收集",
|
||||
"tasks": "任务管理",
|
||||
"vulnerabilities": "漏洞管理",
|
||||
"webshell": "WebShell管理",
|
||||
"mcp": "MCP",
|
||||
"mcpMonitor": "MCP状态监控",
|
||||
"mcpManagement": "MCP管理",
|
||||
@@ -104,6 +105,7 @@
|
||||
},
|
||||
"chat": {
|
||||
"newChat": "新对话",
|
||||
"toggleConversationPanel": "折叠/展开对话列表",
|
||||
"searchHistory": "搜索历史记录...",
|
||||
"conversationGroups": "对话分组",
|
||||
"addGroup": "新建分组",
|
||||
@@ -169,7 +171,9 @@
|
||||
"lastIterSummary": "最后一次迭代:正在生成总结和下一步计划...",
|
||||
"summaryDone": "总结生成完成",
|
||||
"generatingFinalReply": "正在生成最终回复...",
|
||||
"maxIterSummary": "达到最大迭代次数,正在生成总结..."
|
||||
"maxIterSummary": "达到最大迭代次数,正在生成总结...",
|
||||
"analyzingRequestShort": "正在分析您的请求...",
|
||||
"analyzingRequestPlanning": "开始分析请求并制定测试策略"
|
||||
},
|
||||
"timeline": {
|
||||
"params": "参数:",
|
||||
@@ -325,6 +329,72 @@
|
||||
"loadFailed": "加载漏洞失败",
|
||||
"deleteConfirm": "确定要删除此漏洞吗?"
|
||||
},
|
||||
"webshell": {
|
||||
"title": "WebShell 管理",
|
||||
"addConnection": "添加连接",
|
||||
"connections": "连接列表",
|
||||
"noConnections": "暂无连接,请点击「添加连接」",
|
||||
"selectOrAdd": "请从左侧选择连接,或添加新的 WebShell 连接",
|
||||
"url": "Shell 地址",
|
||||
"urlPlaceholder": "http(s)://target.com/shell.php",
|
||||
"password": "连接密码/密钥",
|
||||
"passwordPlaceholder": "如冰蝎/蚁剑的连接密码",
|
||||
"method": "请求方式",
|
||||
"methodPost": "POST",
|
||||
"methodGet": "GET",
|
||||
"type": "Shell 类型",
|
||||
"typePhp": "PHP",
|
||||
"typeAsp": "ASP",
|
||||
"typeAspx": "ASPX",
|
||||
"typeJsp": "JSP",
|
||||
"typeCustom": "自定义",
|
||||
"cmdParam": "命令参数名",
|
||||
"cmdParamPlaceholder": "不填默认为 cmd,如填 xxx 则请求为 xxx=命令",
|
||||
"remark": "备注",
|
||||
"remarkPlaceholder": "便于识别的备注名",
|
||||
"deleteConfirm": "确定要删除该连接吗?",
|
||||
"editConnection": "编辑",
|
||||
"editConnectionTitle": "编辑连接",
|
||||
"tabTerminal": "虚拟终端",
|
||||
"tabFileManager": "文件管理",
|
||||
"tabAiAssistant": "AI 助手",
|
||||
"aiSystemReadyMessage": "系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。",
|
||||
"aiNewConversation": "新对话",
|
||||
"aiPreviousConversation": "之前的对话",
|
||||
"aiDeleteConversation": "删除对话",
|
||||
"aiDeleteConversationConfirm": "确定删除当前对话记录?",
|
||||
"aiPlaceholder": "例如:列出当前目录下的文件",
|
||||
"aiSend": "发送",
|
||||
"quickCommands": "快捷命令",
|
||||
"downloadFile": "下载",
|
||||
"terminalWelcome": "WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)",
|
||||
"filePath": "当前路径",
|
||||
"listDir": "列出目录",
|
||||
"readFile": "读取",
|
||||
"editFile": "编辑",
|
||||
"deleteFile": "删除",
|
||||
"saveFile": "保存",
|
||||
"cancelEdit": "取消",
|
||||
"parentDir": "上级目录",
|
||||
"execError": "执行失败",
|
||||
"testConnectivity": "测试连通性",
|
||||
"testSuccess": "连通性正常,Shell 可访问",
|
||||
"testFailed": "连通性测试失败",
|
||||
"testNoExpectedOutput": "Shell 返回了响应但未得到预期输出,请检查连接密码与命令参数名",
|
||||
"clearScreen": "清屏",
|
||||
"running": "执行中…",
|
||||
"waitFinish": "请等待当前命令执行完成",
|
||||
"newDir": "新建目录",
|
||||
"rename": "重命名",
|
||||
"upload": "上传",
|
||||
"newFile": "新建文件",
|
||||
"filterPlaceholder": "过滤文件名",
|
||||
"batchDelete": "批量删除",
|
||||
"batchDownload": "批量下载",
|
||||
"refresh": "刷新",
|
||||
"selectAll": "全选",
|
||||
"breadcrumbHome": "根"
|
||||
},
|
||||
"mcp": {
|
||||
"monitorTitle": "MCP 状态监控",
|
||||
"execStats": "执行统计",
|
||||
@@ -404,7 +474,12 @@
|
||||
"totalCount": "总数",
|
||||
"enabledCount": "已启用",
|
||||
"disabledCount": "已停用",
|
||||
"connectedCount": "已连接"
|
||||
"connectedCount": "已连接",
|
||||
"toolsCountValue": "🔧 {{count}} 个工具",
|
||||
"connectionErrorLabel": "连接错误:",
|
||||
"secondsUnit": "秒",
|
||||
"urlLabel": "URL",
|
||||
"loadExternalMCPFailed": "加载失败"
|
||||
},
|
||||
"settings": {
|
||||
"title": "系统设置",
|
||||
@@ -1022,8 +1097,15 @@
|
||||
"title": "终端",
|
||||
"description": "在服务器上执行命令,便于运维与调试。命令在服务端执行,请勿执行敏感或破坏性操作。",
|
||||
"terminalTab": "终端 {{n}}",
|
||||
"close": "关闭",
|
||||
"newTerminal": "新终端"
|
||||
"welcomeLine": "CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏",
|
||||
"sessionClosed": "[会话已关闭]",
|
||||
"connectionError": "[终端连接出错]",
|
||||
"connectFailed": "[无法连接终端服务: {{msg}}]",
|
||||
"closeTabTitle": "关闭",
|
||||
"containerClickTitle": "点击此处后输入命令",
|
||||
"xtermNotLoaded": "未加载 xterm.js,请刷新页面或检查网络。",
|
||||
"close": "×",
|
||||
"newTerminal": "+"
|
||||
},
|
||||
"settingsSecurity": {
|
||||
"changePasswordTitle": "修改密码",
|
||||
|
||||
@@ -242,6 +242,14 @@ async function refreshAppData(showTaskErrors = false) {
|
||||
|
||||
async function bootstrapApp() {
|
||||
if (!isAppInitialized) {
|
||||
// 等待 i18n 首包加载完成后再插系统就绪消息,避免清除缓存后语言显示 English 气泡仍是中文
|
||||
try {
|
||||
if (window.i18nReady && typeof window.i18nReady.then === 'function') {
|
||||
await window.i18nReady;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('等待 i18n 就绪失败,继续初始化聊天', e);
|
||||
}
|
||||
initializeChatUI();
|
||||
isAppInitialized = true;
|
||||
}
|
||||
|
||||
+204
-37
@@ -953,7 +953,7 @@ function initializeChatUI() {
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
if (messagesDiv && messagesDiv.childElementCount === 0) {
|
||||
const readyMsg = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsg);
|
||||
addMessage('assistant', readyMsg, null, null, null, { systemReadyMessage: true });
|
||||
}
|
||||
|
||||
addAttackChainButton(currentConversationId);
|
||||
@@ -989,8 +989,60 @@ function wrapTablesInBubble(bubble) {
|
||||
});
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
function addMessage(role, content, mcpExecutionIds = null, progressId = null, createdAt = null) {
|
||||
/**
|
||||
* 将「系统已就绪」类文案按当前语言重新渲染进气泡(与 addMessage 助手分支一致的安全处理)
|
||||
*/
|
||||
function refreshSystemReadyMessageBubbles() {
|
||||
if (typeof window.t !== 'function') return;
|
||||
const text = window.t('chat.systemReadyMessage');
|
||||
const escapeHtmlLocal = (s) => {
|
||||
if (!s) return '';
|
||||
const div = document.createElement('div');
|
||||
div.textContent = s;
|
||||
return div.innerHTML;
|
||||
};
|
||||
const defaultSanitizeConfig = {
|
||||
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
|
||||
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
|
||||
ALLOW_DATA_ATTR: false,
|
||||
};
|
||||
let formattedContent;
|
||||
if (typeof marked !== 'undefined') {
|
||||
try {
|
||||
marked.setOptions({ breaks: true, gfm: true });
|
||||
const parsed = marked.parse(text);
|
||||
formattedContent = typeof DOMPurify !== 'undefined'
|
||||
? DOMPurify.sanitize(parsed, defaultSanitizeConfig)
|
||||
: parsed;
|
||||
} catch (e) {
|
||||
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
} else {
|
||||
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
document.querySelectorAll('.message.assistant[data-system-ready-message]').forEach(function (messageDiv) {
|
||||
const bubble = messageDiv.querySelector('.message-bubble');
|
||||
if (!bubble) return;
|
||||
const copyBtn = bubble.querySelector('.message-copy-btn');
|
||||
if (copyBtn) copyBtn.remove();
|
||||
bubble.innerHTML = formattedContent;
|
||||
if (typeof wrapTablesInBubble === 'function') wrapTablesInBubble(bubble);
|
||||
messageDiv.dataset.originalContent = text;
|
||||
const copyBtnNew = document.createElement('button');
|
||||
copyBtnNew.className = 'message-copy-btn';
|
||||
copyBtnNew.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>' + window.t('common.copy') + '</span>';
|
||||
copyBtnNew.title = window.t('chat.copyMessageTitle');
|
||||
copyBtnNew.onclick = function (e) {
|
||||
e.stopPropagation();
|
||||
copyMessageToClipboard(messageDiv, this);
|
||||
};
|
||||
bubble.appendChild(copyBtnNew);
|
||||
});
|
||||
}
|
||||
|
||||
// 添加消息(options.systemReadyMessage 为 true 时,语言切换会刷新该条文案)
|
||||
function addMessage(role, content, mcpExecutionIds = null, progressId = null, createdAt = null, options = null) {
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
const messageDiv = document.createElement('div');
|
||||
messageCounter++;
|
||||
@@ -1189,7 +1241,12 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
messageTime = new Date();
|
||||
}
|
||||
const msgTimeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
|
||||
timeDiv.textContent = messageTime.toLocaleTimeString(msgTimeLocale, { hour: '2-digit', minute: '2-digit' });
|
||||
const msgTimeOpts = { hour: '2-digit', minute: '2-digit' };
|
||||
if (msgTimeLocale === 'zh-CN') msgTimeOpts.hour12 = false;
|
||||
timeDiv.textContent = messageTime.toLocaleTimeString(msgTimeLocale, msgTimeOpts);
|
||||
try {
|
||||
timeDiv.dataset.messageTime = messageTime.toISOString();
|
||||
} catch (e) { /* ignore */ }
|
||||
contentWrapper.appendChild(timeDiv);
|
||||
|
||||
// 如果有MCP执行ID或进度ID,添加查看详情区域(统一使用"渗透测试详情"样式)
|
||||
@@ -1234,6 +1291,10 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
}
|
||||
|
||||
messageDiv.appendChild(contentWrapper);
|
||||
// 标记「系统就绪」占位消息,便于切换语言后刷新文案
|
||||
if (options && options.systemReadyMessage) {
|
||||
messageDiv.setAttribute('data-system-ready-message', '1');
|
||||
}
|
||||
messagesDiv.appendChild(messageDiv);
|
||||
messagesDiv.scrollTop = messagesDiv.scrollHeight;
|
||||
return id;
|
||||
@@ -1536,10 +1597,19 @@ async function showMCPDetail(executionId) {
|
||||
const normalizedStatus = (exec.status || 'unknown').toLowerCase();
|
||||
statusEl.textContent = getStatusText(exec.status);
|
||||
statusEl.className = `status-chip status-${normalizedStatus}`;
|
||||
try {
|
||||
statusEl.dataset.detailStatus = (exec.status || '') + '';
|
||||
} catch (e) { /* ignore */ }
|
||||
const detailTimeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
|
||||
document.getElementById('detail-time').textContent = exec.startTime
|
||||
? new Date(exec.startTime).toLocaleString(detailTimeLocale)
|
||||
: '—';
|
||||
const detailTimeEl = document.getElementById('detail-time');
|
||||
if (detailTimeEl) {
|
||||
detailTimeEl.textContent = exec.startTime
|
||||
? new Date(exec.startTime).toLocaleString(detailTimeLocale)
|
||||
: '—';
|
||||
try {
|
||||
detailTimeEl.dataset.detailTimeIso = exec.startTime ? new Date(exec.startTime).toISOString() : '';
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
// 请求参数
|
||||
const requestData = {
|
||||
@@ -1712,7 +1782,7 @@ async function startNewConversation() {
|
||||
currentConversationGroupId = null; // 新对话不属于任何分组
|
||||
document.getElementById('chat-messages').innerHTML = '';
|
||||
const readyMsgNew = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsgNew);
|
||||
addMessage('assistant', readyMsgNew, null, null, null, { systemReadyMessage: true });
|
||||
addAttackChainButton(null);
|
||||
updateActiveConversation();
|
||||
// 刷新分组列表,清除分组高亮
|
||||
@@ -1957,33 +2027,24 @@ function formatConversationTimestamp(dateObj, todayStart, yesterdayStart) {
|
||||
const fmtLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
|
||||
const yesterdayLabel = typeof window.t === 'function' ? window.t('chat.yesterday') : '昨天';
|
||||
|
||||
const timeOnlyOpts = { hour: '2-digit', minute: '2-digit' };
|
||||
const dateTimeOpts = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
||||
const fullDateOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
|
||||
if (fmtLocale === 'zh-CN') {
|
||||
timeOnlyOpts.hour12 = false;
|
||||
dateTimeOpts.hour12 = false;
|
||||
fullDateOpts.hour12 = false;
|
||||
}
|
||||
if (messageDate.getTime() === referenceToday.getTime()) {
|
||||
return dateObj.toLocaleTimeString(fmtLocale, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
return dateObj.toLocaleTimeString(fmtLocale, timeOnlyOpts);
|
||||
}
|
||||
if (messageDate.getTime() === referenceYesterday.getTime()) {
|
||||
return yesterdayLabel + ' ' + dateObj.toLocaleTimeString(fmtLocale, {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
return yesterdayLabel + ' ' + dateObj.toLocaleTimeString(fmtLocale, timeOnlyOpts);
|
||||
}
|
||||
if (dateObj.getFullYear() === referenceToday.getFullYear()) {
|
||||
return dateObj.toLocaleString(fmtLocale, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
return dateObj.toLocaleString(fmtLocale, dateTimeOpts);
|
||||
}
|
||||
return dateObj.toLocaleString(fmtLocale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
return dateObj.toLocaleString(fmtLocale, fullDateOpts);
|
||||
}
|
||||
|
||||
function getConversationGroup(dateObj, todayStart, startOfWeek, yesterdayStart) {
|
||||
@@ -2127,7 +2188,7 @@ async function loadConversation(conversationId) {
|
||||
});
|
||||
} else {
|
||||
const readyMsgEmpty = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsgEmpty);
|
||||
addMessage('assistant', readyMsgEmpty, null, null, null, { systemReadyMessage: true });
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
@@ -2168,7 +2229,7 @@ async function deleteConversation(conversationId, skipConfirm = false) {
|
||||
currentConversationId = null;
|
||||
document.getElementById('chat-messages').innerHTML = '';
|
||||
const readyMsgLoad = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsgLoad);
|
||||
addMessage('assistant', readyMsgLoad, null, null, null, { systemReadyMessage: true });
|
||||
addAttackChainButton(null);
|
||||
}
|
||||
|
||||
@@ -2182,8 +2243,16 @@ async function deleteConversation(conversationId, skipConfirm = false) {
|
||||
await loadGroupConversations(currentGroupId);
|
||||
}
|
||||
|
||||
// 刷新对话列表
|
||||
loadConversations();
|
||||
// 刷新对话列表(使用分组接口以与其他入口一致)
|
||||
if (typeof loadConversationsWithGroups === 'function') {
|
||||
loadConversationsWithGroups();
|
||||
} else if (typeof loadConversations === 'function') {
|
||||
loadConversations();
|
||||
}
|
||||
// 通知其他模块(如 WebShell AI 助手)同步删除,保持列表一致
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('conversation-deleted', { detail: { conversationId } }));
|
||||
} catch (e) { /* ignore */ }
|
||||
} catch (error) {
|
||||
console.error('删除对话失败:', error);
|
||||
alert('删除对话失败: ' + error.message);
|
||||
@@ -2256,7 +2325,9 @@ async function showAttackChain(conversationId) {
|
||||
}
|
||||
|
||||
modal.style.display = 'block';
|
||||
|
||||
// 打开时立即按当前语言刷新统计(避免红框内仍显示硬编码中文)
|
||||
updateAttackChainStats({ nodes: [], edges: [] });
|
||||
|
||||
// 清空容器
|
||||
const container = document.getElementById('attack-chain-container');
|
||||
if (container) {
|
||||
@@ -3331,16 +3402,35 @@ function getNodeTypeLabel(type) {
|
||||
return labels[type] || type;
|
||||
}
|
||||
|
||||
// 更新统计信息
|
||||
// 更新统计信息(使用 i18n,与 attackChainModal.nodesEdges 一致)
|
||||
function updateAttackChainStats(chainData) {
|
||||
const statsElement = document.getElementById('attack-chain-stats');
|
||||
if (statsElement) {
|
||||
const nodeCount = chainData.nodes ? chainData.nodes.length : 0;
|
||||
const edgeCount = chainData.edges ? chainData.edges.length : 0;
|
||||
statsElement.textContent = `节点: ${nodeCount} | 边: ${edgeCount}`;
|
||||
if (typeof window.t === 'function') {
|
||||
statsElement.textContent = window.t('attackChainModal.nodesEdges', {
|
||||
nodes: nodeCount,
|
||||
edges: edgeCount
|
||||
});
|
||||
} else {
|
||||
statsElement.textContent = `Nodes: ${nodeCount} | Edges: ${edgeCount}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 语言切换时刷新攻击链统计文案(动态 textContent 不会随 applyTranslations 更新)
|
||||
document.addEventListener('languagechange', function () {
|
||||
if (window.attackChainOriginalData && typeof updateAttackChainStats === 'function') {
|
||||
updateAttackChainStats(window.attackChainOriginalData);
|
||||
} else {
|
||||
const statsEl = document.getElementById('attack-chain-stats');
|
||||
if (statsEl && typeof window.t === 'function') {
|
||||
statsEl.textContent = window.t('attackChainModal.nodesEdges', { nodes: 0, edges: 0 });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭节点详情
|
||||
function closeNodeDetails() {
|
||||
const detailsPanel = document.getElementById('attack-chain-details');
|
||||
@@ -5203,12 +5293,70 @@ function closeBatchManageModal() {
|
||||
allConversationsForBatch = [];
|
||||
}
|
||||
|
||||
// 语言切换时刷新批量管理模态框标题(若当前正在显示)
|
||||
// 语言切换时刷新当前聊天页内的时间与动态文案(消息时间、执行流程时间由 monitor 的 refreshProgressAndTimelineI18n 处理)
|
||||
function refreshChatPanelI18n() {
|
||||
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
|
||||
const timeOpts = { hour: '2-digit', minute: '2-digit' };
|
||||
if (locale === 'zh-CN') timeOpts.hour12 = false;
|
||||
const t = typeof window.t === 'function' ? window.t : function (k) { return k; };
|
||||
|
||||
const messagesEl = document.getElementById('chat-messages');
|
||||
if (messagesEl) {
|
||||
messagesEl.querySelectorAll('.message-time[data-message-time]').forEach(function (el) {
|
||||
try {
|
||||
const d = new Date(el.dataset.messageTime);
|
||||
if (!isNaN(d.getTime())) {
|
||||
el.textContent = d.toLocaleTimeString(locale, timeOpts);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
});
|
||||
messagesEl.querySelectorAll('.mcp-call-label').forEach(function (el) {
|
||||
el.textContent = '\uD83D\uDCCB ' + t('chat.penetrationTestDetail');
|
||||
});
|
||||
messagesEl.querySelectorAll('.process-detail-btn').forEach(function (btn) {
|
||||
const span = btn.querySelector('span');
|
||||
if (!span) return;
|
||||
const assistantEl = btn.closest('.message.assistant');
|
||||
const messageId = assistantEl && assistantEl.id;
|
||||
const detailsId = messageId ? 'process-details-' + messageId : '';
|
||||
const timeline = detailsId ? document.getElementById(detailsId) && document.getElementById(detailsId).querySelector('.progress-timeline') : null;
|
||||
const expanded = timeline && timeline.classList.contains('expanded');
|
||||
span.textContent = expanded ? t('tasks.collapseDetail') : t('chat.expandDetail');
|
||||
});
|
||||
}
|
||||
|
||||
const mcpModal = document.getElementById('mcp-detail-modal');
|
||||
if (mcpModal && mcpModal.style.display === 'block') {
|
||||
const detailTimeEl = document.getElementById('detail-time');
|
||||
if (detailTimeEl && detailTimeEl.dataset.detailTimeIso) {
|
||||
try {
|
||||
const d = new Date(detailTimeEl.dataset.detailTimeIso);
|
||||
if (!isNaN(d.getTime())) {
|
||||
detailTimeEl.textContent = d.toLocaleString(locale);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
const statusEl = document.getElementById('detail-status');
|
||||
if (statusEl && statusEl.dataset.detailStatus !== undefined && typeof getStatusText === 'function') {
|
||||
statusEl.textContent = getStatusText(statusEl.dataset.detailStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 语言切换时刷新批量管理模态框标题(若当前正在显示);并刷新对话列表时间格式与系统就绪提示;刷新当前页消息时间与动态文案
|
||||
document.addEventListener('languagechange', function () {
|
||||
refreshSystemReadyMessageBubbles();
|
||||
refreshChatPanelI18n();
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
if (modal && modal.style.display === 'flex') {
|
||||
updateBatchManageTitle(allConversationsForBatch.length);
|
||||
}
|
||||
// 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式
|
||||
if (typeof loadConversationsWithGroups === 'function') {
|
||||
loadConversationsWithGroups();
|
||||
} else if (typeof loadConversations === 'function') {
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
|
||||
// 显示创建分组模态框
|
||||
@@ -6144,4 +6292,23 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 任意入口删除对话后同步:若删除的是当前对话则清空主区,并刷新侧边栏列表(如从 WebShell AI 助手删除)
|
||||
document.addEventListener('conversation-deleted', (e) => {
|
||||
const id = e.detail && e.detail.conversationId;
|
||||
if (!id) return;
|
||||
if (id === currentConversationId) {
|
||||
currentConversationId = null;
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
if (messagesDiv) messagesDiv.innerHTML = '';
|
||||
const readyMsg = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsg, null, null, null, { systemReadyMessage: true });
|
||||
addAttackChainButton(null);
|
||||
}
|
||||
if (typeof loadConversationsWithGroups === 'function') {
|
||||
loadConversationsWithGroups();
|
||||
} else if (typeof loadConversations === 'function') {
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,10 +38,25 @@ async function refreshDashboard() {
|
||||
apiFetch('/api/skills/stats').then(r => r.ok ? r.json() : null).catch(() => null)
|
||||
]);
|
||||
|
||||
// 运行中任务:Agent 循环任务 + 批量队列「执行中」数量统一统计,避免顶部 KPI 与运行概览不一致
|
||||
let agentRunningCount = null;
|
||||
if (tasksRes && Array.isArray(tasksRes.tasks)) {
|
||||
if (runningEl) runningEl.textContent = String(tasksRes.tasks.length);
|
||||
} else {
|
||||
if (runningEl) runningEl.textContent = '-';
|
||||
agentRunningCount = tasksRes.tasks.length;
|
||||
}
|
||||
let batchRunningCount = 0;
|
||||
if (batchRes && Array.isArray(batchRes.queues)) {
|
||||
batchRes.queues.forEach(q => {
|
||||
if ((q.status || '').toLowerCase() === 'running') batchRunningCount++;
|
||||
});
|
||||
}
|
||||
if (runningEl) {
|
||||
if (agentRunningCount !== null) {
|
||||
runningEl.textContent = String(agentRunningCount + batchRunningCount);
|
||||
} else if (batchRes && Array.isArray(batchRes.queues)) {
|
||||
runningEl.textContent = String(batchRunningCount);
|
||||
} else {
|
||||
runningEl.textContent = '-';
|
||||
}
|
||||
}
|
||||
|
||||
if (vulnRes && typeof vulnRes.total === 'number') {
|
||||
@@ -63,14 +78,14 @@ async function refreshDashboard() {
|
||||
});
|
||||
}
|
||||
|
||||
// 批量任务队列:按状态统计(优化版)
|
||||
// 批量任务队列:按状态统计(优化版;running 与上方 batchRunningCount 一致)
|
||||
if (batchRes && Array.isArray(batchRes.queues)) {
|
||||
const queues = batchRes.queues;
|
||||
let pending = 0, running = 0, done = 0;
|
||||
let pending = 0, running = batchRunningCount, done = 0;
|
||||
queues.forEach(q => {
|
||||
const s = (q.status || '').toLowerCase();
|
||||
if (s === 'pending' || s === 'paused') pending++;
|
||||
else if (s === 'running') running++;
|
||||
else if (s === 'running') { /* already counted into batchRunningCount */ }
|
||||
else if (s === 'completed' || s === 'cancelled') done++;
|
||||
});
|
||||
const total = pending + running + done;
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
const loadedLangs = {};
|
||||
|
||||
// 供 bootstrap 等逻辑等待:避免 chat 在 t() 未就绪时用中文硬编码渲染,导致与语言标签不一致
|
||||
let i18nReadyResolve;
|
||||
window.i18nReady = new Promise(function (resolve) {
|
||||
i18nReadyResolve = resolve;
|
||||
});
|
||||
|
||||
function detectInitialLang() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
@@ -159,6 +165,7 @@
|
||||
async function initI18n() {
|
||||
if (typeof i18next === 'undefined') {
|
||||
console.warn('i18next 未加载,跳过前端国际化初始化');
|
||||
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -201,12 +208,22 @@
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleGlobalClickForLangDropdown);
|
||||
|
||||
// 若 chat 已在 i18n 完成前用后备中文渲染了系统就绪消息,这里按当前语言纠正一次
|
||||
try {
|
||||
if (typeof refreshSystemReadyMessageBubbles === 'function') {
|
||||
refreshSystemReadyMessageBubbles();
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// i18n 初始化在 DOM Ready 后执行
|
||||
initI18n().catch(function (e) {
|
||||
console.error('初始化国际化失败:', e);
|
||||
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
+177
-16
@@ -3,22 +3,59 @@ let activeTaskInterval = null;
|
||||
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
|
||||
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
|
||||
|
||||
// 将后端下发的进度文案转为当前语言的翻译(已知中文 key 映射)
|
||||
// 当前界面语言对应的 BCP 47 标签(与时间格式化一致)
|
||||
function getCurrentTimeLocale() {
|
||||
if (typeof window.__locale === 'string' && window.__locale.length) {
|
||||
return window.__locale.startsWith('zh') ? 'zh-CN' : 'en-US';
|
||||
}
|
||||
if (typeof i18next !== 'undefined' && i18next.language) {
|
||||
return (i18next.language || '').startsWith('zh') ? 'zh-CN' : 'en-US';
|
||||
}
|
||||
return 'zh-CN';
|
||||
}
|
||||
|
||||
// toLocaleTimeString 选项:中文用 24 小时制,避免仍显示 AM/PM
|
||||
function getTimeFormatOptions() {
|
||||
const loc = getCurrentTimeLocale();
|
||||
const base = { hour: '2-digit', minute: '2-digit', second: '2-digit' };
|
||||
if (loc === 'zh-CN') {
|
||||
base.hour12 = false;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
// 将后端下发的进度文案转为当前语言的翻译(中英双向映射,切换语言后能跟上)
|
||||
function translateProgressMessage(message) {
|
||||
if (!message || typeof message !== 'string') return message;
|
||||
if (typeof window.t !== 'function') return message;
|
||||
const trim = message.trim();
|
||||
const map = {
|
||||
// 中文
|
||||
'正在调用AI模型...': 'progress.callingAI',
|
||||
'最后一次迭代:正在生成总结和下一步计划...': 'progress.lastIterSummary',
|
||||
'总结生成完成': 'progress.summaryDone',
|
||||
'正在生成最终回复...': 'progress.generatingFinalReply',
|
||||
'达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary'
|
||||
'达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary',
|
||||
'正在分析您的请求...': 'progress.analyzingRequestShort',
|
||||
'开始分析请求并制定测试策略': 'progress.analyzingRequestPlanning',
|
||||
// 英文(与 en-US.json 一致,避免后端/缓存已是英文时无法随语言切换)
|
||||
'Calling AI model...': 'progress.callingAI',
|
||||
'Last iteration: generating summary and next steps...': 'progress.lastIterSummary',
|
||||
'Summary complete': 'progress.summaryDone',
|
||||
'Generating final reply...': 'progress.generatingFinalReply',
|
||||
'Max iterations reached, generating summary...': 'progress.maxIterSummary',
|
||||
'Analyzing your request...': 'progress.analyzingRequestShort',
|
||||
'Analyzing your request and planning test strategy...': 'progress.analyzingRequestPlanning'
|
||||
};
|
||||
if (map[trim]) return window.t(map[trim]);
|
||||
const callingToolPrefix = '正在调用工具: ';
|
||||
if (trim.indexOf(callingToolPrefix) === 0) {
|
||||
const name = trim.slice(callingToolPrefix.length);
|
||||
const callingToolPrefixCn = '正在调用工具: ';
|
||||
const callingToolPrefixEn = 'Calling tool: ';
|
||||
if (trim.indexOf(callingToolPrefixCn) === 0) {
|
||||
const name = trim.slice(callingToolPrefixCn.length);
|
||||
return window.t('progress.callingTool', { name: name });
|
||||
}
|
||||
if (trim.indexOf(callingToolPrefixEn) === 0) {
|
||||
const name = trim.slice(callingToolPrefixEn.length);
|
||||
return window.t('progress.callingTool', { name: name });
|
||||
}
|
||||
return message;
|
||||
@@ -497,11 +534,12 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
}
|
||||
break;
|
||||
case 'iteration':
|
||||
// 添加迭代标记
|
||||
// 添加迭代标记(data 属性供语言切换时重算标题)
|
||||
addTimelineItem(timeline, 'iteration', {
|
||||
title: typeof window.t === 'function' ? window.t('chat.iterationRound', { n: event.data?.iteration || 1 }) : '第 ' + (event.data?.iteration || 1) + ' 轮迭代',
|
||||
message: event.message,
|
||||
data: event.data
|
||||
data: event.data,
|
||||
iterationN: event.data?.iteration || 1
|
||||
});
|
||||
break;
|
||||
|
||||
@@ -569,6 +607,11 @@ function handleStreamEvent(event, progressElement, progressId,
|
||||
case 'progress':
|
||||
const progressTitle = document.querySelector(`#${progressId} .progress-title`);
|
||||
if (progressTitle) {
|
||||
// 保存原文,语言切换时可用 translateProgressMessage 重新套当前语言
|
||||
const progressEl = document.getElementById(progressId);
|
||||
if (progressEl) {
|
||||
progressEl.dataset.progressRawMessage = event.message || '';
|
||||
}
|
||||
const progressMsg = translateProgressMessage(event.message);
|
||||
progressTitle.textContent = '🔍 ' + progressMsg;
|
||||
}
|
||||
@@ -855,7 +898,30 @@ function addTimelineItem(timeline, type, options) {
|
||||
const itemId = 'timeline-item-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
item.id = itemId;
|
||||
item.className = `timeline-item timeline-item-${type}`;
|
||||
|
||||
// 记录类型与参数,便于 languagechange 时刷新标题文案
|
||||
item.dataset.timelineType = type;
|
||||
if (type === 'iteration') {
|
||||
const n = options.iterationN != null ? options.iterationN : (options.data && options.data.iteration != null ? options.data.iteration : 1);
|
||||
item.dataset.iterationN = String(n);
|
||||
}
|
||||
if (type === 'progress' && options.message) {
|
||||
item.dataset.progressMessage = options.message;
|
||||
}
|
||||
if (type === 'tool_calls_detected' && options.data && options.data.count != null) {
|
||||
item.dataset.toolCallsCount = String(options.data.count);
|
||||
}
|
||||
if (type === 'tool_call' && options.data) {
|
||||
const d = options.data;
|
||||
item.dataset.toolName = (d.toolName != null && d.toolName !== '') ? String(d.toolName) : '';
|
||||
item.dataset.toolIndex = (d.index != null) ? String(d.index) : '0';
|
||||
item.dataset.toolTotal = (d.total != null) ? String(d.total) : '0';
|
||||
}
|
||||
if (type === 'tool_result' && options.data) {
|
||||
const d = options.data;
|
||||
item.dataset.toolName = (d.toolName != null && d.toolName !== '') ? String(d.toolName) : '';
|
||||
item.dataset.toolSuccess = d.success !== false ? '1' : '0';
|
||||
}
|
||||
|
||||
// 使用传入的createdAt时间,如果没有则使用当前时间(向后兼容)
|
||||
let eventTime;
|
||||
if (options.createdAt) {
|
||||
@@ -874,9 +940,14 @@ function addTimelineItem(timeline, type, options) {
|
||||
} else {
|
||||
eventTime = new Date();
|
||||
}
|
||||
|
||||
const timeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
|
||||
const time = eventTime.toLocaleTimeString(timeLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
||||
// 保存事件时间 ISO,语言切换时可重算时间格式
|
||||
try {
|
||||
item.dataset.createdAtIso = eventTime.toISOString();
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
const timeLocale = getCurrentTimeLocale();
|
||||
const timeOpts = getTimeFormatOptions();
|
||||
const time = eventTime.toLocaleTimeString(timeLocale, timeOpts);
|
||||
|
||||
let content = `
|
||||
<div class="timeline-item-header">
|
||||
@@ -896,7 +967,7 @@ function addTimelineItem(timeline, type, options) {
|
||||
<div class="timeline-item-content">
|
||||
<div class="tool-details">
|
||||
<div class="tool-arg-section">
|
||||
<strong>${escapeHtml(paramsLabel)}</strong>
|
||||
<strong data-i18n="timeline.params">${escapeHtml(paramsLabel)}</strong>
|
||||
<pre class="tool-args">${escapeHtml(JSON.stringify(args, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -913,9 +984,9 @@ function addTimelineItem(timeline, type, options) {
|
||||
content += `
|
||||
<div class="timeline-item-content">
|
||||
<div class="tool-result-section ${isError ? 'error' : 'success'}">
|
||||
<strong>${escapeHtml(execResultLabel)}</strong>
|
||||
<strong data-i18n="timeline.executionResult">${escapeHtml(execResultLabel)}</strong>
|
||||
<pre class="tool-result">${escapeHtml(resultStr)}</pre>
|
||||
${data.executionId ? `<div class="tool-execution-id">${escapeHtml(execIdLabel)} <code>${escapeHtml(data.executionId)}</code></div>` : ''}
|
||||
${data.executionId ? `<div class="tool-execution-id"><span data-i18n="timeline.executionId">${escapeHtml(execIdLabel)}</span> <code>${escapeHtml(data.executionId)}</code></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -987,9 +1058,10 @@ function renderActiveTasks(tasks) {
|
||||
item.className = 'active-task-item';
|
||||
|
||||
const startedTime = task.startedAt ? new Date(task.startedAt) : null;
|
||||
const taskTimeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
|
||||
const taskTimeLocale = getCurrentTimeLocale();
|
||||
const timeOpts = getTimeFormatOptions();
|
||||
const timeText = startedTime && !isNaN(startedTime.getTime())
|
||||
? startedTime.toLocaleTimeString(taskTimeLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit' })
|
||||
? startedTime.toLocaleTimeString(taskTimeLocale, timeOpts)
|
||||
: '';
|
||||
|
||||
const _t = function (k) { return typeof window.t === 'function' ? window.t(k) : k; };
|
||||
@@ -1686,6 +1758,95 @@ function formatExecutionDuration(start, end) {
|
||||
return typeof window.t === 'function' ? window.t('mcpMonitor.durationHoursOnly', { hours: hours }) : hours + ' 小时';
|
||||
}
|
||||
|
||||
/**
|
||||
* 语言切换后刷新对话页已渲染的进度条、时间线标题与时间格式(避免仍显示英文或 AM/PM)
|
||||
*/
|
||||
function refreshProgressAndTimelineI18n() {
|
||||
const _t = function (k, o) {
|
||||
return typeof window.t === 'function' ? window.t(k, o) : k;
|
||||
};
|
||||
const timeLocale = getCurrentTimeLocale();
|
||||
const timeOpts = getTimeFormatOptions();
|
||||
|
||||
// 进度块内停止按钮:未禁用时统一为当前语言的「停止任务」(避免仍显示 Stop task)
|
||||
document.querySelectorAll('.progress-message .progress-stop').forEach(function (btn) {
|
||||
if (!btn.disabled && btn.id && btn.id.indexOf('-stop-btn') !== -1) {
|
||||
const cancelling = _t('tasks.cancelling');
|
||||
if (btn.textContent !== cancelling) {
|
||||
btn.textContent = _t('tasks.stopTask');
|
||||
}
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('.progress-toggle').forEach(function (btn) {
|
||||
const timeline = btn.closest('.progress-container, .message-bubble') &&
|
||||
btn.closest('.progress-container, .message-bubble').querySelector('.progress-timeline');
|
||||
const expanded = timeline && timeline.classList.contains('expanded');
|
||||
btn.textContent = expanded ? _t('tasks.collapseDetail') : _t('chat.expandDetail');
|
||||
});
|
||||
document.querySelectorAll('.progress-message').forEach(function (msgEl) {
|
||||
const raw = msgEl.dataset.progressRawMessage;
|
||||
const titleEl = msgEl.querySelector('.progress-title');
|
||||
if (titleEl && raw) {
|
||||
titleEl.textContent = '\uD83D\uDD0D ' + translateProgressMessage(raw);
|
||||
}
|
||||
});
|
||||
// 转换后的详情区顶栏「渗透测试详情」:仅刷新不在 .progress-message 内的 progress 标题
|
||||
document.querySelectorAll('.progress-container .progress-header .progress-title').forEach(function (titleEl) {
|
||||
if (titleEl.closest('.progress-message')) return;
|
||||
titleEl.textContent = '\uD83D\uDCCB ' + _t('chat.penetrationTestDetail');
|
||||
});
|
||||
|
||||
// 时间线项:按类型重算标题,并重绘时间戳
|
||||
document.querySelectorAll('.timeline-item').forEach(function (item) {
|
||||
const type = item.dataset.timelineType;
|
||||
const titleSpan = item.querySelector('.timeline-item-title');
|
||||
const timeSpan = item.querySelector('.timeline-item-time');
|
||||
if (!titleSpan) return;
|
||||
if (type === 'iteration' && item.dataset.iterationN) {
|
||||
const n = parseInt(item.dataset.iterationN, 10) || 1;
|
||||
titleSpan.textContent = _t('chat.iterationRound', { n: n });
|
||||
} else if (type === 'thinking') {
|
||||
titleSpan.textContent = '\uD83E\uDD14 ' + _t('chat.aiThinking');
|
||||
} else if (type === 'tool_calls_detected' && item.dataset.toolCallsCount != null) {
|
||||
const count = parseInt(item.dataset.toolCallsCount, 10) || 0;
|
||||
titleSpan.textContent = '\uD83D\uDD27 ' + _t('chat.toolCallsDetected', { count: count });
|
||||
} else if (type === 'tool_call' && (item.dataset.toolName !== undefined || item.dataset.toolIndex !== undefined)) {
|
||||
const name = (item.dataset.toolName != null && item.dataset.toolName !== '') ? item.dataset.toolName : _t('chat.unknownTool');
|
||||
const index = parseInt(item.dataset.toolIndex, 10) || 0;
|
||||
const total = parseInt(item.dataset.toolTotal, 10) || 0;
|
||||
titleSpan.textContent = '\uD83D\uDD27 ' + _t('chat.callTool', { name: name, index: index, total: total });
|
||||
} else if (type === 'tool_result' && (item.dataset.toolName !== undefined || item.dataset.toolSuccess !== undefined)) {
|
||||
const name = (item.dataset.toolName != null && item.dataset.toolName !== '') ? item.dataset.toolName : _t('chat.unknownTool');
|
||||
const success = item.dataset.toolSuccess === '1';
|
||||
const icon = success ? '\u2705 ' : '\u274C ';
|
||||
titleSpan.textContent = icon + (success ? _t('chat.toolExecComplete', { name: name }) : _t('chat.toolExecFailed', { name: name }));
|
||||
} else if (type === 'cancelled') {
|
||||
titleSpan.textContent = '\u26D4 ' + _t('chat.taskCancelled');
|
||||
} else if (type === 'progress' && item.dataset.progressMessage !== undefined) {
|
||||
titleSpan.textContent = typeof window.translateProgressMessage === 'function' ? window.translateProgressMessage(item.dataset.progressMessage) : item.dataset.progressMessage;
|
||||
}
|
||||
if (timeSpan && item.dataset.createdAtIso) {
|
||||
const d = new Date(item.dataset.createdAtIso);
|
||||
if (!isNaN(d.getTime())) {
|
||||
timeSpan.textContent = d.toLocaleTimeString(timeLocale, timeOpts);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 详情区「展开/收起」按钮
|
||||
document.querySelectorAll('.process-detail-btn span').forEach(function (span) {
|
||||
const btn = span.closest('.process-detail-btn');
|
||||
const assistantId = btn && btn.closest('.message.assistant') && btn.closest('.message.assistant').id;
|
||||
if (!assistantId) return;
|
||||
const detailsId = 'process-details-' + assistantId;
|
||||
const timeline = document.getElementById(detailsId) && document.getElementById(detailsId).querySelector('.progress-timeline');
|
||||
const expanded = timeline && timeline.classList.contains('expanded');
|
||||
span.textContent = expanded ? _t('tasks.collapseDetail') : _t('chat.expandDetail');
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
updateBatchActionsState();
|
||||
loadActiveTasks();
|
||||
refreshProgressAndTimelineI18n();
|
||||
});
|
||||
|
||||
@@ -57,7 +57,11 @@ async function loadRoles() {
|
||||
return roles;
|
||||
} catch (error) {
|
||||
console.error('加载角色失败:', error);
|
||||
showNotification(_t('roles.loadFailed') + ': ' + error.message, 'error');
|
||||
// 提示文案使用 i18n;若此时 i18n 尚未初始化,则回退为可读中文,而不是暴露 key(roles.loadFailed)
|
||||
var loadFailedLabel = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('roles.loadFailed')
|
||||
: '加载角色失败';
|
||||
showNotification(loadFailedLabel + ': ' + error.message, 'error');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
+35
-3
@@ -8,7 +8,7 @@ function initRouter() {
|
||||
if (hash) {
|
||||
const hashParts = hash.split('?');
|
||||
const pageId = hashParts[0];
|
||||
if (pageId && ['dashboard', 'chat', 'info-collect', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
|
||||
if (pageId && ['dashboard', 'chat', 'info-collect', 'vulnerabilities', 'webshell', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings', 'tasks'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
|
||||
// 如果是chat页面且带有conversation参数,加载对应对话
|
||||
@@ -243,7 +243,8 @@ function initPage(pageId) {
|
||||
}
|
||||
break;
|
||||
case 'chat':
|
||||
// 对话页面已由chat.js初始化
|
||||
// 恢复对话列表折叠状态(从其他页返回时保持用户选择)
|
||||
initConversationSidebarState();
|
||||
break;
|
||||
case 'info-collect':
|
||||
// 信息收集页面
|
||||
@@ -292,6 +293,12 @@ function initPage(pageId) {
|
||||
initVulnerabilityPage();
|
||||
}
|
||||
break;
|
||||
case 'webshell':
|
||||
// 初始化 WebShell 管理页面
|
||||
if (typeof initWebshellPage === 'function') {
|
||||
initWebshellPage();
|
||||
}
|
||||
break;
|
||||
case 'settings':
|
||||
// 初始化设置页面(不需要加载工具列表)
|
||||
if (typeof loadConfig === 'function') {
|
||||
@@ -361,7 +368,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const hashParts = hash.split('?');
|
||||
const pageId = hashParts[0];
|
||||
|
||||
if (pageId && ['chat', 'info-collect', 'tasks', 'vulnerabilities', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
|
||||
if (pageId && ['chat', 'info-collect', 'tasks', 'vulnerabilities', 'webshell', 'mcp-monitor', 'mcp-management', 'knowledge-management', 'knowledge-retrieval-logs', 'roles-management', 'skills-monitor', 'skills-management', 'settings'].includes(pageId)) {
|
||||
switchPage(pageId);
|
||||
|
||||
// 如果是chat页面且带有conversation参数,加载对应对话
|
||||
@@ -421,11 +428,36 @@ function initSidebarState() {
|
||||
sidebar.classList.add('collapsed');
|
||||
}
|
||||
}
|
||||
initConversationSidebarState();
|
||||
}
|
||||
|
||||
// 切换对话页左侧列表折叠/展开
|
||||
function toggleConversationSidebar() {
|
||||
const sidebar = document.getElementById('conversation-sidebar');
|
||||
if (sidebar) {
|
||||
sidebar.classList.toggle('collapsed');
|
||||
const isCollapsed = sidebar.classList.contains('collapsed');
|
||||
localStorage.setItem('conversationSidebarCollapsed', isCollapsed ? 'true' : 'false');
|
||||
}
|
||||
}
|
||||
|
||||
// 恢复对话列表折叠状态(进入对话页时生效)
|
||||
function initConversationSidebarState() {
|
||||
const sidebar = document.getElementById('conversation-sidebar');
|
||||
if (sidebar) {
|
||||
const savedState = localStorage.getItem('conversationSidebarCollapsed');
|
||||
if (savedState === 'true') {
|
||||
sidebar.classList.add('collapsed');
|
||||
} else {
|
||||
sidebar.classList.remove('collapsed');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 导出函数供其他脚本使用
|
||||
window.switchPage = switchPage;
|
||||
window.toggleSubmenu = toggleSubmenu;
|
||||
window.toggleSidebar = toggleSidebar;
|
||||
window.toggleConversationSidebar = toggleConversationSidebar;
|
||||
window.currentPage = function() { return currentPage; };
|
||||
|
||||
|
||||
@@ -1166,7 +1166,8 @@ async function loadExternalMCPs() {
|
||||
console.error('加载外部MCP列表失败:', error);
|
||||
const list = document.getElementById('external-mcp-list');
|
||||
if (list) {
|
||||
list.innerHTML = `<div class="error">加载失败: ${escapeHtml(error.message)}</div>`;
|
||||
const errT = typeof window.t === 'function' ? window.t : (k) => k;
|
||||
list.innerHTML = `<div class="error">${escapeHtml(errT('mcp.loadExternalMCPFailed'))}: ${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1224,7 +1225,7 @@ function renderExternalMCPList(servers) {
|
||||
<div class="external-mcp-item">
|
||||
<div class="external-mcp-item-header">
|
||||
<div class="external-mcp-item-info">
|
||||
<h4>${transportIcon} ${escapeHtml(name)}${server.tool_count !== undefined && server.tool_count > 0 ? `<span class="tool-count-badge" title="工具数量">🔧 ${server.tool_count}</span>` : ''}</h4>
|
||||
<h4>${transportIcon} ${escapeHtml(name)}${server.tool_count !== undefined && server.tool_count > 0 ? `<span class="tool-count-badge" title="${escapeHtml(statusT('mcp.toolCount'))}">🔧 ${server.tool_count}</span>` : ''}</h4>
|
||||
<span class="external-mcp-status ${statusClass}">${statusText}</span>
|
||||
</div>
|
||||
<div class="external-mcp-item-actions">
|
||||
@@ -1234,7 +1235,7 @@ function renderExternalMCPList(servers) {
|
||||
</button>` :
|
||||
status === 'connecting' ?
|
||||
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" disabled style="opacity: 0.6; cursor: not-allowed;">
|
||||
⏳ 连接中...
|
||||
⏳ ${statusT('mcp.connecting')}
|
||||
</button>` : ''}
|
||||
<button class="btn-small" onclick="editExternalMCP('${escapeHtml(name)}')" title="${statusT('mcp.editConfig')}" ${status === 'connecting' ? 'disabled' : ''}>✏️ ${statusT('common.edit')}</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteExternalMCP('${escapeHtml(name)}')" title="${statusT('mcp.deleteConfig')}" ${status === 'connecting' ? 'disabled' : ''}>🗑 ${statusT('common.delete')}</button>
|
||||
@@ -1242,7 +1243,7 @@ function renderExternalMCPList(servers) {
|
||||
</div>
|
||||
${status === 'error' && server.error ? `
|
||||
<div class="external-mcp-error" style="margin: 12px 0; padding: 12px; background: #fee; border-left: 3px solid #f44; border-radius: 4px; color: #c33; font-size: 0.875rem;">
|
||||
<strong>❌ 连接错误:</strong>${escapeHtml(server.error)}
|
||||
<strong>❌ ${statusT('mcp.connectionErrorLabel')}</strong>${escapeHtml(server.error)}
|
||||
</div>` : ''}
|
||||
<div class="external-mcp-item-details">
|
||||
<div>
|
||||
@@ -1252,7 +1253,7 @@ function renderExternalMCPList(servers) {
|
||||
${server.tool_count !== undefined && server.tool_count > 0 ? `
|
||||
<div>
|
||||
<strong>${statusT('mcp.toolCount')}</strong>
|
||||
<span style="font-weight: 600; color: var(--accent-color);">🔧 ${server.tool_count} 个工具</span>
|
||||
<span style="font-weight: 600; color: var(--accent-color);">${statusT('mcp.toolsCountValue', { count: server.tool_count })}</span>
|
||||
</div>` : server.tool_count === 0 && status === 'connected' ? `
|
||||
<div>
|
||||
<strong>${statusT('mcp.toolCount')}</strong>
|
||||
@@ -1266,7 +1267,7 @@ function renderExternalMCPList(servers) {
|
||||
${server.config.timeout ? `
|
||||
<div>
|
||||
<strong>${statusT('mcp.timeout')}</strong>
|
||||
<span>${server.config.timeout} 秒</span>
|
||||
<span>${server.config.timeout} ${statusT('mcp.secondsUnit')}</span>
|
||||
</div>` : ''}
|
||||
${transport === 'stdio' && server.config.command ? `
|
||||
<div>
|
||||
@@ -1275,7 +1276,7 @@ function renderExternalMCPList(servers) {
|
||||
</div>` : ''}
|
||||
${transport === 'http' && server.config.url ? `
|
||||
<div>
|
||||
<strong>URL</strong>
|
||||
<strong>${statusT('mcp.urlLabel')}</strong>
|
||||
<span style="font-family: monospace; font-size: 0.8125rem; word-break: break-all;">${escapeHtml(server.config.url)}</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
@@ -1327,7 +1328,7 @@ async function editExternalMCP(name) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取外部MCP配置失败');
|
||||
throw new Error(typeof window.t === 'function' ? window.t('mcp.getConfigFailed') : '获取外部MCP配置失败');
|
||||
}
|
||||
|
||||
const server = await response.json();
|
||||
@@ -1742,3 +1743,20 @@ openSettings = async function() {
|
||||
await originalOpenSettings();
|
||||
await loadExternalMCPs();
|
||||
};
|
||||
|
||||
// 语言切换后重新渲染 MCP 管理页中由 JS 写入的区块(innerHTML 不会随 data-i18n 自动更新)
|
||||
document.addEventListener('languagechange', function () {
|
||||
try {
|
||||
const mcpPage = document.getElementById('page-mcp-management');
|
||||
if (mcpPage && mcpPage.classList.contains('active')) {
|
||||
if (typeof loadExternalMCPs === 'function') {
|
||||
loadExternalMCPs().catch(function () { /* ignore */ });
|
||||
}
|
||||
if (typeof updateToolsStats === 'function') {
|
||||
updateToolsStats().catch(function () { /* ignore */ });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('languagechange MCP refresh failed', e);
|
||||
}
|
||||
});
|
||||
|
||||
+76
-11
@@ -26,7 +26,33 @@
|
||||
return terminals[0] || null;
|
||||
}
|
||||
|
||||
var WELCOME_LINE = 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏\r\n';
|
||||
function tr(key, opts) {
|
||||
if (typeof window !== 'undefined' && typeof window.t === 'function') {
|
||||
return window.t(key, opts);
|
||||
}
|
||||
// i18n 未就绪时的后备(与 zh-CN 一致)
|
||||
var fallbacks = {
|
||||
'settingsTerminal.welcomeLine': 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏',
|
||||
'settingsTerminal.sessionClosed': '[会话已关闭]',
|
||||
'settingsTerminal.connectionError': '[终端连接出错]',
|
||||
'settingsTerminal.connectFailed': '[无法连接终端服务: {{msg}}]',
|
||||
'settingsTerminal.closeTabTitle': '关闭',
|
||||
'settingsTerminal.containerClickTitle': '点击此处后输入命令',
|
||||
'settingsTerminal.xtermNotLoaded': '未加载 xterm.js,请刷新页面或检查网络。',
|
||||
'settingsTerminal.terminalTab': '终端 {{n}}'
|
||||
};
|
||||
var s = fallbacks[key] || key;
|
||||
if (opts && typeof opts === 'object') {
|
||||
Object.keys(opts).forEach(function (k) {
|
||||
s = s.split('{{' + k + '}}').join(String(opts[k]));
|
||||
});
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function getWelcomeLine() {
|
||||
return tr('settingsTerminal.welcomeLine') + '\r\n';
|
||||
}
|
||||
|
||||
function writePrompt(tab) {
|
||||
// 提示符交由后端 Shell 自行输出,这里仅保留占位函数,避免旧代码报错
|
||||
@@ -35,7 +61,7 @@
|
||||
function redrawTabDisplay(t) {
|
||||
if (!t || !t.term) return;
|
||||
t.term.clear();
|
||||
t.term.write(WELCOME_LINE);
|
||||
t.term.write(getWelcomeLine());
|
||||
}
|
||||
|
||||
function writeln(tabOrS, s) {
|
||||
@@ -121,19 +147,19 @@
|
||||
ws.onclose = function () {
|
||||
tab.running = false;
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[2m[会话已关闭]\x1b[0m');
|
||||
tab.term.writeln('\r\n\x1b[2m' + tr('settingsTerminal.sessionClosed') + '\x1b[0m');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function () {
|
||||
tab.running = false;
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[31m[终端连接出错]\x1b[0m');
|
||||
tab.term.writeln('\r\n\x1b[31m' + tr('settingsTerminal.connectionError') + '\x1b[0m');
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[31m[无法连接终端服务: ' + String(e) + ']\x1b[0m');
|
||||
tab.term.writeln('\r\n\x1b[31m' + tr('settingsTerminal.connectFailed', { msg: String(e) }) + '\x1b[0m');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,13 +208,13 @@
|
||||
term.loadAddon(fitAddon);
|
||||
}
|
||||
term.open(container);
|
||||
term.write(WELCOME_LINE);
|
||||
term.write(getWelcomeLine());
|
||||
container.addEventListener('click', function () {
|
||||
switchTerminalTab(tab.id);
|
||||
if (term) term.focus();
|
||||
});
|
||||
container.setAttribute('tabindex', '0');
|
||||
container.title = '点击此处后输入命令';
|
||||
container.title = tr('settingsTerminal.containerClickTitle');
|
||||
|
||||
function sendToWS(data) {
|
||||
ensureTerminalWS(tab);
|
||||
@@ -211,6 +237,9 @@
|
||||
|
||||
tab.term = term;
|
||||
tab.fitAddon = fitAddon;
|
||||
// 立即建立 WebSocket,让后端 PTY/Shell 马上启动并输出提示符;
|
||||
// 若等到首次按键才 connect,用户会感觉必须先按回车才能输入(实为连接尚未建立)。
|
||||
ensureTerminalWS(tab);
|
||||
return term;
|
||||
}
|
||||
|
||||
@@ -253,12 +282,12 @@
|
||||
tabDiv.setAttribute('data-tab-id', String(id));
|
||||
var label = document.createElement('span');
|
||||
label.className = 'terminal-tab-label';
|
||||
label.textContent = '终端 ' + id;
|
||||
label.textContent = tr('settingsTerminal.terminalTab', { n: id });
|
||||
label.onclick = function () { switchTerminalTab(id); };
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.type = 'button';
|
||||
closeBtn.className = 'terminal-tab-close';
|
||||
closeBtn.title = '关闭';
|
||||
closeBtn.title = tr('settingsTerminal.closeTabTitle');
|
||||
closeBtn.textContent = '×';
|
||||
closeBtn.onclick = function (e) { e.stopPropagation(); removeTerminalTab(id); };
|
||||
tabDiv.appendChild(label);
|
||||
@@ -340,7 +369,7 @@
|
||||
var t = terminals[i];
|
||||
tabDivs[i].setAttribute('data-tab-id', String(t.id));
|
||||
var lbl = tabDivs[i].querySelector('.terminal-tab-label');
|
||||
if (lbl) lbl.textContent = '终端 ' + t.id;
|
||||
if (lbl) lbl.textContent = tr('settingsTerminal.terminalTab', { n: t.id });
|
||||
if (lbl) lbl.onclick = (function (tid) { return function () { switchTerminalTab(tid); }; })(t.id);
|
||||
var cb = tabDivs[i].querySelector('.terminal-tab-close');
|
||||
if (cb) cb.onclick = (function (tid) { return function (e) { e.stopPropagation(); removeTerminalTab(tid); }; })(t.id);
|
||||
@@ -364,6 +393,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function refreshTerminalI18n() {
|
||||
// 语言切换后更新标签与容器 title;已打开的终端内容不强制清屏,以免丢失会话输出
|
||||
try {
|
||||
var tabsEl = document.querySelector('.terminal-tabs');
|
||||
if (tabsEl) {
|
||||
var tabDivs = tabsEl.querySelectorAll('.terminal-tab');
|
||||
for (var i = 0; i < tabDivs.length && i < terminals.length; i++) {
|
||||
var tid = terminals[i].id;
|
||||
var lbl = tabDivs[i].querySelector('.terminal-tab-label');
|
||||
if (lbl) lbl.textContent = tr('settingsTerminal.terminalTab', { n: tid });
|
||||
var cb = tabDivs[i].querySelector('.terminal-tab-close');
|
||||
if (cb) cb.title = tr('settingsTerminal.closeTabTitle');
|
||||
}
|
||||
}
|
||||
terminals.forEach(function (tab) {
|
||||
if (!tab || !tab.term) return;
|
||||
var cont = document.getElementById(tab.containerId);
|
||||
if (cont) cont.title = tr('settingsTerminal.containerClickTitle');
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
refreshTerminalI18n();
|
||||
});
|
||||
|
||||
function initTerminal() {
|
||||
var pane1 = document.getElementById('terminal-pane-1');
|
||||
var container1 = document.getElementById('terminal-container-1');
|
||||
@@ -377,7 +440,7 @@
|
||||
inited = true;
|
||||
|
||||
if (typeof Terminal === 'undefined') {
|
||||
container1.innerHTML = '<p class="terminal-error">未加载 xterm.js,请刷新页面或检查网络。</p>';
|
||||
container1.innerHTML = '<p class="terminal-error">' + escapeHtml(tr('settingsTerminal.xtermNotLoaded')) + '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -388,6 +451,8 @@
|
||||
|
||||
updateTerminalTabCloseVisibility();
|
||||
|
||||
refreshTerminalI18n();
|
||||
|
||||
setTimeout(function () {
|
||||
try { if (tab.fitAddon) tab.fitAddon.fit(); if (tab.term) tab.term.focus(); } catch (e) {}
|
||||
}, 100);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,6 +135,15 @@
|
||||
<span data-i18n="nav.vulnerabilities">漏洞管理</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" data-page="webshell">
|
||||
<div class="nav-item-content" data-title="WebShell管理" onclick="switchPage('webshell')" data-i18n="nav.webshell" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4 17 10 11 4 5"></polyline>
|
||||
<line x1="12" y1="19" x2="20" y2="19"></line>
|
||||
</svg>
|
||||
<span data-i18n="nav.webshell">WebShell管理</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')" data-i18n="nav.mcp" data-i18n-attr="data-title" data-i18n-skip-text="true">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -400,12 +409,18 @@
|
||||
<!-- 对话页面 -->
|
||||
<div id="page-chat" class="page">
|
||||
<div class="chat-page-layout">
|
||||
<!-- 历史对话侧边栏 -->
|
||||
<aside class="conversation-sidebar">
|
||||
<div class="sidebar-header">
|
||||
<button class="new-chat-btn" onclick="startNewConversation()">
|
||||
<!-- 历史对话侧边栏(可折叠,与主导航侧边栏类似) -->
|
||||
<aside class="conversation-sidebar" id="conversation-sidebar">
|
||||
<!-- 头部一行:折叠与「新对话」并排,避免绝对定位重叠(flex 为最佳实践) -->
|
||||
<div class="sidebar-header conversation-sidebar-header">
|
||||
<button type="button" class="new-chat-btn" onclick="startNewConversation()">
|
||||
<span>+</span> <span data-i18n="chat.newChat">新对话</span>
|
||||
</button>
|
||||
<button type="button" class="conversation-sidebar-collapse-btn" onclick="toggleConversationSidebar()" data-i18n="chat.toggleConversationPanel" data-i18n-attr="title" data-i18n-skip-text="true" title="折叠/展开对话列表" aria-label="折叠/展开对话列表">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<!-- 全局搜索 -->
|
||||
@@ -945,6 +960,32 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebShell 管理页面 -->
|
||||
<div id="page-webshell" class="page">
|
||||
<div class="page-header">
|
||||
<h2 data-i18n="webshell.title">WebShell 管理</h2>
|
||||
<div class="page-header-actions">
|
||||
<button class="btn-primary" onclick="showAddWebshellModal()" data-i18n="webshell.addConnection">添加连接</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="page-content webshell-page-content">
|
||||
<div class="webshell-layout">
|
||||
<div id="webshell-sidebar" class="webshell-sidebar">
|
||||
<div class="webshell-sidebar-header" data-i18n="webshell.connections">连接列表</div>
|
||||
<div id="webshell-list" class="webshell-list">
|
||||
<div class="webshell-empty" data-i18n="webshell.noConnections">暂无连接,请点击「添加连接」</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="webshell-resize-handle" class="webshell-resize-handle" title="拖拽调整宽度"></div>
|
||||
<div class="webshell-main">
|
||||
<div id="webshell-workspace" class="webshell-workspace">
|
||||
<div class="webshell-workspace-placeholder" data-i18n="webshell.selectOrAdd">请从左侧选择连接,或添加新的 WebShell 连接</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 任务管理页面 -->
|
||||
<div id="page-tasks" class="page">
|
||||
<div class="page-header">
|
||||
@@ -1120,7 +1161,7 @@
|
||||
<input type="password" id="openai-api-key" data-i18n="settingsBasic.openaiApiKeyPlaceholder" data-i18n-attr="placeholder" placeholder="输入OpenAI API Key" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="openai-model" data-i18n="settingsBasic.model">模型 <span style="color: red;">*</span></label>
|
||||
<label for="openai-model"><span data-i18n="settingsBasic.model">模型</span> <span style="color: red;">*</span></label>
|
||||
<input type="text" id="openai-model" data-i18n="settingsBasic.modelPlaceholder" data-i18n-attr="placeholder" placeholder="gpt-4" required />
|
||||
</div>
|
||||
</div>
|
||||
@@ -1581,7 +1622,7 @@
|
||||
<div class="attack-chain-visualization-area">
|
||||
<div class="attack-chain-toolbar">
|
||||
<div class="attack-chain-info">
|
||||
<span id="attack-chain-stats">节点: 0 | 边: 0</span>
|
||||
<span id="attack-chain-stats">Nodes: 0 | Edges: 0</span>
|
||||
</div>
|
||||
<div class="attack-chain-filters">
|
||||
<input type="text" id="attack-chain-search" data-i18n="attackChainModal.searchPlaceholder" data-i18n-attr="placeholder" placeholder="搜索节点..."
|
||||
@@ -2090,6 +2131,57 @@ version: 1.0.0<br>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- WebShell 添加连接模态框 -->
|
||||
<div id="webshell-modal" class="modal">
|
||||
<div class="modal-content" style="max-width: 560px;">
|
||||
<div class="modal-header">
|
||||
<h2 id="webshell-modal-title" data-i18n="webshell.addConnection">添加连接</h2>
|
||||
<span class="modal-close" onclick="closeWebshellModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="webshell-edit-id" value="" />
|
||||
<div class="form-group">
|
||||
<label for="webshell-url"><span data-i18n="webshell.url">Shell 地址</span> <span style="color: red;">*</span></label>
|
||||
<input type="text" id="webshell-url" data-i18n="webshell.urlPlaceholder" data-i18n-attr="placeholder" placeholder="http(s)://target.com/shell.php" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-password" data-i18n="webshell.password">连接密码/密钥</label>
|
||||
<input type="text" id="webshell-password" data-i18n="webshell.passwordPlaceholder" data-i18n-attr="placeholder" placeholder="如冰蝎/蚁剑的连接密码" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-type" data-i18n="webshell.type">Shell 类型</label>
|
||||
<select id="webshell-type">
|
||||
<option value="php" data-i18n="webshell.typePhp">PHP</option>
|
||||
<option value="asp" data-i18n="webshell.typeAsp">ASP</option>
|
||||
<option value="aspx" data-i18n="webshell.typeAspx">ASPX</option>
|
||||
<option value="jsp" data-i18n="webshell.typeJsp">JSP</option>
|
||||
<option value="custom" data-i18n="webshell.typeCustom">自定义</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-method" data-i18n="webshell.method">请求方式</label>
|
||||
<select id="webshell-method">
|
||||
<option value="post" data-i18n="webshell.methodPost">POST</option>
|
||||
<option value="get" data-i18n="webshell.methodGet">GET</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-cmd-param" data-i18n="webshell.cmdParam">命令参数名</label>
|
||||
<input type="text" id="webshell-cmd-param" data-i18n="webshell.cmdParamPlaceholder" data-i18n-attr="placeholder" placeholder="不填默认为 cmd,如 xxx 则请求为 xxx=命令" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="webshell-remark" data-i18n="webshell.remark">备注</label>
|
||||
<input type="text" id="webshell-remark" data-i18n="webshell.remarkPlaceholder" data-i18n-attr="placeholder" placeholder="便于识别的备注名" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn-secondary" id="webshell-test-btn" onclick="testWebshellConnection()" data-i18n="webshell.testConnectivity">测试连通性</button>
|
||||
<button class="btn-secondary" onclick="closeWebshellModal()" data-i18n="common.cancel">取消</button>
|
||||
<button class="btn-primary" onclick="saveWebshellConnection()" data-i18n="common.save">保存</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 角色选择弹窗 -->
|
||||
<div id="role-select-modal" class="modal">
|
||||
<div class="modal-content role-select-modal-content">
|
||||
@@ -2221,6 +2313,7 @@ version: 1.0.0<br>
|
||||
<script src="/static/js/knowledge.js"></script>
|
||||
<script src="/static/js/skills.js"></script>
|
||||
<script src="/static/js/vulnerability.js?v=4"></script>
|
||||
<script src="/static/js/webshell.js"></script>
|
||||
<script src="/static/js/tasks.js"></script>
|
||||
<script src="/static/js/roles.js"></script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user