Compare commits

...

30 Commits

Author SHA1 Message Date
公明 dd7d15845c Update config.yaml 2026-03-12 21:17:57 +08:00
公明 ee9559e074 Add files via upload 2026-03-12 21:17:22 +08:00
公明 872e570518 Add files via upload 2026-03-12 20:54:09 +08:00
公明 a5ffafba77 Delete .github directory 2026-03-12 17:33:37 +08:00
公明 3da7f77e1c Update zh-CN.json 2026-03-12 15:40:49 +08:00
公明 26ad9646be Update en-US.json 2026-03-12 15:40:00 +08:00
公明 959a97870b Update en-US.json 2026-03-12 13:51:42 +08:00
公明 c8bbfcd171 Update zh-CN.json 2026-03-12 13:51:08 +08:00
公明 5f2862b629 Delete tools/nmap-advanced.yaml 2026-03-11 21:13:12 +08:00
公明 ee6c4b6f19 Add files via upload 2026-03-11 21:12:36 +08:00
公明 55b8decbaa Add files via upload 2026-03-11 21:01:45 +08:00
公明 1222adc485 Add files via upload 2026-03-11 20:02:21 +08:00
公明 38972bf93b Add files via upload 2026-03-11 19:47:43 +08:00
公明 127a5dd5c3 Update config.yaml 2026-03-10 09:12:58 +08:00
公明 f5f73d41c0 Add files via upload 2026-03-10 09:12:16 +08:00
公明 9811209002 Add files via upload 2026-03-10 00:28:33 +08:00
公明 f44bb42842 Update version number to v1.3.22 2026-03-10 00:25:35 +08:00
公明 d2e751e3d3 Add files via upload 2026-03-10 00:23:19 +08:00
公明 a5c285c8f3 Update version number to v1.3.21 2026-03-10 00:06:37 +08:00
公明 98938aef00 Remove user message check for Qwen model
Removed the logic to ensure at least one user message is included in recent messages to avoid Qwen model error.
2026-03-10 00:04:19 +08:00
公明 71f6a97a90 Add files via upload 2026-03-09 23:00:24 +08:00
公明 2fce15f82a Enhance MCP config with authentication headers
Added authentication headers for MCP server configuration.
2026-03-09 22:40:06 +08:00
公明 52b70d8b16 Add files via upload 2026-03-09 22:38:24 +08:00
公明 5b3709b9ad Add files via upload 2026-03-09 22:37:37 +08:00
公明 639f65602d Add files via upload 2026-03-09 22:36:22 +08:00
公明 52b6c3fe1b Add files via upload 2026-03-09 22:35:39 +08:00
公明 f26ee8e6e7 Add files via upload 2026-03-09 22:19:22 +08:00
公明 379486d36c Add files via upload 2026-03-09 21:35:49 +08:00
公明 317461e259 Add files via upload 2026-03-09 21:30:32 +08:00
公明 b7e724407b Add files via upload 2026-03-09 20:40:36 +08:00
33 changed files with 2609 additions and 947 deletions
-78
View File
@@ -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 可能会被标记为 `需要更多信息` 或直接关闭。请确保提供完整的信息以便我们能够快速定位和解决问题。
-68
View File
@@ -1,68 +0,0 @@
---
name: ✨ 功能优化建议
about: 提出新功能或优化建议
title: '[FEATURE] '
labels: ['enhancement', '待讨论']
assignees: ''
---
## 💡 功能描述
<!-- 请清晰、简洁地描述你希望添加或优化的功能 -->
## 🎯 使用场景
<!-- 描述这个功能的使用场景,解决什么问题 -->
<!-- 例如:在什么情况下会用到这个功能?它如何改善用户体验? -->
## 🔄 当前行为
<!-- 描述当前系统是如何处理相关需求的,或者为什么需要这个功能 -->
## ✨ 期望行为
<!-- 详细描述你期望的新功能或优化后的行为 -->
## 📸 参考示例(如有)
<!--
如果有其他项目的类似功能实现,可以在此提供截图或链接作为参考
⚠️ 请确保截图完整,包含所有相关界面元素
-->
<!-- 请在此处拖拽或粘贴参考截图 -->
## 🛠️ 实现建议(可选)
<!-- 如果你有具体的实现思路或技术建议,可以在此描述 -->
## 📊 优先级评估
<!-- 请选择你认为的优先级 -->
- [ ] 🔴 高优先级(严重影响使用体验或功能缺失)
- [ ] 🟡 中优先级(能显著改善体验)
- [ ] 🟢 低优先级(锦上添花的功能)
## 🔍 相关功能
<!-- 这个功能是否与现有功能相关? -->
<!-- 例如:是否与工具管理、攻击链分析、知识库等功能相关? -->
## 📝 额外信息
<!-- 任何其他有助于理解需求的信息 -->
- 是否已有替代方案?
- 这个功能是否会影响现有功能?
- 是否有相关的其他 issue 或讨论?
## ✅ 检查清单
<!-- 提交前请确认以下项目 -->
- [ ] 我已清晰描述了功能需求和使用场景
- [ ] 我已提供完整的参考截图(如有)
- [ ] 我已评估了功能的优先级
- [ ] 我已确认这不是重复的 issue
- [ ] 我已考虑了对现有功能的影响
---
**注意**:请提供尽可能详细的信息,包括使用场景、参考示例等,这将有助于我们更好地理解和实现你的需求。
+29 -15
View File
@@ -262,21 +262,33 @@ go build -o cyberstrike-ai cmd/server/main.go
```
Replace the paths with your local locations; Cursor will launch the stdio server automatically.
#### MCP HTTP quick start
1. Ensure `config.yaml` has `mcp.enabled: true` and adjust `mcp.host` / `mcp.port` if you need a non-default binding (localhost:8081 works well for local Cursor usage).
2. Start the main service (`./run.sh` or `go run cmd/server/main.go`); the MCP endpoint lives at `http://<host>:<port>/mcp`.
3. In Cursor, choose **Add Custom MCP → HTTP** and set `Base URL` to `http://127.0.0.1:8081/mcp`.
4. Prefer committing the setup via `.cursor/mcp.json` so teammates can reuse it:
```json
{
"mcpServers": {
"cyberstrike-ai-http": {
"transport": "http",
"url": "http://127.0.0.1:8081/mcp"
}
}
}
```
#### MCP HTTP quick start (Cursor / Claude Code)
The HTTP MCP server runs on a separate port (default `8081`) and supports **header-based authentication** so only clients that send the correct header can call tools.
1. **Enable MCP in config** In `config.yaml` set `mcp.enabled: true` and optionally `mcp.host` / `mcp.port`. For auth (recommended if the port is reachable from the network), set:
- `mcp.auth_header` header name (e.g. `X-MCP-Token`);
- `mcp.auth_header_value` secret value. **Leave it empty** if you want the server to **auto-generate** a random token on first start and write it back to the config.
2. **Start the service** Run `./run.sh` or `go run cmd/server/main.go`. The MCP endpoint is `http://<host>:<port>/mcp` (e.g. `http://localhost:8081/mcp`).
3. **Copy the JSON from the terminal** When MCP is enabled, the server prints a **ready-to-paste** JSON block. If `auth_header_value` was empty, it will have been generated and saved; the printed JSON includes the URL and headers.
4. **Use in Cursor or Claude Code**:
- **Cursor**: Paste the block into `~/.cursor/mcp.json` (or your projects `.cursor/mcp.json`) under `mcpServers`, or merge it into your existing `mcpServers`.
- **Claude Code**: Paste into `.mcp.json` or `~/.claude.json` under `mcpServers`.
Example of what the terminal prints (with auth enabled):
```json
{
"mcpServers": {
"cyberstrike-ai": {
"url": "http://localhost:8081/mcp",
"headers": {
"X-MCP-Token": "<auto-generated-or-your-value>"
},
"type": "http"
}
}
}
```
If you do not set `auth_header` / `auth_header_value`, the endpoint accepts requests without authentication (suitable only for localhost or trusted networks).
#### External MCP federation (HTTP/stdio/SSE)
CyberStrikeAI supports connecting to external MCP servers via three transport modes:
@@ -396,6 +408,8 @@ mcp:
enabled: true
host: "0.0.0.0"
port: 8081
auth_header: "X-MCP-Token" # optional; leave empty for no auth
auth_header_value: "" # optional; leave empty to auto-generate on first start
openai:
api_key: "sk-xxx"
base_url: "https://api.deepseek.com/v1"
+29 -15
View File
@@ -260,21 +260,33 @@ go build -o cyberstrike-ai cmd/server/main.go
```
将路径替换成你本地的实际地址,Cursor 会自动启动 stdio 版本的 MCP。
#### MCP HTTP 快速集成
1. 确认 `config.yaml` 中 `mcp.enabled: true`,按照需要调整 `mcp.host` / `mcp.port`(本地建议 `127.0.0.1:8081`
2. 启动主服务(`./run.sh` 或 `go run cmd/server/main.go`),MCP 端点默认暴露在 `http://<host>:<port>/mcp`。
3. 在 Cursor 内 `Add Custom MCP → HTTP`,将 `Base URL` 设置 `http://127.0.0.1:8081/mcp`。
4. 也可以在项目根目录创建 `.cursor/mcp.json` 以便团队共享:
```json
{
"mcpServers": {
"cyberstrike-ai-http": {
"transport": "http",
"url": "http://127.0.0.1:8081/mcp"
}
}
}
```
#### MCP HTTP 快速集成Cursor / Claude Code
HTTP MCP 服务在独立端口(默认 `8081`)运行,支持 **Header 鉴权**:仅携带正确 header 的客户端可调用工具
1. **在配置中启用 MCP** – 在 `config.yaml` 中设置 `mcp.enabled: true`,并按需设置 `mcp.host` / `mcp.port`。若需鉴权(端口对外暴露时建议开启),可设置:
- `mcp.auth_header`:鉴权用的 header 名(如 `X-MCP-Token`);
- `mcp.auth_header_value`:鉴权密钥。**留空**时,首次启动会自动生成随机密钥并写回配置文件。
2. **启动服务** 执行 `./run.sh` 或 `go run cmd/server/main.go`。MCP 端点为 `http://<host>:<port>/mcp`(例如 `http://localhost:8081/mcp`)。
3. **从终端复制 JSON** – 启用 MCP 后,启动时会在终端打印一段 **可直接复制的 JSON**。若 `auth_header_value` 留空,会自动生成并写入配置,打印内容中会包含 URL 与 headers。
4. **在 Cursor 或 Claude Code 中使用**
- **Cursor**:将整段 JSON 粘贴到 `~/.cursor/mcp.json` 或项目下的 `.cursor/mcp.json` 的 `mcpServers` 中(或合并进现有 `mcpServers`)。
- **Claude Code**:粘贴到 `.mcp.json` 或 `~/.claude.json` 的 `mcpServers` 中。
终端打印示例(开启鉴权时):
```json
{
"mcpServers": {
"cyberstrike-ai": {
"url": "http://localhost:8081/mcp",
"headers": {
"X-MCP-Token": "<自动生成或你配置的值>"
},
"type": "http"
}
}
}
```
若不配置 `auth_header` / `auth_header_value`,则端点不鉴权(仅适合本机或可信网络)。
#### 外部 MCP 联邦(HTTP/stdio/SSE
CyberStrikeAI 支持通过三种传输模式连接外部 MCP 服务器:
@@ -395,6 +407,8 @@ mcp:
enabled: true
host: "0.0.0.0"
port: 8081
auth_header: "X-MCP-Token" # 可选;留空则不鉴权
auth_header_value: "" # 可选;留空则首次启动自动生成并写回
openai:
api_key: "sk-xxx"
base_url: "https://api.deepseek.com/v1"
+9
View File
@@ -19,6 +19,15 @@ func main() {
return
}
// MCP 启用且 auth_header_value 为空时,自动生成随机密钥并写回配置
if err := config.EnsureMCPAuth(*configPath, cfg); err != nil {
fmt.Printf("MCP 鉴权配置失败: %v\n", err)
return
}
if cfg.MCP.Enabled {
config.PrintMCPConfigJSON(cfg.MCP)
}
// 初始化日志
log := logger.New(cfg.Log.Level, cfg.Log.Output)
+5 -3
View File
@@ -10,7 +10,7 @@
# ============================================
# 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.3.20"
version: "v1.3.24"
# 服务器配置
server:
@@ -93,8 +93,10 @@ security:
# MCP (Model Context Protocol) 用于工具注册和调用
mcp:
enabled: false # 是否启用 MCP 服务器(http模式)
host: 0.0.0.0 # MCP 服务器监听地址
port: 8081 # MCP 服务器端口
host: 0.0.0.0 # MCP 服务器监听地址
port: 8081 # MCP 服务器端口
auth_header: "X-MCP-Token" # 鉴权:请求需携带该 header 且值与 auth_header_value 一致方可调用。留空表示不鉴权
auth_header_value: "" # 鉴权密钥值(与 auth_header 配合使用,建议使用随机字符串)
# 外部 MCP 配置
external_mcp:
+1 -22
View File
@@ -345,29 +345,8 @@ func (mc *MemoryCompressor) adjustRecentStartForToolCalls(msgs []ChatMessage, re
adjusted--
}
// Ensure at least one user message is included in recent messages to avoid Qwen model error
// Qwen models require a user message in the message array, otherwise they return:
// "No user query found in messages"
hasUserMessage := false
for i := adjusted; i < len(msgs); i++ {
if strings.EqualFold(msgs[i].Role, "user") {
hasUserMessage = true
break
}
}
// If no user message in recent messages, adjust backwards to include one
if !hasUserMessage {
for adjusted > 0 {
adjusted--
if strings.EqualFold(msgs[adjusted].Role, "user") {
break
}
}
}
if adjusted != recentStart {
mc.logger.Debug("adjusted recent window to keep tool call context and user message",
mc.logger.Debug("adjusted recent window to keep tool call context",
zap.Int("original_recent_start", recentStart),
zap.Int("adjusted_recent_start", adjusted),
)
+16 -1
View File
@@ -442,6 +442,21 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
}
// mcpHandlerWithAuth 在鉴权通过后转发到 MCP 处理;若配置了 auth_header 则校验请求头,否则直接放行
func (a *App) mcpHandlerWithAuth(w http.ResponseWriter, r *http.Request) {
cfg := a.config.MCP
if cfg.AuthHeader != "" {
if r.Header.Get(cfg.AuthHeader) != cfg.AuthHeaderValue {
a.logger.Logger.Debug("MCP 鉴权失败:header 缺失或值不匹配", zap.String("header", cfg.AuthHeader))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"unauthorized"}`))
return
}
}
a.mcpServer.HandleHTTP(w, r)
}
// Run 启动应用
func (a *App) Run() error {
// 启动MCP服务器(如果启用)
@@ -451,7 +466,7 @@ func (a *App) Run() error {
a.logger.Info("启动MCP服务器", zap.String("address", mcpAddr))
mux := http.NewServeMux()
mux.HandleFunc("/mcp", a.mcpServer.HandleHTTP)
mux.HandleFunc("/mcp", a.mcpHandlerWithAuth)
if err := http.ListenAndServe(mcpAddr, mux); err != nil {
a.logger.Error("MCP服务器启动失败", zap.Error(err))
+125 -3
View File
@@ -3,6 +3,8 @@ package config
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -74,9 +76,11 @@ type LogConfig struct {
}
type MCPConfig struct {
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
AuthHeader string `yaml:"auth_header,omitempty"` // 鉴权 header 名,留空表示不鉴权
AuthHeaderValue string `yaml:"auth_header_value,omitempty"` // 鉴权 header 值,需与请求中该 header 一致
}
type OpenAIConfig struct {
@@ -384,6 +388,124 @@ func PrintGeneratedPasswordWarning(password string, persisted bool, persistErr s
fmt.Println("----------------------------------------------------------------")
}
// generateRandomToken 生成用于 MCP 鉴权的随机字符串(64 位十六进制)
func generateRandomToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// persistMCPAuth 将 MCP 的 auth_header / auth_header_value 写回配置文件
func persistMCPAuth(path string, mcp *MCPConfig) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
lines := strings.Split(string(data), "\n")
inMcpBlock := false
mcpIndent := -1
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if !inMcpBlock {
if strings.HasPrefix(trimmed, "mcp:") {
inMcpBlock = true
mcpIndent = len(line) - len(strings.TrimLeft(line, " "))
}
continue
}
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
leadingSpaces := len(line) - len(strings.TrimLeft(line, " "))
if leadingSpaces <= mcpIndent {
inMcpBlock = false
mcpIndent = -1
if strings.HasPrefix(trimmed, "mcp:") {
inMcpBlock = true
mcpIndent = leadingSpaces
}
continue
}
prefix := line[:leadingSpaces]
rest := strings.TrimSpace(line[leadingSpaces:])
comment := ""
if idx := strings.Index(line, "#"); idx >= 0 {
comment = strings.TrimRight(line[idx:], " ")
}
withComment := ""
if comment != "" {
if !strings.HasPrefix(comment, " ") {
withComment = " "
}
withComment += comment
}
if strings.HasPrefix(rest, "auth_header_value:") {
lines[i] = fmt.Sprintf("%sauth_header_value: %q%s", prefix, mcp.AuthHeaderValue, withComment)
} else if strings.HasPrefix(rest, "auth_header:") {
lines[i] = fmt.Sprintf("%sauth_header: %q%s", prefix, mcp.AuthHeader, withComment)
}
}
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
}
// EnsureMCPAuth 在 MCP 启用且 auth_header_value 为空时,自动生成随机密钥并写回配置
func EnsureMCPAuth(path string, cfg *Config) error {
if !cfg.MCP.Enabled || strings.TrimSpace(cfg.MCP.AuthHeaderValue) != "" {
return nil
}
token, err := generateRandomToken()
if err != nil {
return fmt.Errorf("生成 MCP 鉴权密钥失败: %w", err)
}
cfg.MCP.AuthHeaderValue = token
if strings.TrimSpace(cfg.MCP.AuthHeader) == "" {
cfg.MCP.AuthHeader = "X-MCP-Token"
}
return persistMCPAuth(path, &cfg.MCP)
}
// PrintMCPConfigJSON 向终端输出 MCP 配置的 JSON,可直接复制到 Cursor / Claude Code 的 mcp 配置中使用
func PrintMCPConfigJSON(mcp MCPConfig) {
if !mcp.Enabled {
return
}
hostForURL := strings.TrimSpace(mcp.Host)
if hostForURL == "" || hostForURL == "0.0.0.0" {
hostForURL = "localhost"
}
url := fmt.Sprintf("http://%s:%d/mcp", hostForURL, mcp.Port)
headers := map[string]string{}
if mcp.AuthHeader != "" {
headers[mcp.AuthHeader] = mcp.AuthHeaderValue
}
serverEntry := map[string]interface{}{
"url": url,
}
if len(headers) > 0 {
serverEntry["headers"] = headers
}
// Claude Code 需要 type: "http"
serverEntry["type"] = "http"
out := map[string]interface{}{
"mcpServers": map[string]interface{}{
"cyberstrike-ai": serverEntry,
},
}
b, _ := json.MarshalIndent(out, "", " ")
fmt.Println("[CyberStrikeAI] MCP 配置(可复制到 Cursor / Claude Code 使用):")
fmt.Println(" Cursor: 放入 ~/.cursor/mcp.json 的 mcpServers,或项目 .cursor/mcp.json")
fmt.Println(" Claude Code: 放入 .mcp.json 或 ~/.claude.json 的 mcpServers")
fmt.Println("----------------------------------------------------------------")
fmt.Println(string(b))
fmt.Println("----------------------------------------------------------------")
}
// LoadToolsFromDir 从目录加载所有工具配置文件
func LoadToolsFromDir(dir string) ([]ToolConfig, error) {
var tools []ToolConfig
+1
View File
@@ -4411,6 +4411,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
},
}
enrichSpecWithI18nKeys(spec)
c.JSON(http.StatusOK, spec)
}
+139
View File
@@ -0,0 +1,139 @@
package handler
// apiDocI18n 为 OpenAPI 文档提供 x-i18n-* 扩展键,供前端 apiDocs 国际化使用。
// 前端通过 apiDocs.tags.* / apiDocs.summary.* / apiDocs.response.* 翻译。
var apiDocI18nTagToKey = map[string]string{
"认证": "auth", "对话管理": "conversationManagement", "对话交互": "conversationInteraction",
"批量任务": "batchTasks", "对话分组": "conversationGroups", "漏洞管理": "vulnerabilityManagement",
"角色管理": "roleManagement", "Skills管理": "skillsManagement", "监控": "monitoring",
"配置管理": "configManagement", "外部MCP管理": "externalMCPManagement", "攻击链": "attackChain",
"知识库": "knowledgeBase", "MCP": "mcp",
}
var apiDocI18nSummaryToKey = map[string]string{
"用户登录": "login", "用户登出": "logout", "修改密码": "changePassword", "验证Token": "validateToken",
"创建对话": "createConversation", "列出对话": "listConversations", "查看对话详情": "getConversationDetail",
"更新对话": "updateConversation", "删除对话": "deleteConversation", "获取对话结果": "getConversationResult",
"发送消息并获取AI回复(非流式)": "sendMessageNonStream", "发送消息并获取AI回复(流式)": "sendMessageStream",
"取消任务": "cancelTask", "列出运行中的任务": "listRunningTasks", "列出已完成的任务": "listCompletedTasks",
"创建批量任务队列": "createBatchQueue", "列出批量任务队列": "listBatchQueues", "获取批量任务队列": "getBatchQueue",
"删除批量任务队列": "deleteBatchQueue", "启动批量任务队列": "startBatchQueue", "暂停批量任务队列": "pauseBatchQueue",
"添加任务到队列": "addTaskToQueue", "SQL注入扫描": "sqlInjectionScan", "端口扫描": "portScan",
"更新批量任务": "updateBatchTask", "删除批量任务": "deleteBatchTask",
"创建分组": "createGroup", "列出分组": "listGroups", "获取分组": "getGroup", "更新分组": "updateGroup",
"删除分组": "deleteGroup", "获取分组中的对话": "getGroupConversations", "添加对话到分组": "addConversationToGroup",
"从分组移除对话": "removeConversationFromGroup",
"列出漏洞": "listVulnerabilities", "创建漏洞": "createVulnerability", "获取漏洞统计": "getVulnerabilityStats",
"获取漏洞": "getVulnerability", "更新漏洞": "updateVulnerability", "删除漏洞": "deleteVulnerability",
"列出角色": "listRoles", "创建角色": "createRole", "获取角色": "getRole", "更新角色": "updateRole", "删除角色": "deleteRole",
"获取可用Skills列表": "getAvailableSkills", "列出Skills": "listSkills", "创建Skill": "createSkill",
"获取Skill统计": "getSkillStats", "清空Skill统计": "clearSkillStats", "获取Skill": "getSkill",
"更新Skill": "updateSkill", "删除Skill": "deleteSkill", "获取绑定角色": "getBoundRoles",
"获取监控信息": "getMonitorInfo", "获取执行记录": "getExecutionRecords", "删除执行记录": "deleteExecutionRecord",
"批量删除执行记录": "batchDeleteExecutionRecords", "获取统计信息": "getStats",
"获取配置": "getConfig", "更新配置": "updateConfig", "获取工具配置": "getToolConfig", "应用配置": "applyConfig",
"列出外部MCP": "listExternalMCP", "获取外部MCP统计": "getExternalMCPStats", "获取外部MCP": "getExternalMCP",
"添加或更新外部MCP": "addOrUpdateExternalMCP", "stdio模式配置": "stdioModeConfig", "SSE模式配置": "sseModeConfig",
"删除外部MCP": "deleteExternalMCP", "启动外部MCP": "startExternalMCP", "停止外部MCP": "stopExternalMCP",
"获取攻击链": "getAttackChain", "重新生成攻击链": "regenerateAttackChain",
"设置对话置顶": "pinConversation", "设置分组置顶": "pinGroup", "设置分组中对话的置顶": "pinGroupConversation",
"获取分类": "getCategories", "列出知识项": "listKnowledgeItems", "创建知识项": "createKnowledgeItem",
"获取知识项": "getKnowledgeItem", "更新知识项": "updateKnowledgeItem", "删除知识项": "deleteKnowledgeItem",
"获取索引状态": "getIndexStatus", "重建索引": "rebuildIndex", "扫描知识库": "scanKnowledgeBase",
"搜索知识库": "searchKnowledgeBase", "基础搜索": "basicSearch", "按风险类型搜索": "searchByRiskType",
"获取检索日志": "getRetrievalLogs", "删除检索日志": "deleteRetrievalLog",
"MCP端点": "mcpEndpoint", "列出所有工具": "listAllTools", "调用工具": "invokeTool", "初始化连接": "initConnection",
"成功响应": "successResponse", "错误响应": "errorResponse",
}
var apiDocI18nResponseDescToKey = map[string]string{
"获取成功": "getSuccess", "未授权": "unauthorized", "未授权,需要有效的Token": "unauthorizedToken",
"创建成功": "createSuccess", "请求参数错误": "badRequest", "对话不存在": "conversationNotFound",
"对话不存在或结果不存在": "conversationOrResultNotFound", "请求参数错误(如task为空)": "badRequestTaskEmpty",
"请求参数错误或分组名称已存在": "badRequestGroupNameExists", "分组不存在": "groupNotFound",
"请求参数错误(如配置格式不正确、缺少必需字段等)": "badRequestConfig",
"请求参数错误(如query为空)": "badRequestQueryEmpty", "方法不允许(仅支持POST请求)": "methodNotAllowed",
"登录成功": "loginSuccess", "密码错误": "invalidPassword", "登出成功": "logoutSuccess",
"密码修改成功": "passwordChanged", "Token有效": "tokenValid", "Token无效或已过期": "tokenInvalid",
"对话创建成功": "conversationCreated", "服务器内部错误": "internalError", "更新成功": "updateSuccess",
"删除成功": "deleteSuccess", "队列不存在": "queueNotFound", "启动成功": "startSuccess",
"暂停成功": "pauseSuccess", "添加成功": "addSuccess",
"任务不存在": "taskNotFound", "对话或分组不存在": "conversationOrGroupNotFound",
"取消请求已提交": "cancelSubmitted", "未找到正在执行的任务": "noRunningTask",
"消息发送成功,返回AI回复": "messageSent", "流式响应(Server-Sent Events": "streamResponse",
}
// enrichSpecWithI18nKeys 在 spec 的每个 operation 上写入 x-i18n-tags、x-i18n-summary
// 在每个 response 上写入 x-i18n-description,供前端按 key 做国际化。
func enrichSpecWithI18nKeys(spec map[string]interface{}) {
paths, _ := spec["paths"].(map[string]interface{})
if paths == nil {
return
}
for _, pathItem := range paths {
pm, _ := pathItem.(map[string]interface{})
if pm == nil {
continue
}
for _, method := range []string{"get", "post", "put", "delete", "patch"} {
opVal, ok := pm[method]
if !ok {
continue
}
op, _ := opVal.(map[string]interface{})
if op == nil {
continue
}
// x-i18n-tags: 与 tags 一一对应的 i18n 键数组(spec 中 tags 为 []string
switch tags := op["tags"].(type) {
case []string:
if len(tags) > 0 {
keys := make([]string, 0, len(tags))
for _, s := range tags {
if k := apiDocI18nTagToKey[s]; k != "" {
keys = append(keys, k)
} else {
keys = append(keys, s)
}
}
op["x-i18n-tags"] = keys
}
case []interface{}:
if len(tags) > 0 {
keys := make([]interface{}, 0, len(tags))
for _, t := range tags {
if s, ok := t.(string); ok {
if k := apiDocI18nTagToKey[s]; k != "" {
keys = append(keys, k)
} else {
keys = append(keys, s)
}
}
}
if len(keys) > 0 {
op["x-i18n-tags"] = keys
}
}
}
// x-i18n-summary
if summary, _ := op["summary"].(string); summary != "" {
if k := apiDocI18nSummaryToKey[summary]; k != "" {
op["x-i18n-summary"] = k
}
}
// responses -> 每个 status -> x-i18n-description
if respMap, _ := op["responses"].(map[string]interface{}); respMap != nil {
for _, rv := range respMap {
if r, _ := rv.(map[string]interface{}); r != nil {
if desc, _ := r["description"].(string); desc != "" {
if k := apiDocI18nResponseDescToKey[desc]; k != "" {
r["x-i18n-description"] = k
}
}
}
}
}
}
}
}
+18 -8
View File
@@ -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
}
}
+56 -6
View File
@@ -19,6 +19,7 @@ type AgentTask struct {
Message string `json:"message,omitempty"`
StartedAt time.Time `json:"startedAt"`
Status string `json:"status"`
CancellingAt time.Time `json:"-"` // 进入 cancelling 状态的时间,用于清理长时间卡住的任务
cancel func(error)
}
@@ -41,13 +42,61 @@ type AgentTaskManager struct {
historyRetention time.Duration // 历史记录保留时间
}
const (
// cancellingStuckThreshold 处于「取消中」超过此时长则强制从运行列表移除。正常取消会在当前步骤内返回,
// 超过则视为卡住,尽快释放会话。常见做法多为 30–60s 内释放。
cancellingStuckThreshold = 45 * time.Second
// cancellingStuckThresholdLegacy 未记录 CancellingAt 时用 StartedAt 判断的兜底时长
cancellingStuckThresholdLegacy = 2 * time.Minute
cleanupInterval = 15 * time.Second // 与上面阈值配合,最长约 60s 内移除
)
// NewAgentTaskManager 创建任务管理器
func NewAgentTaskManager() *AgentTaskManager {
return &AgentTaskManager{
m := &AgentTaskManager{
tasks: make(map[string]*AgentTask),
completedTasks: make([]*CompletedTask, 0),
maxHistorySize: 50, // 最多保留50条历史记录
historyRetention: 24 * time.Hour, // 保留24小时
maxHistorySize: 50, // 最多保留50条历史记录
historyRetention: 24 * time.Hour, // 保留24小时
}
go m.runStuckCancellingCleanup()
return m
}
// runStuckCancellingCleanup 定期将长时间处于「取消中」的任务强制结束,避免卡住无法发新消息
func (m *AgentTaskManager) runStuckCancellingCleanup() {
ticker := time.NewTicker(cleanupInterval)
defer ticker.Stop()
for range ticker.C {
m.cleanupStuckCancelling()
}
}
func (m *AgentTaskManager) cleanupStuckCancelling() {
m.mu.Lock()
var toFinish []string
now := time.Now()
for id, task := range m.tasks {
if task.Status != "cancelling" {
continue
}
var elapsed time.Duration
if !task.CancellingAt.IsZero() {
elapsed = now.Sub(task.CancellingAt)
if elapsed < cancellingStuckThreshold {
continue
}
} else {
elapsed = now.Sub(task.StartedAt)
if elapsed < cancellingStuckThresholdLegacy {
continue
}
}
toFinish = append(toFinish, id)
}
m.mu.Unlock()
for _, id := range toFinish {
m.FinishTask(id, "cancelled")
}
}
@@ -76,7 +125,7 @@ func (m *AgentTaskManager) StartTask(conversationID, message string, cancel cont
return task, nil
}
// CancelTask 取消指定会话的任务
// CancelTask 取消指定会话的任务。若任务已在取消中,仍返回 (true, nil) 以便接口幂等、前端不报错。
func (m *AgentTaskManager) CancelTask(conversationID string, cause error) (bool, error) {
m.mu.Lock()
task, exists := m.tasks[conversationID]
@@ -85,13 +134,14 @@ func (m *AgentTaskManager) CancelTask(conversationID string, cause error) (bool,
return false, nil
}
// 如果已经处于取消流程,直接返回
// 如果已经处于取消流程,视为成功(幂等),避免前端重复点击报「未找到任务」
if task.Status == "cancelling" {
m.mu.Unlock()
return false, nil
return true, nil
}
task.Status = "cancelling"
task.CancellingAt = time.Now()
cancel := task.cancel
m.mu.Unlock()
+8 -3
View File
@@ -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 后的第一个位置参数传入。
示例等价 CLIdalfox url "http://target/page?q=test"
required: true
flag: "-u"
format: "flag"
position: 0
format: "positional"
- name: "pipe_mode"
type: "bool"
description: "使用管道模式输入"
-86
View File
@@ -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
View File
@@ -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: |
网络映射端口扫描工具,用于发现网络中的主机、服务和开放端口
网络映射端口扫描,合并了原「nmap」与「nmap-advanced」的能力
**主要功能**
- 主机发现:检测网络中的活动主机
- 端口扫描:识别目标主机上开放的端口
- 服务识别:检测运行在端口上的服务类型和版
- 操作系统检测:识别目标主机的操作系统类型
- 漏洞检测:使用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: 0 # 位置参数,放在命令最后
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
View File
@@ -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
# --scriptsnone | default | customCLI 默认 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"
# --toptop 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"
+103
View File
@@ -457,6 +457,104 @@ body {
flex-shrink: 0;
height: 100%;
overflow: hidden;
position: relative;
transition: width 0.2s ease;
}
/* 对话页左侧列表折叠(腾出空间给主对话区) */
.conversation-sidebar.collapsed {
width: 56px;
}
/* 对话列表头部:折叠 + 新对话同一行,不重叠 */
.conversation-sidebar-header {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 8px;
flex-shrink: 0;
}
.conversation-sidebar-header .new-chat-btn {
flex: 1;
min-width: 0;
width: auto; /* 覆盖 .new-chat-btn 的 width:100%,让 flex 分配剩余空间 */
}
.conversation-sidebar-collapse-btn {
/* 不再使用 absolute,避免盖住「新对话」 */
position: static;
flex-shrink: 0;
align-self: center;
width: 36px;
height: 36px;
min-width: 36px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 8px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.conversation-sidebar-collapse-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--accent-color);
}
/* 折叠后箭头朝右表示「展开」 */
.conversation-sidebar.collapsed .conversation-sidebar-collapse-btn {
transform: rotate(180deg);
}
.conversation-sidebar-collapse-btn svg {
width: 16px;
height: 16px;
stroke: currentColor;
}
.conversation-sidebar.collapsed .sidebar-content {
display: none;
}
.conversation-sidebar.collapsed .conversation-sidebar-header {
flex-direction: column;
align-items: center;
padding: 12px 8px;
gap: 10px;
border-bottom: none;
}
.conversation-sidebar.collapsed .conversation-sidebar-collapse-btn {
order: 2;
}
.conversation-sidebar.collapsed .new-chat-btn {
order: 1; /* 窄条:先「+」再折叠,纵向排列 */
width: 40px;
height: 40px;
min-width: 40px;
padding: 0;
margin: 0;
border-radius: 8px;
gap: 0;
flex: none;
}
/* 折叠时只保留「+」,隐藏「新对话」文案 */
.conversation-sidebar.collapsed .new-chat-btn span:last-child {
display: none;
}
.conversation-sidebar.collapsed .new-chat-btn span:first-child {
font-size: 1.5em;
margin: 0;
}
header {
@@ -2593,6 +2691,11 @@ header {
height: 100%;
overflow: hidden;
}
/* 对话列表折叠态在窄屏下仍保持窄条,避免被 240px 覆盖 */
.conversation-sidebar.collapsed {
width: 56px;
}
.sidebar-content {
min-height: 0;
+420 -16
View File
@@ -1,4 +1,8 @@
{
"lang": {
"zhCN": "中文",
"enUS": "English"
},
"common": {
"ok": "OK",
"cancel": "Cancel",
@@ -14,7 +18,8 @@
"confirm": "Confirm",
"copy": "Copy",
"copied": "Copied",
"copyFailed": "Copy failed"
"copyFailed": "Copy failed",
"view": "View"
},
"header": {
"title": "CyberStrikeAI",
@@ -99,6 +104,7 @@
},
"chat": {
"newChat": "New chat",
"toggleConversationPanel": "Collapse/expand conversation list",
"searchHistory": "Search history...",
"conversationGroups": "Conversation groups",
"addGroup": "New group",
@@ -121,6 +127,7 @@
"copyMessageTitle": "Copy message",
"emptyGroupConversations": "This group has no conversations yet.",
"noMatchingConversationsInGroup": "No matching conversations found.",
"noHistoryConversations": "No conversation history yet",
"renameGroupPrompt": "Please enter new name:",
"deleteGroupConfirm": "Are you sure you want to delete this group? Conversations in the group will not be deleted, but will be removed from the group.",
"deleteConversationConfirm": "Are you sure you want to delete this conversation?",
@@ -130,10 +137,54 @@
"executeFailed": "Execution failed",
"callOpenAIFailed": "Call OpenAI failed",
"systemReadyMessage": "System is ready. Please enter your test requirements, and the system will automatically perform the corresponding security tests.",
"addNewGroup": "+ New group"
"addNewGroup": "+ New group",
"callNumber": "Call #{{n}}",
"iterationRound": "Iteration {{n}}",
"aiThinking": "AI thinking",
"toolCallsDetected": "Detected {{count}} tool call(s)",
"callTool": "Call tool: {{name}} ({{index}}/{{total}})",
"toolExecComplete": "Tool {{name}} completed",
"toolExecFailed": "Tool {{name}} failed",
"knowledgeRetrieval": "Knowledge retrieval",
"knowledgeRetrievalTag": "Knowledge retrieval",
"error": "Error",
"taskCancelled": "Task cancelled",
"unknownTool": "Unknown tool",
"noDescription": "No description",
"noResponseData": "No response data",
"loading": "Loading...",
"loadFailed": "Load failed: {{message}}",
"noAttackChainData": "No attack chain data",
"copyFailedManual": "Copy failed, please select and copy manually",
"searching": "Searching...",
"loadFailedRetry": "Load failed, please retry",
"dataFormatError": "Data format error",
"progressInProgress": "Penetration test in progress...",
"executionFailed": "Execution failed",
"penetrationTestComplete": "Penetration test complete",
"yesterday": "Yesterday"
},
"progress": {
"callingAI": "Calling AI model...",
"callingTool": "Calling tool: {{name}}",
"lastIterSummary": "Last iteration: generating summary and next steps...",
"summaryDone": "Summary complete",
"generatingFinalReply": "Generating final reply...",
"maxIterSummary": "Max iterations reached, generating summary..."
},
"timeline": {
"params": "Parameters:",
"executionResult": "Execution result:",
"executionId": "Execution ID:",
"noResult": "No result",
"running": "Running...",
"completed": "Completed",
"execFailed": "Execution failed"
},
"tasks": {
"title": "Task management",
"stopTask": "Stop task",
"collapseDetail": "Collapse details",
"newTask": "New task",
"autoRefresh": "Auto refresh",
"historyHint": "Tip: Completed task history available. Check \"Show history\" to view.",
@@ -153,6 +204,10 @@
"unknownTime": "Unknown time",
"clearHistoryConfirm": "Clear all task history?",
"cancelTaskFailed": "Cancel task failed",
"cancelFailed": "Cancel failed",
"taskInfoNotSynced": "Task info not synced yet, please try again later.",
"loadActiveTasksFailed": "Failed to load active tasks",
"cannotGetTaskStatus": "Cannot get task status",
"copiedToast": "Copied!",
"cancelling": "Cancelling...",
"enterTaskPrompt": "Enter at least one task",
@@ -340,6 +395,7 @@
"configStdioNeedCommand": "Config error: \"{{name}}\" stdio mode needs command",
"configHttpNeedUrl": "Config error: \"{{name}}\" http mode needs url",
"configSseNeedUrl": "Config error: \"{{name}}\" sse mode needs url",
"configEditMustContainName": "Config error: In edit mode, JSON must contain config name \"{{name}}\"",
"saveSuccess": "Saved",
"deleteSuccess": "Deleted",
"deleteExternalConfirm": "Delete external MCP \"{{name}}\"?",
@@ -349,7 +405,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",
@@ -364,15 +425,36 @@
"description": "Configure WeCom, DingTalk and Lark bots so you can chat with CyberStrikeAI on your phone without opening the web UI.",
"wecom": {
"title": "WeCom",
"enabled": "Enable WeCom bot"
"enabled": "Enable WeCom bot",
"token": "Token",
"tokenPlaceholder": "Token",
"encodingAesKey": "EncodingAESKey",
"encodingAesKeyPlaceholder": "EncodingAESKey (leave empty for plain mode)",
"corpId": "CorpID",
"corpIdPlaceholder": "Corp ID",
"secret": "Secret",
"secretPlaceholder": "App Secret",
"agentId": "AgentID",
"agentIdPlaceholder": "App AgentId"
},
"dingtalk": {
"title": "DingTalk",
"enabled": "Enable DingTalk bot"
"enabled": "Enable DingTalk bot",
"clientIdLabel": "Client ID (AppKey)",
"clientIdPlaceholder": "DingTalk App Key",
"clientSecretLabel": "Client Secret",
"clientSecretPlaceholder": "DingTalk App Secret",
"streamHint": "Enable bot capability and configure streaming access in the open platform."
},
"lark": {
"title": "Lark",
"enabled": "Enable Lark bot"
"enabled": "Enable Lark bot",
"appIdLabel": "App ID",
"appIdPlaceholder": "Lark/Feishu App ID",
"appSecretLabel": "App Secret",
"appSecretPlaceholder": "Lark/Feishu App Secret",
"verifyTokenLabel": "Verify Token (Optional)",
"verifyTokenPlaceholder": "Event subscription Verification Token"
}
},
"apply": {
@@ -414,7 +496,15 @@
"title": "Role management",
"createRole": "Create role",
"searchPlaceholder": "Search roles...",
"deleteConfirm": "Delete this role?"
"deleteConfirm": "Delete this role?",
"loadFailed": "Failed to load roles",
"noDescription": "No description",
"defaultRoleDescription": "Default role, no extra user prompt, uses default MCP",
"noMatchingRoles": "No matching roles",
"noRoles": "No roles",
"enabled": "Enabled",
"disabled": "Disabled",
"noDescriptionShort": "No description"
},
"skills": {
"title": "Skills management",
@@ -436,10 +526,246 @@
"loadStatsFailed": "Failed to load skills monitor data",
"clearStatsConfirm": "Clear all Skills statistics? This cannot be undone.",
"statsCleared": "Skills statistics cleared",
"clearStatsFailed": "Failed to clear statistics"
"clearStatsFailed": "Failed to clear statistics",
"noDescription": "No description",
"viewSkillTitle": "View Skill: {{name}}",
"descriptionLabel": "Description:",
"pathLabel": "Path:",
"modTimeLabel": "Modified:",
"contentLabel": "Content:",
"nameRequired": "Skill name is required",
"contentRequired": "Skill content is required",
"nameInvalid": "Skill name can only contain letters, numbers, hyphens and underscores",
"saveSuccess": "Skill updated",
"createdSuccess": "Skill created",
"deleteConfirm": "Are you sure you want to delete skill \"{{name}}\"? This cannot be undone.",
"deleteConfirmWithRoles": "Are you sure you want to delete skill \"{{name}}\"?\n\n⚠️ This skill is currently bound to {{count}} role(s):\n{{roles}}\n\nAfter deletion, the system will automatically remove this skill from those roles.\n\nThis cannot be undone. Continue?",
"deleteSuccess": "Skill deleted",
"deleteSuccessWithRoles": "Skill deleted and automatically removed from {{count}} role(s): {{roles}}",
"loadFailedShort": "Load failed",
"totalSkillsCount": "Total Skills",
"totalCallsCount": "Total Call Count",
"successfulCalls": "Successful Calls",
"failedCalls": "Failed Calls",
"successRate": "Success Rate",
"skillName": "Skill Name",
"totalCalls": "Total Calls",
"success": "Success",
"failure": "Failure",
"lastCallTime": "Last Call Time",
"noCallRecords": "No Skills call records yet",
"loadStatsErrorShort": "Failed to load statistics",
"loadCallStatsError": "Failed to load call statistics"
},
"apiDocs": {
"curlCopied": "curl command copied to clipboard!"
"pageTitle": "API Docs - CyberStrikeAI",
"title": "API Docs",
"subtitle": "CyberStrikeAI platform API documentation with online testing",
"authTitle": "API Authentication",
"authAllNeedToken": "All API endpoints require Token authentication.",
"authGetToken": "1. Get Token:",
"authGetTokenDesc": "After logging in on the frontend, the Token is saved automatically. You can also get it via:",
"authUseToken": "2. Use Token:",
"authUseTokenDesc": "Add the Authorization header:",
"authTip": "💡 This page will use your logged-in Token automatically; no need to fill it manually.",
"tokenDetected": "✓ Token detected - You can test API endpoints directly",
"tokenNotDetected": "⚠ No Token detected - Please log in on the frontend first, then refresh this page. When testing, add Authorization: Bearer token in the request header",
"sidebarGroupTitle": "API Groups",
"allApis": "All APIs",
"loading": "Loading...",
"loadingDesc": "Loading API documentation",
"errorLoginRequired": "Login required to view API docs. Please log in on the frontend first, then refresh this page.",
"errorLoadSpec": "Failed to load API spec: ",
"errorLoadFailed": "Failed to load API docs: ",
"errorSpecInvalid": "Invalid API spec format",
"loadFailed": "Load failed",
"backToLogin": "Back to login",
"noApis": "No APIs",
"noEndpointsInGroup": "No API endpoints in this group",
"sectionDescription": "Description",
"viewDetailDesc": "View details",
"hideDetailDesc": "Hide details",
"noDescription": "No description",
"sectionParams": "Parameters",
"paramName": "Parameter",
"type": "Type",
"description": "Description",
"required": "Required",
"optional": "Optional",
"sectionRequestBody": "Request body",
"example": "Example",
"exampleJson": "Example JSON:",
"sectionResponse": "Response",
"testSection": "Test",
"requestBodyJson": "Request body (JSON)",
"queryParams": "Query parameters:",
"sendRequest": "Send request",
"copyCurl": "Copy cURL",
"clearResult": "Clear result",
"copyCurlTitle": "Copy cURL command",
"clearResultTitle": "Clear test result",
"sendingRequest": "Sending request...",
"errorPathParamRequired": "Path parameter {{name}} is required",
"errorQueryParamRequired": "Query parameter {{name}} is required",
"errorTokenRequired": "No Token detected. Please log in on the frontend and refresh, or add Authorization: Bearer your_token in the request header",
"errorJsonInvalid": "Invalid request body JSON: ",
"requestFailed": "Request failed: ",
"copied": "Copied",
"curlCopied": "curl command copied to clipboard!",
"copyFailedManual": "Copy failed, please copy manually:\n\n",
"curlGenFailed": "Failed to generate cURL command: ",
"requestBodyPlaceholder": "Enter request body in JSON format",
"tags": {
"auth": "Authentication",
"conversationManagement": "Conversation Management",
"conversationInteraction": "Conversation Interaction",
"batchTasks": "Batch Tasks",
"conversationGroups": "Conversation Groups",
"vulnerabilityManagement": "Vulnerability Management",
"roleManagement": "Role Management",
"skillsManagement": "Skills Management",
"monitoring": "Monitoring",
"configManagement": "Configuration Management",
"externalMCPManagement": "External MCP Management",
"attackChain": "Attack Chain",
"knowledgeBase": "Knowledge Base",
"mcp": "MCP"
},
"summary": {
"login": "User login",
"logout": "User logout",
"changePassword": "Change password",
"validateToken": "Validate Token",
"createConversation": "Create conversation",
"listConversations": "List conversations",
"getConversationDetail": "Get conversation detail",
"updateConversation": "Update conversation",
"deleteConversation": "Delete conversation",
"getConversationResult": "Get conversation result",
"sendMessageNonStream": "Send message and get AI reply (non-stream)",
"sendMessageStream": "Send message and get AI reply (stream)",
"cancelTask": "Cancel task",
"listRunningTasks": "List running tasks",
"listCompletedTasks": "List completed tasks",
"createBatchQueue": "Create batch task queue",
"listBatchQueues": "List batch task queues",
"getBatchQueue": "Get batch task queue",
"deleteBatchQueue": "Delete batch task queue",
"startBatchQueue": "Start batch task queue",
"pauseBatchQueue": "Pause batch task queue",
"addTaskToQueue": "Add task to queue",
"sqlInjectionScan": "SQL injection scan",
"portScan": "Port scan",
"updateBatchTask": "Update batch task",
"deleteBatchTask": "Delete batch task",
"createGroup": "Create group",
"listGroups": "List groups",
"getGroup": "Get group",
"updateGroup": "Update group",
"deleteGroup": "Delete group",
"getGroupConversations": "Get conversations in group",
"addConversationToGroup": "Add conversation to group",
"removeConversationFromGroup": "Remove conversation from group",
"listVulnerabilities": "List vulnerabilities",
"createVulnerability": "Create vulnerability",
"getVulnerabilityStats": "Get vulnerability statistics",
"getVulnerability": "Get vulnerability",
"updateVulnerability": "Update vulnerability",
"deleteVulnerability": "Delete vulnerability",
"listRoles": "List roles",
"createRole": "Create role",
"getRole": "Get role",
"updateRole": "Update role",
"deleteRole": "Delete role",
"getAvailableSkills": "Get available Skills list",
"listSkills": "List Skills",
"createSkill": "Create Skill",
"getSkillStats": "Get Skill statistics",
"clearSkillStats": "Clear Skill statistics",
"getSkill": "Get Skill",
"updateSkill": "Update Skill",
"deleteSkill": "Delete Skill",
"getBoundRoles": "Get bound roles",
"clearSkillStatsAlt": "Clear Skill statistics",
"getMonitorInfo": "Get monitoring info",
"getExecutionRecords": "Get execution records",
"deleteExecutionRecord": "Delete execution record",
"batchDeleteExecutionRecords": "Batch delete execution records",
"getStats": "Get statistics",
"getConfig": "Get configuration",
"updateConfig": "Update configuration",
"getToolConfig": "Get tool configuration",
"applyConfig": "Apply configuration",
"listExternalMCP": "List external MCP",
"getExternalMCPStats": "Get external MCP statistics",
"getExternalMCP": "Get external MCP",
"addOrUpdateExternalMCP": "Add or update external MCP",
"stdioModeConfig": "stdio mode config",
"sseModeConfig": "SSE mode config",
"deleteExternalMCP": "Delete external MCP",
"startExternalMCP": "Start external MCP",
"stopExternalMCP": "Stop external MCP",
"getAttackChain": "Get attack chain",
"regenerateAttackChain": "Regenerate attack chain",
"pinConversation": "Pin conversation",
"pinGroup": "Pin group",
"pinGroupConversation": "Pin conversation in group",
"getCategories": "Get categories",
"listKnowledgeItems": "List knowledge items",
"createKnowledgeItem": "Create knowledge item",
"getKnowledgeItem": "Get knowledge item",
"updateKnowledgeItem": "Update knowledge item",
"deleteKnowledgeItem": "Delete knowledge item",
"getIndexStatus": "Get index status",
"rebuildIndex": "Rebuild index",
"scanKnowledgeBase": "Scan knowledge base",
"searchKnowledgeBase": "Search knowledge base",
"basicSearch": "Basic search",
"searchByRiskType": "Search by risk type",
"getRetrievalLogs": "Get retrieval logs",
"deleteRetrievalLog": "Delete retrieval log",
"mcpEndpoint": "MCP endpoint",
"listAllTools": "List all tools",
"invokeTool": "Invoke tool",
"initConnection": "Initialize connection",
"successResponse": "Success response",
"errorResponse": "Error response"
},
"response": {
"getSuccess": "Success",
"unauthorized": "Unauthorized",
"unauthorizedToken": "Unauthorized, valid Token required",
"createSuccess": "Created successfully",
"badRequest": "Bad request",
"conversationNotFound": "Conversation not found",
"conversationOrResultNotFound": "Conversation or result not found",
"badRequestTaskEmpty": "Bad request (e.g. task is empty)",
"badRequestGroupNameExists": "Bad request or group name already exists",
"groupNotFound": "Group not found",
"badRequestConfig": "Bad request (e.g. invalid config or missing required fields)",
"badRequestQueryEmpty": "Bad request (e.g. query is empty)",
"methodNotAllowed": "Method not allowed (POST only)",
"loginSuccess": "Login successful",
"invalidPassword": "Invalid password",
"logoutSuccess": "Logout successful",
"passwordChanged": "Password changed successfully",
"tokenValid": "Token valid",
"tokenInvalid": "Token invalid or expired",
"conversationCreated": "Conversation created",
"internalError": "Internal server error",
"updateSuccess": "Updated successfully",
"deleteSuccess": "Deleted successfully",
"queueNotFound": "Queue not found",
"startSuccess": "Started successfully",
"pauseSuccess": "Paused successfully",
"addSuccess": "Added successfully",
"taskNotFound": "Task not found",
"conversationOrGroupNotFound": "Conversation or group not found",
"cancelSubmitted": "Cancel request submitted",
"noRunningTask": "No running task found",
"messageSent": "Message sent, AI reply returned",
"streamResponse": "Stream response (Server-Sent Events)"
}
},
"chatGroup": {
"search": "Search",
@@ -580,6 +906,15 @@
"presetLogin": "Login page + China",
"presetDomain": "By domain",
"presetIp": "By IP",
"queryPresetsAria": "FOFA query presets",
"fieldsPresetsAria": "FOFA field presets",
"resultsToolbarAria": "Results toolbar",
"fillExample": "Fill example",
"parseBtnTitle": "Parse natural language to FOFA query",
"minFieldsTitle": "For quick export",
"webCommonTitle": "For browsing and filtering",
"intelEnhancedTitle": "More fingerprint/intel",
"fullLabel": "full",
"nlPlaceholder": "e.g. Apache sites in Missouri, US, title contains Home",
"showHideColumns": "Show/hide columns",
"exportCsvTitle": "Export results as CSV (UTF-8)",
@@ -617,7 +952,14 @@
"clearStatsTitle": "Clear all statistics",
"skillsCallStats": "Skills call stats",
"searchPlaceholder": "Search Skills...",
"loading": "Loading..."
"loading": "Loading...",
"paginationShow": "Show {{start}}-{{end}} of {{total}}",
"perPageLabel": "Per page",
"firstPage": "First",
"prevPage": "Previous",
"pageOf": "Page {{current}} / {{total}}",
"nextPage": "Next",
"lastPage": "Last"
},
"settingsBasic": {
"basicTitle": "Basic settings",
@@ -686,8 +1028,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",
@@ -702,14 +1051,33 @@
"changePasswordBtn": "Change password"
},
"settingsRobotsExtra": {
"botCommandsTitle": "Bot commands",
"botCommandsDesc": "You can send these commands in chat (Chinese and English supported):"
"botCommandsTitle": "Bot command instructions",
"botCommandsDesc": "You can send the following commands in chat (Chinese and English supported):",
"botCmdHelp": "Show this help",
"botCmdList": "List conversations",
"botCmdSwitch": "Switch to conversation",
"botCmdNew": "Start new conversation",
"botCmdClear": "Clear context",
"botCmdCurrent": "Show current conversation",
"botCmdStop": "Stop running task",
"botCmdRoles": "List roles",
"botCmdRole": "Switch role",
"botCmdDelete": "Delete conversation",
"botCmdVersion": "Show version",
"botCommandsFooter": "Otherwise, send any text for AI penetration testing / security analysis."
},
"mcpDetailModal": {
"title": "Tool call details",
"execInfo": "Execution info",
"tool": "Tool",
"status": "Status",
"statusPending": "Pending",
"statusRunning": "Running",
"statusCompleted": "Completed",
"statusFailed": "Failed",
"unknown": "Unknown",
"getDetailFailed": "Failed to get details",
"execSuccessNoContent": "Execution succeeded with no displayable content.",
"time": "Time",
"executionId": "Execution ID",
"requestParams": "Request params",
@@ -752,6 +1120,15 @@
"configJson": "Config JSON",
"formatLabel": "Format:",
"formatDesc": "JSON object; key = config name, value = config. Use Start/Stop buttons to control state.",
"configExample": "Configuration example:",
"stdioMode": "stdio mode:",
"httpMode": "HTTP mode:",
"sseMode": "SSE mode:",
"placeholder": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\"],\n \"description\": \"Description\",\n \"timeout\": 300\n }\n}",
"exampleStdio": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\", \"--server\", \"http://example.com\"],\n \"description\": \"Description\",\n \"timeout\": 300\n }\n}",
"exampleHttp": "{\n \"cyberstrike-ai-http\": {\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:8081/mcp\"\n }\n}",
"exampleSse": "{\n \"cyberstrike-ai-sse\": {\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:8081/mcp/sse\"\n }\n}",
"exampleDescription": "Example description",
"formatJson": "Format JSON",
"loadExample": "Load example"
},
@@ -765,7 +1142,7 @@
"descriptionPlaceholder": "Short description",
"contentLabel": "Content (Markdown)",
"contentPlaceholder": "Enter skill content in Markdown...",
"contentHint": "YAML front matter supported (optional)"
"contentHint": "YAML front matter supported (optional), e.g.:"
},
"knowledgeItemModal": {
"addKnowledge": "Add knowledge",
@@ -920,6 +1297,33 @@
"searchSkillsPlaceholder": "Search skill...",
"loadingSkills": "Loading skills...",
"relatedSkillsHint": "Selected skills are injected into system prompt before task execution.",
"enableRole": "Enable this role"
"enableRole": "Enable this role",
"selectAll": "Select All",
"deselectAll": "Deselect All",
"roleNameRequired": "Role name is required",
"roleNotFound": "Role not found",
"firstRoleNoToolsHint": "First role with no tools selected will use all tools by default.",
"currentPageSelected": "Current page: {{current}} / {{total}}",
"totalSelected": "Total selected: {{current}} / {{total}}",
"usingAllEnabledTools": "(Using all enabled tools)",
"currentPageSelectedTitle": "Selected on current page (enabled tools only)",
"totalSelectedTitle": "Total tools linked to this role",
"skillsSelectedCount": "Selected {{count}} / {{total}}",
"loadToolsFailed": "Failed to load tools",
"loadSkillsFailed": "Failed to load skills",
"cannotDeleteDefaultRole": "Cannot delete default role",
"noMatchingSkills": "No matching skills",
"noSkillsAvailable": "No skills available",
"usingAllTools": "Use all tools",
"andNMore": " and {{count}} more",
"toolsLabel": "Tools:",
"noTools": "No tools",
"paginationShow": "{{start}}-{{end}} of {{total}} tools",
"paginationSearch": " (search: \"{{keyword}}\")",
"firstPage": "First",
"prevPage": "Previous",
"pageOf": "Page {{page}} / {{total}}",
"nextPage": "Next",
"lastPage": "Last"
}
}
+419 -15
View File
@@ -1,4 +1,8 @@
{
"lang": {
"zhCN": "中文",
"enUS": "English"
},
"common": {
"ok": "确定",
"cancel": "取消",
@@ -14,7 +18,8 @@
"confirm": "确认",
"copy": "复制",
"copied": "已复制",
"copyFailed": "复制失败"
"copyFailed": "复制失败",
"view": "查看"
},
"header": {
"title": "CyberStrikeAI",
@@ -99,6 +104,7 @@
},
"chat": {
"newChat": "新对话",
"toggleConversationPanel": "折叠/展开对话列表",
"searchHistory": "搜索历史记录...",
"conversationGroups": "对话分组",
"addGroup": "新建分组",
@@ -121,6 +127,7 @@
"copyMessageTitle": "复制消息内容",
"emptyGroupConversations": "该分组暂无对话",
"noMatchingConversationsInGroup": "未找到匹配的对话",
"noHistoryConversations": "暂无历史对话",
"renameGroupPrompt": "请输入新名称:",
"deleteGroupConfirm": "确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。",
"deleteConversationConfirm": "确定要删除此对话吗?",
@@ -130,10 +137,54 @@
"executeFailed": "执行失败",
"callOpenAIFailed": "调用OpenAI失败",
"systemReadyMessage": "系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。",
"addNewGroup": "+ 新增分组"
"addNewGroup": "+ 新增分组",
"callNumber": "调用 #{{n}}",
"iterationRound": "第 {{n}} 轮迭代",
"aiThinking": "AI思考",
"toolCallsDetected": "检测到 {{count}} 个工具调用",
"callTool": "调用工具: {{name}} ({{index}}/{{total}})",
"toolExecComplete": "工具 {{name}} 执行完成",
"toolExecFailed": "工具 {{name}} 执行失败",
"knowledgeRetrieval": "知识检索",
"knowledgeRetrievalTag": "知识检索",
"error": "错误",
"taskCancelled": "任务已取消",
"unknownTool": "未知工具",
"noDescription": "暂无描述",
"noResponseData": "暂无响应数据",
"loading": "加载中...",
"loadFailed": "加载失败: {{message}}",
"noAttackChainData": "暂无攻击链数据",
"copyFailedManual": "复制失败,请手动选择内容复制",
"searching": "搜索中...",
"loadFailedRetry": "加载失败,请重试",
"dataFormatError": "数据格式错误",
"progressInProgress": "渗透测试进行中...",
"executionFailed": "执行失败",
"penetrationTestComplete": "渗透测试完成",
"yesterday": "昨天"
},
"progress": {
"callingAI": "正在调用AI模型...",
"callingTool": "正在调用工具: {{name}}",
"lastIterSummary": "最后一次迭代:正在生成总结和下一步计划...",
"summaryDone": "总结生成完成",
"generatingFinalReply": "正在生成最终回复...",
"maxIterSummary": "达到最大迭代次数,正在生成总结..."
},
"timeline": {
"params": "参数:",
"executionResult": "执行结果:",
"executionId": "执行ID:",
"noResult": "无结果",
"running": "执行中...",
"completed": "已完成",
"execFailed": "执行失败"
},
"tasks": {
"title": "任务管理",
"stopTask": "停止任务",
"collapseDetail": "收起详情",
"newTask": "新建任务",
"autoRefresh": "自动刷新",
"historyHint": "提示:有已完成的任务历史,请勾选\"显示历史记录\"查看",
@@ -153,6 +204,10 @@
"unknownTime": "未知时间",
"clearHistoryConfirm": "确定要清空所有任务历史记录吗?",
"cancelTaskFailed": "取消任务失败",
"cancelFailed": "取消失败",
"taskInfoNotSynced": "任务信息尚未同步,请稍后再试。",
"loadActiveTasksFailed": "获取活跃任务失败",
"cannotGetTaskStatus": "无法获取任务状态",
"copiedToast": "已复制!",
"cancelling": "取消中...",
"enterTaskPrompt": "请输入至少一个任务",
@@ -340,6 +395,7 @@
"configStdioNeedCommand": "配置错误: \"{{name}}\" stdio模式需要command字段",
"configHttpNeedUrl": "配置错误: \"{{name}}\" http模式需要url字段",
"configSseNeedUrl": "配置错误: \"{{name}}\" sse模式需要url字段",
"configEditMustContainName": "配置错误: 编辑模式下,JSON必须包含配置名称 \"{{name}}\"",
"saveSuccess": "保存成功",
"deleteSuccess": "删除成功",
"deleteExternalConfirm": "确定要删除外部MCP \"{{name}}\" 吗?",
@@ -349,7 +405,12 @@
"totalCount": "总数",
"enabledCount": "已启用",
"disabledCount": "已停用",
"connectedCount": "已连接"
"connectedCount": "已连接",
"toolsCountValue": "🔧 {{count}} 个工具",
"connectionErrorLabel": "连接错误:",
"secondsUnit": "秒",
"urlLabel": "URL",
"loadExternalMCPFailed": "加载失败"
},
"settings": {
"title": "系统设置",
@@ -364,15 +425,36 @@
"description": "配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。",
"wecom": {
"title": "企业微信",
"enabled": "启用企业微信机器人"
"enabled": "启用企业微信机器人",
"token": "Token",
"tokenPlaceholder": "Token",
"encodingAesKey": "EncodingAESKey",
"encodingAesKeyPlaceholder": "EncodingAESKey(明文模式可留空)",
"corpId": "CorpID",
"corpIdPlaceholder": "企业 ID",
"secret": "Secret",
"secretPlaceholder": "应用 Secret",
"agentId": "AgentID",
"agentIdPlaceholder": "应用 AgentId"
},
"dingtalk": {
"title": "钉钉",
"enabled": "启用钉钉机器人"
"enabled": "启用钉钉机器人",
"clientIdLabel": "Client ID (AppKey)",
"clientIdPlaceholder": "钉钉应用 AppKey",
"clientSecretLabel": "Client Secret",
"clientSecretPlaceholder": "钉钉应用 Secret",
"streamHint": "需开启机器人能力并配置流式接入"
},
"lark": {
"title": "飞书 (Lark)",
"enabled": "启用飞书机器人"
"enabled": "启用飞书机器人",
"appIdLabel": "App ID",
"appIdPlaceholder": "飞书应用 App ID",
"appSecretLabel": "App Secret",
"appSecretPlaceholder": "飞书应用 App Secret",
"verifyTokenLabel": "Verify Token(可选)",
"verifyTokenPlaceholder": "事件订阅 Verification Token"
}
},
"apply": {
@@ -414,7 +496,15 @@
"title": "角色管理",
"createRole": "创建角色",
"searchPlaceholder": "搜索角色...",
"deleteConfirm": "确定要删除角色..."
"deleteConfirm": "确定要删除角色...",
"loadFailed": "加载角色失败",
"noDescription": "暂无描述",
"defaultRoleDescription": "默认角色,不额外携带用户提示词,使用默认MCP",
"noMatchingRoles": "没有找到匹配的角色",
"noRoles": "暂无角色",
"enabled": "已启用",
"disabled": "已禁用",
"noDescriptionShort": "无描述"
},
"skills": {
"title": "Skills管理",
@@ -436,10 +526,246 @@
"loadStatsFailed": "加载skills监控数据失败",
"clearStatsConfirm": "确定要清空所有Skills统计数据吗?此操作不可恢复。",
"statsCleared": "已清空所有Skills统计数据",
"clearStatsFailed": "清空统计数据失败"
"clearStatsFailed": "清空统计数据失败",
"noDescription": "无描述",
"viewSkillTitle": "查看Skill: {{name}}",
"descriptionLabel": "描述:",
"pathLabel": "路径:",
"modTimeLabel": "修改时间:",
"contentLabel": "内容:",
"nameRequired": "skill名称不能为空",
"contentRequired": "skill内容不能为空",
"nameInvalid": "skill名称只能包含字母、数字、连字符和下划线",
"saveSuccess": "skill已更新",
"createdSuccess": "skill已创建",
"deleteConfirm": "确定要删除skill \"{{name}}\" 吗?此操作不可恢复。",
"deleteConfirmWithRoles": "确定要删除skill \"{{name}}\" 吗?\n\n⚠️ 该skill当前已被以下 {{count}} 个角色绑定:\n{{roles}}\n\n删除后,系统将自动从这些角色中移除该skill的绑定。\n\n此操作不可恢复,是否继续?",
"deleteSuccess": "skill已删除",
"deleteSuccessWithRoles": "skill已删除,已自动从 {{count}} 个角色中移除绑定:{{roles}}",
"loadFailedShort": "加载失败",
"totalSkillsCount": "总Skills数",
"totalCallsCount": "总调用次数",
"successfulCalls": "成功调用",
"failedCalls": "失败调用",
"successRate": "成功率",
"skillName": "Skill名称",
"totalCalls": "总调用",
"success": "成功",
"failure": "失败",
"lastCallTime": "最后调用时间",
"noCallRecords": "暂无Skills调用记录",
"loadStatsErrorShort": "无法加载统计信息",
"loadCallStatsError": "无法加载调用统计"
},
"apiDocs": {
"curlCopied": "curl命令已复制到剪贴板!"
"pageTitle": "API 文档 - CyberStrikeAI",
"title": "API 文档",
"subtitle": "CyberStrikeAI 平台 API 接口文档,支持在线测试",
"authTitle": "API 认证说明",
"authAllNeedToken": "所有 API 接口都需要 Token 认证。",
"authGetToken": "1. 获取 Token",
"authGetTokenDesc": "在前端页面登录后,Token 会自动保存。您也可以通过以下方式获取:",
"authUseToken": "2. 使用 Token",
"authUseTokenDesc": "在请求头中添加 Authorization 字段:",
"authTip": "💡 提示:本页面会自动使用您已登录的 Token,无需手动填写。",
"tokenDetected": "✓ 已检测到 Token - 您可以直接测试 API 接口",
"tokenNotDetected": "⚠ 未检测到 Token - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token",
"sidebarGroupTitle": "API 分组",
"allApis": "全部接口",
"loading": "加载中...",
"loadingDesc": "正在加载 API 文档",
"errorLoginRequired": "需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。",
"errorLoadSpec": "加载API规范失败: ",
"errorLoadFailed": "加载API文档失败: ",
"errorSpecInvalid": "API规范格式错误",
"loadFailed": "加载失败",
"backToLogin": "返回首页登录",
"noApis": "暂无API",
"noEndpointsInGroup": "该分组下没有API端点",
"sectionDescription": "描述",
"viewDetailDesc": "查看详细说明",
"hideDetailDesc": "隐藏详细说明",
"noDescription": "无描述",
"sectionParams": "参数",
"paramName": "参数名",
"type": "类型",
"description": "描述",
"required": "必需",
"optional": "可选",
"sectionRequestBody": "请求体",
"example": "示例",
"exampleJson": "示例JSON:",
"sectionResponse": "响应",
"testSection": "测试接口",
"requestBodyJson": "请求体 (JSON)",
"queryParams": "查询参数:",
"sendRequest": "发送请求",
"copyCurl": "复制curl",
"clearResult": "清除结果",
"copyCurlTitle": "复制curl命令",
"clearResultTitle": "清除测试结果",
"sendingRequest": "发送请求中...",
"errorPathParamRequired": "路径参数 {{name}} 不能为空",
"errorQueryParamRequired": "查询参数 {{name}} 不能为空",
"errorTokenRequired": "未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token",
"errorJsonInvalid": "请求体JSON格式错误: ",
"requestFailed": "请求失败: ",
"copied": "已复制",
"curlCopied": "curl命令已复制到剪贴板!",
"copyFailedManual": "复制失败,请手动复制:\n\n",
"curlGenFailed": "生成curl命令失败: ",
"requestBodyPlaceholder": "请输入JSON格式的请求体",
"tags": {
"auth": "认证",
"conversationManagement": "对话管理",
"conversationInteraction": "对话交互",
"batchTasks": "批量任务",
"conversationGroups": "对话分组",
"vulnerabilityManagement": "漏洞管理",
"roleManagement": "角色管理",
"skillsManagement": "Skills管理",
"monitoring": "监控",
"configManagement": "配置管理",
"externalMCPManagement": "外部MCP管理",
"attackChain": "攻击链",
"knowledgeBase": "知识库",
"mcp": "MCP"
},
"summary": {
"login": "用户登录",
"logout": "用户登出",
"changePassword": "修改密码",
"validateToken": "验证Token",
"createConversation": "创建对话",
"listConversations": "列出对话",
"getConversationDetail": "查看对话详情",
"updateConversation": "更新对话",
"deleteConversation": "删除对话",
"getConversationResult": "获取对话结果",
"sendMessageNonStream": "发送消息并获取AI回复(非流式)",
"sendMessageStream": "发送消息并获取AI回复(流式)",
"cancelTask": "取消任务",
"listRunningTasks": "列出运行中的任务",
"listCompletedTasks": "列出已完成的任务",
"createBatchQueue": "创建批量任务队列",
"listBatchQueues": "列出批量任务队列",
"getBatchQueue": "获取批量任务队列",
"deleteBatchQueue": "删除批量任务队列",
"startBatchQueue": "启动批量任务队列",
"pauseBatchQueue": "暂停批量任务队列",
"addTaskToQueue": "添加任务到队列",
"sqlInjectionScan": "SQL注入扫描",
"portScan": "端口扫描",
"updateBatchTask": "更新批量任务",
"deleteBatchTask": "删除批量任务",
"createGroup": "创建分组",
"listGroups": "列出分组",
"getGroup": "获取分组",
"updateGroup": "更新分组",
"deleteGroup": "删除分组",
"getGroupConversations": "获取分组中的对话",
"addConversationToGroup": "添加对话到分组",
"removeConversationFromGroup": "从分组移除对话",
"listVulnerabilities": "列出漏洞",
"createVulnerability": "创建漏洞",
"getVulnerabilityStats": "获取漏洞统计",
"getVulnerability": "获取漏洞",
"updateVulnerability": "更新漏洞",
"deleteVulnerability": "删除漏洞",
"listRoles": "列出角色",
"createRole": "创建角色",
"getRole": "获取角色",
"updateRole": "更新角色",
"deleteRole": "删除角色",
"getAvailableSkills": "获取可用Skills列表",
"listSkills": "列出Skills",
"createSkill": "创建Skill",
"getSkillStats": "获取Skill统计",
"clearSkillStats": "清空Skill统计",
"getSkill": "获取Skill",
"updateSkill": "更新Skill",
"deleteSkill": "删除Skill",
"getBoundRoles": "获取绑定角色",
"clearSkillStatsAlt": "清空Skill统计",
"getMonitorInfo": "获取监控信息",
"getExecutionRecords": "获取执行记录",
"deleteExecutionRecord": "删除执行记录",
"batchDeleteExecutionRecords": "批量删除执行记录",
"getStats": "获取统计信息",
"getConfig": "获取配置",
"updateConfig": "更新配置",
"getToolConfig": "获取工具配置",
"applyConfig": "应用配置",
"listExternalMCP": "列出外部MCP",
"getExternalMCPStats": "获取外部MCP统计",
"getExternalMCP": "获取外部MCP",
"addOrUpdateExternalMCP": "添加或更新外部MCP",
"stdioModeConfig": "stdio模式配置",
"sseModeConfig": "SSE模式配置",
"deleteExternalMCP": "删除外部MCP",
"startExternalMCP": "启动外部MCP",
"stopExternalMCP": "停止外部MCP",
"getAttackChain": "获取攻击链",
"regenerateAttackChain": "重新生成攻击链",
"pinConversation": "设置对话置顶",
"pinGroup": "设置分组置顶",
"pinGroupConversation": "设置分组中对话的置顶",
"getCategories": "获取分类",
"listKnowledgeItems": "列出知识项",
"createKnowledgeItem": "创建知识项",
"getKnowledgeItem": "获取知识项",
"updateKnowledgeItem": "更新知识项",
"deleteKnowledgeItem": "删除知识项",
"getIndexStatus": "获取索引状态",
"rebuildIndex": "重建索引",
"scanKnowledgeBase": "扫描知识库",
"searchKnowledgeBase": "搜索知识库",
"basicSearch": "基础搜索",
"searchByRiskType": "按风险类型搜索",
"getRetrievalLogs": "获取检索日志",
"deleteRetrievalLog": "删除检索日志",
"mcpEndpoint": "MCP端点",
"listAllTools": "列出所有工具",
"invokeTool": "调用工具",
"initConnection": "初始化连接",
"successResponse": "成功响应",
"errorResponse": "错误响应"
},
"response": {
"getSuccess": "获取成功",
"unauthorized": "未授权",
"unauthorizedToken": "未授权,需要有效的Token",
"createSuccess": "创建成功",
"badRequest": "请求参数错误",
"conversationNotFound": "对话不存在",
"conversationOrResultNotFound": "对话不存在或结果不存在",
"badRequestTaskEmpty": "请求参数错误(如task为空)",
"badRequestGroupNameExists": "请求参数错误或分组名称已存在",
"groupNotFound": "分组不存在",
"badRequestConfig": "请求参数错误(如配置格式不正确、缺少必需字段等)",
"badRequestQueryEmpty": "请求参数错误(如query为空)",
"methodNotAllowed": "方法不允许(仅支持POST请求)",
"loginSuccess": "登录成功",
"invalidPassword": "密码错误",
"logoutSuccess": "登出成功",
"passwordChanged": "密码修改成功",
"tokenValid": "Token有效",
"tokenInvalid": "Token无效或已过期",
"conversationCreated": "对话创建成功",
"internalError": "服务器内部错误",
"updateSuccess": "更新成功",
"deleteSuccess": "删除成功",
"queueNotFound": "队列不存在",
"startSuccess": "启动成功",
"pauseSuccess": "暂停成功",
"addSuccess": "添加成功",
"taskNotFound": "任务不存在",
"conversationOrGroupNotFound": "对话或分组不存在",
"cancelSubmitted": "取消请求已提交",
"noRunningTask": "未找到正在执行的任务",
"messageSent": "消息发送成功,返回AI回复",
"streamResponse": "流式响应(Server-Sent Events"
}
},
"chatGroup": {
"search": "搜索",
@@ -580,6 +906,15 @@
"presetLogin": "登录页 + 中国",
"presetDomain": "指定域名",
"presetIp": "指定 IP",
"queryPresetsAria": "FOFA 查询示例",
"fieldsPresetsAria": "FOFA 字段模板",
"resultsToolbarAria": "结果工具条",
"fillExample": "填入示例",
"parseBtnTitle": "将自然语言解析为 FOFA 查询语法",
"minFieldsTitle": "适合快速导出目标",
"webCommonTitle": "适合浏览和筛选",
"intelEnhancedTitle": "更偏指纹/情报",
"fullLabel": "full",
"nlPlaceholder": "例如:找美国 Missouri 的 Apache 站点,标题包含 Home",
"showHideColumns": "显示/隐藏字段",
"exportCsvTitle": "导出当前结果为 CSVUTF-8,兼容中文)",
@@ -617,7 +952,14 @@
"clearStatsTitle": "清空所有统计数据",
"skillsCallStats": "Skills调用统计",
"searchPlaceholder": "搜索Skills...",
"loading": "加载中..."
"loading": "加载中...",
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 条",
"perPageLabel": "每页显示",
"firstPage": "首页",
"prevPage": "上一页",
"pageOf": "第 {{current}} / {{total}} 页",
"nextPage": "下一页",
"lastPage": "尾页"
},
"settingsBasic": {
"basicTitle": "基本设置",
@@ -686,8 +1028,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": "修改密码",
@@ -703,13 +1052,32 @@
},
"settingsRobotsExtra": {
"botCommandsTitle": "机器人命令说明",
"botCommandsDesc": "在对话中可发送以下命令(支持中英文):"
"botCommandsDesc": "在对话中可发送以下命令(支持中英文):",
"botCmdHelp": "显示本帮助 | Show this help",
"botCmdList": "列出所有对话标题与 ID | List conversations",
"botCmdSwitch": "指定对话继续 | Switch to conversation",
"botCmdNew": "开启新对话 | Start new conversation",
"botCmdClear": "清空当前上下文 | Clear context",
"botCmdCurrent": "显示当前对话 ID 与标题 | Show current conversation",
"botCmdStop": "中断当前任务 | Stop running task",
"botCmdRoles": "列出所有可用角色 | List roles",
"botCmdRole": "切换当前角色 | Switch role",
"botCmdDelete": "删除指定对话 | Delete conversation",
"botCmdVersion": "显示当前版本号 | Show version",
"botCommandsFooter": "除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis."
},
"mcpDetailModal": {
"title": "工具调用详情",
"execInfo": "执行信息",
"tool": "工具",
"status": "状态",
"statusPending": "等待中",
"statusRunning": "执行中",
"statusCompleted": "已完成",
"statusFailed": "失败",
"unknown": "未知",
"getDetailFailed": "获取详情失败",
"execSuccessNoContent": "执行成功,未返回可展示的文本内容。",
"time": "时间",
"executionId": "执行 ID",
"requestParams": "请求参数",
@@ -752,6 +1120,15 @@
"configJson": "配置JSON",
"formatLabel": "配置格式:",
"formatDesc": "JSON对象,key为配置名称,value为配置内容。状态通过\"启动/停止\"按钮控制,无需在JSON中配置。",
"configExample": "配置示例:",
"stdioMode": "stdio模式:",
"httpMode": "HTTP模式:",
"sseMode": "SSE模式:",
"placeholder": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\"],\n \"description\": \"描述\",\n \"timeout\": 300\n }\n}",
"exampleStdio": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\", \"--server\", \"http://example.com\"],\n \"description\": \"描述\",\n \"timeout\": 300\n }\n}",
"exampleHttp": "{\n \"cyberstrike-ai-http\": {\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:8081/mcp\"\n }\n}",
"exampleSse": "{\n \"cyberstrike-ai-sse\": {\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:8081/mcp/sse\"\n }\n}",
"exampleDescription": "示例描述",
"formatJson": "格式化JSON",
"loadExample": "加载示例"
},
@@ -765,7 +1142,7 @@
"descriptionPlaceholder": "Skill的简短描述",
"contentLabel": "内容(Markdown格式)",
"contentPlaceholder": "输入skill内容,支持Markdown格式...",
"contentHint": "支持YAML front matter格式(可选)"
"contentHint": "支持YAML front matter格式(可选),例如:"
},
"knowledgeItemModal": {
"addKnowledge": "添加知识",
@@ -920,6 +1297,33 @@
"searchSkillsPlaceholder": "搜索skill...",
"loadingSkills": "正在加载skills列表...",
"relatedSkillsHint": "勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。",
"enableRole": "启用此角色"
"enableRole": "启用此角色",
"selectAll": "全选",
"deselectAll": "全不选",
"roleNameRequired": "角色名称不能为空",
"roleNotFound": "角色不存在",
"firstRoleNoToolsHint": "检测到这是首次添加角色且未选择工具,将默认使用全部工具",
"currentPageSelected": "当前页已选中: {{current}} / {{total}}",
"totalSelected": "总计已选中: {{current}} / {{total}}",
"usingAllEnabledTools": "(使用所有已启用工具)",
"currentPageSelectedTitle": "当前页选中的工具数(只统计已启用的工具)",
"totalSelectedTitle": "角色已关联的工具总数(基于角色实际配置)",
"skillsSelectedCount": "已选择 {{count}} / {{total}}",
"loadToolsFailed": "加载工具列表失败",
"loadSkillsFailed": "加载skills列表失败",
"cannotDeleteDefaultRole": "不能删除默认角色",
"noMatchingSkills": "没有找到匹配的skills",
"noSkillsAvailable": "暂无可用skills",
"usingAllTools": "使用所有工具",
"andNMore": " 等 {{count}} 个",
"toolsLabel": "工具:",
"noTools": "暂无工具",
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 个工具",
"paginationSearch": " (搜索: \"{{keyword}}\")",
"firstPage": "首页",
"prevPage": "上一页",
"pageOf": "第 {{page}} / {{total}} 页",
"nextPage": "下一页",
"lastPage": "末页"
}
}
+146 -61
View File
@@ -3,13 +3,74 @@
let apiSpec = null;
let currentToken = null;
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
function waitForI18n() {
return new Promise(function (resolve) {
if (window.t) return resolve();
var n = 0;
var iv = setInterval(function () {
if (window.t) { clearInterval(iv); resolve(); return; }
n++;
if (n >= 100) { clearInterval(iv); resolve(); }
}, 50);
});
}
// 从 OpenAPI spec 的 x-i18n-tags 构建 tag -> i18n key 映射(方案 A:后端提供键)
var apiSpecTagToKey = {};
function buildApiSpecTagToKey() {
apiSpecTagToKey = {};
if (!apiSpec || !apiSpec.paths) return;
Object.keys(apiSpec.paths).forEach(function (path) {
var pathItem = apiSpec.paths[path];
if (!pathItem || typeof pathItem !== 'object') return;
['get', 'post', 'put', 'delete', 'patch'].forEach(function (method) {
var op = pathItem[method];
if (!op || !op.tags || !op['x-i18n-tags']) return;
var tags = op.tags;
var keys = op['x-i18n-tags'];
for (var i = 0; i < tags.length && i < keys.length; i++) {
apiSpecTagToKey[tags[i]] = typeof keys[i] === 'string' ? keys[i] : keys[i];
}
});
});
}
function translateApiDocTag(tag) {
if (!tag) return tag;
var key = apiSpecTagToKey[tag];
return key ? _t('apiDocs.tags.' + key) : tag;
}
function translateApiDocSummaryFromOp(op) {
var key = op && op['x-i18n-summary'];
if (key) return _t('apiDocs.summary.' + key);
return op && op.summary ? op.summary : '';
}
function translateApiDocResponseDescFromResp(resp) {
if (!resp) return '';
var key = resp['x-i18n-description'];
if (key) return _t('apiDocs.response.' + key);
return resp.description || '';
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
await waitForI18n();
await loadToken();
await loadAPISpec();
if (apiSpec) {
renderAPIDocs();
}
document.addEventListener('languagechange', function () {
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(document);
}
if (apiSpec) {
renderAPIDocs();
}
});
});
// 加载token
@@ -43,22 +104,25 @@ async function loadAPISpec() {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 401) {
showError('需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。');
showError(_t('apiDocs.errorLoginRequired'));
return;
}
throw new Error('加载API规范失败: ' + response.status);
throw new Error(_t('apiDocs.errorLoadSpec') + response.status);
}
apiSpec = await response.json();
buildApiSpecTagToKey();
} catch (error) {
console.error('加载API规范失败:', error);
showError('加载API文档失败: ' + error.message);
showError(_t('apiDocs.errorLoadFailed') + error.message);
}
}
// 显示错误
function showError(message) {
const main = document.getElementById('api-docs-main');
const loadFailed = _t('apiDocs.loadFailed');
const backToLogin = _t('apiDocs.backToLogin');
main.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -66,10 +130,10 @@ function showError(message) {
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<h3>加载失败</h3>
<p>${message}</p>
<h3>${escapeHtml(loadFailed)}</h3>
<p>${escapeHtml(message)}</p>
<div style="margin-top: 16px;">
<a href="/" style="color: var(--accent-color); text-decoration: none;">返回首页登录</a>
<a href="/" style="color: var(--accent-color); text-decoration: none;">${escapeHtml(backToLogin)}</a>
</div>
</div>
`;
@@ -78,7 +142,7 @@ function showError(message) {
// 渲染API文档
function renderAPIDocs() {
if (!apiSpec || !apiSpec.paths) {
showError('API规范格式错误');
showError(_t('apiDocs.errorSpecInvalid'));
return;
}
@@ -109,7 +173,7 @@ function renderAuthInfo() {
tokenStatus.style.display = 'block';
tokenStatus.style.background = 'rgba(255, 152, 0, 0.1)';
tokenStatus.style.borderLeftColor = '#ff9800';
tokenStatus.innerHTML = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;"><strong>⚠ 未检测到 Token</strong> - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token</p>';
tokenStatus.innerHTML = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;">' + escapeHtml(_t('apiDocs.tokenNotDetected')) + '</p>';
}
}
@@ -127,11 +191,14 @@ function renderSidebar() {
const groupList = document.getElementById('api-group-list');
const allGroups = Array.from(groups).sort();
while (groupList.children.length > 1) {
groupList.removeChild(groupList.lastChild);
}
allGroups.forEach(group => {
const li = document.createElement('li');
li.className = 'api-group-item';
li.innerHTML = `<a href="#" class="api-group-link" data-group="${group}">${group}</a>`;
const groupLabel = translateApiDocTag(group);
li.innerHTML = `<a href="#" class="api-group-link" data-group="${escapeHtml(group)}">${escapeHtml(groupLabel)}</a>`;
groupList.appendChild(li);
});
@@ -176,7 +243,7 @@ function renderEndpoints(filterGroup = null) {
});
if (endpoints.length === 0) {
main.innerHTML = '<div class="empty-state"><h3>暂无API</h3><p>该分组下没有API端点</p></div>';
main.innerHTML = '<div class="empty-state"><h3>' + escapeHtml(_t('apiDocs.noApis')) + '</h3><p>' + escapeHtml(_t('apiDocs.noEndpointsInGroup')) + '</p></div>';
return;
}
@@ -192,8 +259,8 @@ function createEndpointCard(endpoint) {
const methodClass = endpoint.method.toLowerCase();
const tags = endpoint.tags || [];
const tagHtml = tags.map(tag => `<span class="api-tag">${tag}</span>`).join('');
const tagHtml = tags.map(tag => `<span class="api-tag">${escapeHtml(translateApiDocTag(tag))}</span>`).join('');
const summaryText = translateApiDocSummaryFromOp(endpoint);
card.innerHTML = `
<div class="api-endpoint-header">
<div class="api-endpoint-title">
@@ -204,21 +271,21 @@ function createEndpointCard(endpoint) {
</div>
<div class="api-endpoint-body">
<div class="api-section">
<div class="api-section-title">描述</div>
${endpoint.summary ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(endpoint.summary)}</div>` : ''}
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionDescription'))}</div>
${summaryText ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(summaryText)}</div>` : ''}
${endpoint.description ? `
<div class="api-description-toggle">
<button class="description-toggle-btn" onclick="toggleDescription(this)">
<svg class="description-toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
<span>查看详细说明</span>
<span>${escapeHtml(_t('apiDocs.viewDetailDesc'))}</span>
</button>
<div class="api-description-detail" style="display: none;">
${formatDescription(endpoint.description)}
</div>
</div>
` : endpoint.summary ? '' : '<div class="api-description">无描述</div>'}
` : endpoint.summary ? '' : '<div class="api-description">' + escapeHtml(_t('apiDocs.noDescription')) + '</div>'}
</div>
${renderParameters(endpoint)}
@@ -236,8 +303,10 @@ function renderParameters(endpoint) {
const params = endpoint.parameters || [];
if (params.length === 0) return '';
const requiredLabel = escapeHtml(_t('apiDocs.required'));
const optionalLabel = escapeHtml(_t('apiDocs.optional'));
const rows = params.map(param => {
const required = param.required ? '<span class="api-param-required">必需</span>' : '<span class="api-param-optional">可选</span>';
const required = param.required ? '<span class="api-param-required">' + requiredLabel + '</span>' : '<span class="api-param-optional">' + optionalLabel + '</span>';
// 处理描述文本,将换行符转换为<br>
let descriptionHtml = '-';
if (param.description) {
@@ -255,17 +324,20 @@ function renderParameters(endpoint) {
`;
}).join('');
const paramName = escapeHtml(_t('apiDocs.paramName'));
const typeLabel = escapeHtml(_t('apiDocs.type'));
const descLabel = escapeHtml(_t('apiDocs.description'));
return `
<div class="api-section">
<div class="api-section-title">参数</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionParams'))}</div>
<div class="api-table-wrapper">
<table class="api-params-table">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>必需</th>
<th>${paramName}</th>
<th>${typeLabel}</th>
<th>${descLabel}</th>
<th>${requiredLabel}</th>
</tr>
</thead>
<tbody>
@@ -297,11 +369,13 @@ function renderRequestBody(endpoint) {
let paramsTable = '';
if (schema.properties) {
const requiredFields = schema.required || [];
const reqLabel = escapeHtml(_t('apiDocs.required'));
const optLabel = escapeHtml(_t('apiDocs.optional'));
const rows = Object.keys(schema.properties).map(key => {
const prop = schema.properties[key];
const required = requiredFields.includes(key)
? '<span class="api-param-required">必需</span>'
: '<span class="api-param-optional">可选</span>';
? '<span class="api-param-required">' + reqLabel + '</span>'
: '<span class="api-param-optional">' + optLabel + '</span>';
// 处理嵌套类型
let typeDisplay = prop.type || 'object';
@@ -338,16 +412,20 @@ function renderRequestBody(endpoint) {
}).join('');
if (rows) {
const pName = escapeHtml(_t('apiDocs.paramName'));
const tLabel = escapeHtml(_t('apiDocs.type'));
const dLabel = escapeHtml(_t('apiDocs.description'));
const exLabel = escapeHtml(_t('apiDocs.example'));
paramsTable = `
<div class="api-table-wrapper" style="margin-top: 12px;">
<table class="api-params-table">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>必需</th>
<th>示例</th>
<th>${pName}</th>
<th>${tLabel}</th>
<th>${dLabel}</th>
<th>${reqLabel}</th>
<th>${exLabel}</th>
</tr>
</thead>
<tbody>
@@ -389,12 +467,12 @@ function renderRequestBody(endpoint) {
return `
<div class="api-section">
<div class="api-section-title">请求体</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionRequestBody'))}</div>
${endpoint.requestBody.description ? `<div class="api-description">${endpoint.requestBody.description}</div>` : ''}
${paramsTable}
${example ? `
<div style="margin-top: 16px;">
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">示例JSON:</div>
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(_t('apiDocs.exampleJson'))}</div>
<div class="api-response-example">
<pre>${escapeHtml(example)}</pre>
</div>
@@ -414,11 +492,11 @@ function renderResponses(endpoint) {
if (schema.example) {
example = JSON.stringify(schema.example, null, 2);
}
const descText = translateApiDocResponseDescFromResp(response);
return `
<div style="margin-bottom: 16px;">
<strong style="color: ${status.startsWith('2') ? 'var(--success-color)' : status.startsWith('4') ? 'var(--error-color)' : 'var(--warning-color)'}">${status}</strong>
${response.description ? `<span style="color: var(--text-secondary); margin-left: 8px;">${response.description}</span>` : ''}
${descText ? `<span style="color: var(--text-secondary); margin-left: 8px;">${escapeHtml(descText)}</span>` : ''}
${example ? `
<div class="api-response-example" style="margin-top: 8px;">
<pre>${escapeHtml(example)}</pre>
@@ -432,7 +510,7 @@ function renderResponses(endpoint) {
return `
<div class="api-section">
<div class="api-section-title">响应</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionResponse'))}</div>
${responseItems}
</div>
`;
@@ -462,8 +540,8 @@ function renderTestSection(endpoint) {
const bodyInputId = `test-body-${escapeId(path)}-${method}`;
bodyInput = `
<div class="api-test-input-group">
<label>请求体 (JSON)</label>
<textarea id="${bodyInputId}" class="test-body-input" placeholder='请输入JSON格式的请求体'>${defaultBody}</textarea>
<label>${escapeHtml(_t('apiDocs.requestBodyJson'))}</label>
<textarea id="${bodyInputId}" class="test-body-input" placeholder='${escapeHtml(_t('apiDocs.requestBodyPlaceholder'))}'>${defaultBody}</textarea>
</div>
`;
}
@@ -491,7 +569,7 @@ function renderTestSection(endpoint) {
const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`;
const defaultValue = param.schema?.default !== undefined ? param.schema.default : '';
const placeholder = param.description || param.name;
const required = param.required ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">可选</span>';
const required = param.required ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">' + escapeHtml(_t('apiDocs.optional')) + '</span>';
return `
<div class="api-test-input-group">
<label>${param.name} ${required}</label>
@@ -505,33 +583,40 @@ function renderTestSection(endpoint) {
}).join('');
}
const testSectionTitle = escapeHtml(_t('apiDocs.testSection'));
const queryParamsTitle = escapeHtml(_t('apiDocs.queryParams'));
const sendRequestLabel = escapeHtml(_t('apiDocs.sendRequest'));
const copyCurlLabel = escapeHtml(_t('apiDocs.copyCurl'));
const clearResultLabel = escapeHtml(_t('apiDocs.clearResult'));
const copyCurlTitle = escapeHtml(_t('apiDocs.copyCurlTitle'));
const clearResultTitle = escapeHtml(_t('apiDocs.clearResultTitle'));
return `
<div class="api-test-section">
<div class="api-section-title">测试接口</div>
<div class="api-section-title">${testSectionTitle}</div>
<div class="api-test-form">
${pathParamsInput}
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">查询参数:</div>${queryParamsInput}</div>` : ''}
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${queryParamsTitle}</div>${queryParamsInput}</div>` : ''}
${bodyInput}
<div class="api-test-buttons">
<button class="api-test-btn primary" onclick="testAPI('${method}', '${escapeHtml(path)}', '${endpoint.operationId || ''}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
发送请求
${sendRequestLabel}
</button>
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="复制curl命令">
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="${copyCurlTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
</svg>
复制curl
${copyCurlLabel}
</button>
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="清除测试结果">
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="${clearResultTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
清除结果
${clearResultLabel}
</button>
</div>
<div id="test-result-${escapeId(path)}-${method}" class="api-test-result" style="display: none;"></div>
@@ -548,7 +633,7 @@ async function testAPI(method, path, operationId) {
resultDiv.style.display = 'block';
resultDiv.className = 'api-test-result loading';
resultDiv.textContent = '发送请求中...';
resultDiv.textContent = _t('apiDocs.sendingRequest');
try {
// 替换路径参数
@@ -561,7 +646,7 @@ async function testAPI(method, path, operationId) {
if (input && input.value) {
actualPath = actualPath.replace(param, encodeURIComponent(input.value));
} else {
throw new Error(`路径参数 ${paramName} 不能为空`);
throw new Error(_t('apiDocs.errorPathParamRequired', { name: paramName }));
}
});
@@ -580,7 +665,7 @@ async function testAPI(method, path, operationId) {
if (input && input.value !== '' && input.value !== null && input.value !== undefined) {
queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`);
} else if (param.required) {
throw new Error(`查询参数 ${param.name} 不能为空`);
throw new Error(_t('apiDocs.errorQueryParamRequired', { name: param.name }));
}
});
}
@@ -602,8 +687,7 @@ async function testAPI(method, path, operationId) {
if (currentToken) {
options.headers['Authorization'] = 'Bearer ' + currentToken;
} else {
// 如果没有token,提示用户
throw new Error('未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token');
throw new Error(_t('apiDocs.errorTokenRequired'));
}
// 添加请求体
@@ -614,7 +698,7 @@ async function testAPI(method, path, operationId) {
try {
options.body = JSON.stringify(JSON.parse(bodyInput.value.trim()));
} catch (e) {
throw new Error('请求体JSON格式错误: ' + e.message);
throw new Error(_t('apiDocs.errorJsonInvalid') + e.message);
}
}
}
@@ -636,7 +720,7 @@ async function testAPI(method, path, operationId) {
} catch (error) {
resultDiv.className = 'api-test-result error';
resultDiv.textContent = '请求失败: ' + error.message;
resultDiv.textContent = _t('apiDocs.requestFailed') + error.message;
}
}
@@ -727,17 +811,17 @@ function copyCurlCommand(event, method, path) {
// 复制到剪贴板
const button = event ? event.target.closest('button') : null;
navigator.clipboard.writeText(curlCommand).then(() => {
// 显示成功提示
if (button) {
const originalText = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
const copiedLabel = escapeHtml(_t('apiDocs.copied'));
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>' + copiedLabel;
button.style.color = 'var(--success-color)';
setTimeout(() => {
button.innerHTML = originalText;
button.style.color = '';
}, 2000);
} else {
alert('curl命令已复制到剪贴板!');
alert(_t('apiDocs.curlCopied'));
}
}).catch(err => {
console.error('复制失败:', err);
@@ -752,24 +836,25 @@ function copyCurlCommand(event, method, path) {
document.execCommand('copy');
if (button) {
const originalText = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
const copiedLabel = escapeHtml(_t('apiDocs.copied'));
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>' + copiedLabel;
button.style.color = 'var(--success-color)';
setTimeout(() => {
button.innerHTML = originalText;
button.style.color = '';
}, 2000);
} else {
alert('curl命令已复制到剪贴板!');
alert(_t('apiDocs.curlCopied'));
}
} catch (e) {
alert('复制失败,请手动复制:\n\n' + curlCommand);
alert(_t('apiDocs.copyFailedManual') + curlCommand);
}
document.body.removeChild(textarea);
});
} catch (error) {
console.error('生成curl命令失败:', error);
alert('生成curl命令失败: ' + error.message);
alert(_t('apiDocs.curlGenFailed') + error.message);
}
}
@@ -935,10 +1020,10 @@ function toggleDescription(button) {
if (detail.style.display === 'none') {
detail.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
span.textContent = '隐藏详细说明';
span.textContent = typeof window.t === 'function' ? window.t('apiDocs.hideDetailDesc') : '隐藏详细说明';
} else {
detail.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
span.textContent = '查看详细说明';
span.textContent = typeof window.t === 'function' ? window.t('apiDocs.viewDetailDesc') : '查看详细说明';
}
}
+15 -7
View File
@@ -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;
}
@@ -250,13 +258,13 @@ async function bootstrapApp() {
// 通用工具函数
function getStatusText(status) {
const statusMap = {
'pending': '等待中',
'running': '执行中',
'completed': '已完成',
'failed': '失败'
};
return statusMap[status] || status;
if (typeof window.t !== 'function') {
const fallback = { pending: '等待中', running: '执行中', completed: '已完成', failed: '失败' };
return fallback[status] || status;
}
const keyMap = { pending: 'mcpDetailModal.statusPending', running: 'mcpDetailModal.statusRunning', completed: 'mcpDetailModal.statusCompleted', failed: 'mcpDetailModal.statusFailed' };
const key = keyMap[status];
return key ? window.t(key) : status;
}
function formatDuration(ms) {
+160 -69
View File
@@ -755,7 +755,7 @@ function renderMentionSuggestions({ showLoading = false } = {}) {
const disabledClass = toolEnabled ? '' : 'disabled';
const badge = tool.isExternal ? '<span class="mention-item-badge">外部</span>' : '<span class="mention-item-badge internal">内置</span>';
const nameHtml = escapeHtml(tool.name);
const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : '暂无描述';
const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : (typeof window.t === 'function' ? window.t('chat.noDescription') : '暂无描述');
const descHtml = `<div class="mention-item-desc">${description}</div>`;
// 根据工具在当前角色中的启用状态显示状态标签
const statusLabel = toolEnabled ? '可用' : (tool.roleEnabled !== undefined ? '已禁用(当前角色)' : '已禁用');
@@ -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++;
@@ -1188,7 +1240,10 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
} else {
messageTime = new Date();
}
timeDiv.textContent = messageTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
const msgTimeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
const msgTimeOpts = { hour: '2-digit', minute: '2-digit' };
if (msgTimeLocale === 'zh-CN') msgTimeOpts.hour12 = false;
timeDiv.textContent = messageTime.toLocaleTimeString(msgTimeLocale, msgTimeOpts);
contentWrapper.appendChild(timeDiv);
// 如果有MCP执行ID或进度ID,添加查看详情区域(统一使用"渗透测试详情"样式)
@@ -1209,7 +1264,7 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
mcpExecutionIds.forEach((execId, index) => {
const detailBtn = document.createElement('button');
detailBtn.className = 'mcp-detail-btn';
detailBtn.innerHTML = `<span>调用 #${index + 1}</span>`;
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + '</span>';
detailBtn.onclick = () => showMCPDetail(execId);
buttonsContainer.appendChild(detailBtn);
// 异步获取工具名称并更新按钮文本
@@ -1233,6 +1288,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;
@@ -1265,7 +1324,7 @@ function copyMessageToClipboard(messageDiv, button) {
showCopySuccess(button);
}).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动选择内容复制');
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
});
}
return;
@@ -1276,11 +1335,11 @@ function copyMessageToClipboard(messageDiv, button) {
showCopySuccess(button);
}).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动选择内容复制');
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
});
} catch (error) {
console.error('复制消息时出错:', error);
alert('复制失败,请手动选择内容复制');
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
}
}
@@ -1408,32 +1467,33 @@ function renderProcessDetails(messageId, processDetails) {
// 根据事件类型渲染不同的内容
let itemTitle = title;
if (eventType === 'iteration') {
itemTitle = `${data.iteration || 1} 轮迭代`;
itemTitle = (typeof window.t === 'function' ? window.t('chat.iterationRound', { n: data.iteration || 1 }) : '第 ' + (data.iteration || 1) + ' 轮迭代');
} else if (eventType === 'thinking') {
itemTitle = '🤔 AI思考';
itemTitle = '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考');
} else if (eventType === 'tool_calls_detected') {
itemTitle = `🔧 检测到 ${data.count || 0} 个工具调用`;
itemTitle = '🔧 ' + (typeof window.t === 'function' ? window.t('chat.toolCallsDetected', { count: data.count || 0 }) : '检测到 ' + (data.count || 0) + ' 个工具调用');
} else if (eventType === 'tool_call') {
const toolName = data.toolName || '未知工具';
const toolName = data.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
const index = data.index || 0;
const total = data.total || 0;
itemTitle = `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`;
itemTitle = '🔧 ' + (typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')');
} else if (eventType === 'tool_result') {
const toolName = data.toolName || '未知工具';
const toolName = data.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
const success = data.success !== false;
const statusIcon = success ? '✅' : '❌';
itemTitle = `${statusIcon} 工具 ${escapeHtml(toolName)} 执行${success ? '完成' : '失败'}`;
// 如果是知识检索工具,添加特殊标记
const execText = success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行失败');
itemTitle = statusIcon + ' ' + execText;
if (toolName === BuiltinTools.SEARCH_KNOWLEDGE_BASE && success) {
itemTitle = `📚 ${itemTitle} - 知识检索`;
itemTitle = '📚 ' + itemTitle + ' - ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrievalTag') : '知识检索');
}
} else if (eventType === 'knowledge_retrieval') {
itemTitle = '📚 知识检索';
itemTitle = '📚 ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrieval') : '知识检索');
} else if (eventType === 'error') {
itemTitle = '❌ 错误';
itemTitle = '❌ ' + (typeof window.t === 'function' ? window.t('chat.error') : '错误');
} else if (eventType === 'cancelled') {
itemTitle = '⛔ 任务已取消';
itemTitle = '⛔ ' + (typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消');
} else if (eventType === 'progress') {
itemTitle = typeof window.translateProgressMessage === 'function' ? window.translateProgressMessage(detail.message || '') : (detail.message || '');
}
addTimelineItem(timeline, eventType, {
@@ -1509,7 +1569,7 @@ async function updateButtonWithToolName(button, executionId, index) {
const response = await apiFetch(`/api/monitor/execution/${executionId}`);
if (response.ok) {
const exec = await response.json();
const toolName = exec.toolName || '未知工具';
const toolName = exec.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
// 格式化工具名称(如果是 name::toolName 格式,只显示 toolName 部分)
const displayToolName = toolName.includes('::') ? toolName.split('::')[1] : toolName;
button.querySelector('span').textContent = `${displayToolName} #${index}`;
@@ -1528,14 +1588,15 @@ async function showMCPDetail(executionId) {
if (response.ok) {
// 填充模态框内容
document.getElementById('detail-tool-name').textContent = exec.toolName || 'Unknown';
document.getElementById('detail-tool-name').textContent = exec.toolName || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : 'Unknown');
document.getElementById('detail-execution-id').textContent = exec.id || 'N/A';
const statusEl = document.getElementById('detail-status');
const normalizedStatus = (exec.status || 'unknown').toLowerCase();
statusEl.textContent = getStatusText(exec.status);
statusEl.className = `status-chip status-${normalizedStatus}`;
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('zh-CN')
? new Date(exec.startTime).toLocaleString(detailTimeLocale)
: '—';
// 请求参数
@@ -1598,22 +1659,22 @@ async function showMCPDetail(executionId) {
successText = content.text;
}
if (!successText) {
successText = '执行成功,未返回可展示的文本内容。';
successText = typeof window.t === 'function' ? window.t('mcpDetailModal.execSuccessNoContent') : '执行成功,未返回可展示的文本内容。';
}
successElement.textContent = successText;
}
}
} else {
responseElement.textContent = '暂无响应数据';
responseElement.textContent = typeof window.t === 'function' ? window.t('chat.noResponseData') : '暂无响应数据';
}
// 显示模态框
document.getElementById('mcp-detail-modal').style.display = 'block';
} else {
alert('获取详情失败: ' + (exec.error || '未知错误'));
alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + (exec.error || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : '未知错误')));
}
} catch (error) {
alert('获取详情失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + error.message);
}
}
@@ -1709,7 +1770,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();
// 刷新分组列表,清除分组高亮
@@ -1749,12 +1810,13 @@ async function loadConversations(searchQuery = '') {
const sidebarContent = listContainer.closest('.sidebar-content');
const savedScrollTop = sidebarContent ? sidebarContent.scrollTop : 0;
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;" data-i18n="chat.noHistoryConversations"></div>';
listContainer.innerHTML = '';
// 如果响应不是200,显示空状态(友好处理,不显示错误)
if (!response.ok) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -1762,6 +1824,7 @@ async function loadConversations(searchQuery = '') {
if (!Array.isArray(conversations) || conversations.length === 0) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -1827,6 +1890,7 @@ async function loadConversations(searchQuery = '') {
if (!rendered) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -1845,8 +1909,9 @@ async function loadConversations(searchQuery = '') {
// 错误时显示空状态,而不是错误提示(更友好的用户体验)
const listContainer = document.getElementById('conversations-list');
if (listContainer) {
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;" data-i18n="chat.noHistoryConversations"></div>';
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
}
}
}
@@ -1947,34 +2012,27 @@ function formatConversationTimestamp(dateObj, todayStart, yesterdayStart) {
const referenceToday = todayStart || new Date(now.getFullYear(), now.getMonth(), now.getDate());
const referenceYesterday = yesterdayStart || new Date(referenceToday.getTime() - 24 * 60 * 60 * 1000);
const messageDate = new Date(dateObj.getFullYear(), dateObj.getMonth(), dateObj.getDate());
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('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
return dateObj.toLocaleTimeString(fmtLocale, timeOnlyOpts);
}
if (messageDate.getTime() === referenceYesterday.getTime()) {
return '昨天 ' + dateObj.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
});
return yesterdayLabel + ' ' + dateObj.toLocaleTimeString(fmtLocale, timeOnlyOpts);
}
if (dateObj.getFullYear() === referenceToday.getFullYear()) {
return dateObj.toLocaleString('zh-CN', {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
return dateObj.toLocaleString(fmtLocale, dateTimeOpts);
}
return dateObj.toLocaleString('zh-CN', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
return dateObj.toLocaleString(fmtLocale, fullDateOpts);
}
function getConversationGroup(dateObj, todayStart, startOfWeek, yesterdayStart) {
@@ -2118,7 +2176,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 });
}
// 滚动到底部
@@ -2159,7 +2217,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);
}
@@ -2247,11 +2305,13 @@ async function showAttackChain(conversationId) {
}
modal.style.display = 'block';
// 打开时立即按当前语言刷新统计(避免红框内仍显示硬编码中文)
updateAttackChainStats({ nodes: [], edges: [] });
// 清空容器
const container = document.getElementById('attack-chain-container');
if (container) {
container.innerHTML = '<div class="loading-spinner">加载中...</div>';
container.innerHTML = '<div class="loading-spinner">' + (typeof window.t === 'function' ? window.t('chat.loading') : '加载中...') + '</div>';
}
// 隐藏详情面板
@@ -2351,7 +2411,7 @@ async function loadAttackChain(conversationId) {
console.error('加载攻击链失败:', error);
const container = document.getElementById('attack-chain-container');
if (container) {
container.innerHTML = `<div class="error-message">加载失败: ${error.message}</div>`;
container.innerHTML = '<div class="error-message">' + (typeof window.t === 'function' ? window.t('chat.loadFailed', { message: error.message }) : '加载失败: ' + error.message) + '</div>';
}
// 错误时也重置加载状态
setAttackChainLoading(conversationId, false);
@@ -2377,7 +2437,7 @@ function renderAttackChain(chainData) {
container.innerHTML = '';
if (!chainData.nodes || chainData.nodes.length === 0) {
container.innerHTML = '<div class="empty-message">暂无攻击链数据</div>';
container.innerHTML = '<div class="empty-message">' + (typeof window.t === 'function' ? window.t('chat.noAttackChainData') : '暂无攻击链数据') + '</div>';
return;
}
@@ -3322,16 +3382,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');
@@ -3994,12 +4073,13 @@ async function loadConversationsWithGroups(searchQuery = '') {
const sidebarContent = listContainer.closest('.sidebar-content');
const savedScrollTop = sidebarContent ? sidebarContent.scrollTop : 0;
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;" data-i18n="chat.noHistoryConversations"></div>';
listContainer.innerHTML = '';
// 如果响应不是200,显示空状态(友好处理,不显示错误)
if (!response.ok) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -4007,6 +4087,7 @@ async function loadConversationsWithGroups(searchQuery = '') {
if (!Array.isArray(conversations) || conversations.length === 0) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -4068,6 +4149,7 @@ async function loadConversationsWithGroups(searchQuery = '') {
if (fragment.children.length === 0) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -4086,8 +4168,9 @@ async function loadConversationsWithGroups(searchQuery = '') {
// 错误时显示空状态,而不是错误提示(更友好的用户体验)
const listContainer = document.getElementById('conversations-list');
if (listContainer) {
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;">暂无历史对话</div>';
const emptyStateHtml = '<div style="padding: 20px; text-align: center; color: var(--text-muted); font-size: 0.875rem;" data-i18n="chat.noHistoryConversations"></div>';
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
}
}
}
@@ -5190,12 +5273,19 @@ function closeBatchManageModal() {
allConversationsForBatch = [];
}
// 语言切换时刷新批量管理模态框标题(若当前正在显示)
// 语言切换时刷新批量管理模态框标题(若当前正在显示);并刷新对话列表时间格式与系统就绪提示
document.addEventListener('languagechange', function () {
refreshSystemReadyMessageBubbles();
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();
}
});
// 显示创建分组模态框
@@ -5536,9 +5626,9 @@ async function loadGroupConversations(groupId, searchQuery = '') {
// 显示加载状态
if (searchQuery) {
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">搜索中...</div>';
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.searching') : '搜索中...') + '</div>';
} else {
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载中...</div>';
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.loading') : '加载中...') + '</div>';
}
// 构建URL,如果有搜索关键词则添加search参数
@@ -5550,7 +5640,7 @@ async function loadGroupConversations(groupId, searchQuery = '') {
const response = await apiFetch(url);
if (!response.ok) {
console.error(`Failed to load conversations for group ${groupId}:`, response.statusText);
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载失败,请重试</div>';
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.loadFailedRetry') : '加载失败,请重试') + '</div>';
return;
}
@@ -5564,7 +5654,7 @@ async function loadGroupConversations(groupId, searchQuery = '') {
// 验证返回的数据类型
if (!Array.isArray(groupConvs)) {
console.error(`Invalid response for group ${groupId}:`, groupConvs);
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">数据格式错误</div>';
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.dataFormatError') : '数据格式错误') + '</div>';
return;
}
@@ -5656,7 +5746,8 @@ async function loadGroupConversations(groupId, searchQuery = '') {
const timeWrapper = document.createElement('div');
timeWrapper.className = 'group-conversation-time';
const dateObj = fullConv.updatedAt ? new Date(fullConv.updatedAt) : new Date();
timeWrapper.textContent = dateObj.toLocaleString('zh-CN', {
const convListLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
timeWrapper.textContent = dateObj.toLocaleString(convListLocale, {
year: 'numeric',
month: 'long',
day: 'numeric',
+21 -6
View File
@@ -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;
+36 -8
View File
@@ -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);
@@ -61,18 +67,23 @@
const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
const attrList = el.getAttribute('data-i18n-attr');
const text = i18next.t(key);
// 仅当未使用 data-i18n-attr 时才替换元素文本内容(否则会覆盖卡片内的数字、子节点等)
// input/textarea:永不设置 textContent(会变成 value),只更新属性
if (!attrList && !skipText && !isFormControl && text && typeof text === 'string') {
// 仅当元素无子元素(仅文本或空)时才替换文本,避免覆盖卡片内的数字、子节点等;input/textarea 永不设置 textContent
const hasNoElementChildren = !el.querySelector('*');
if (!skipText && !isFormControl && hasNoElementChildren && text && typeof text === 'string') {
el.textContent = text;
}
if (attrList) {
const titleKey = el.getAttribute('data-i18n-title');
attrList.split(',').map(function (s) { return s.trim(); }).forEach(function (attr) {
if (!attr) return;
if (text && typeof text === 'string') {
el.setAttribute(attr, text);
var val = text;
if (attr === 'title' && titleKey) {
var titleText = i18next.t(titleKey);
if (titleText && typeof titleText === 'string') val = titleText;
}
if (val && typeof val === 'string') {
el.setAttribute(attr, val);
}
});
}
@@ -104,9 +115,9 @@
if (!label || typeof i18next === 'undefined') return;
const lang = (i18next.language || DEFAULT_LANG).toLowerCase();
if (lang.indexOf('zh') === 0) {
label.textContent = '中文';
label.textContent = i18next.t('lang.zhCN');
} else {
label.textContent = 'English';
label.textContent = i18next.t('lang.enUS');
}
}
@@ -143,6 +154,9 @@
}
applyTranslations(document);
updateLangLabel();
try {
window.__locale = lang;
} catch (e) { /* ignore */ }
try {
document.dispatchEvent(new CustomEvent('languagechange', { detail: { lang: lang } }));
} catch (e) { /* ignore */ }
@@ -151,6 +165,7 @@
async function initI18n() {
if (typeof i18next === 'undefined') {
console.warn('i18next 未加载,跳过前端国际化初始化');
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
return;
}
@@ -165,6 +180,9 @@
await loadLanguageResources(initialLang);
applyTranslations(document);
updateLangLabel();
try {
window.__locale = i18next.language || initialLang;
} catch (e) { /* ignore */ }
// 导出全局函数供其他脚本调用(支持插值参数,如 _t('key', { count: 2 })
window.t = function (key, opts) {
@@ -190,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();
});
});
})();
+251 -94
View File
@@ -3,6 +3,63 @@ let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
// 当前界面语言对应的 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',
// 英文(与 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'
};
if (map[trim]) return window.t(map[trim]);
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;
}
if (typeof window !== 'undefined') {
window.translateProgressMessage = translateProgressMessage;
}
// 存储工具调用ID到DOM元素的映射,用于更新执行状态
const toolCallStatusMap = new Map();
@@ -57,11 +114,15 @@ function markProgressCancelling(progressId) {
}
}
function finalizeProgressTask(progressId, finalLabel = '已完成') {
function finalizeProgressTask(progressId, finalLabel) {
const stopBtn = document.getElementById(`${progressId}-stop-btn`);
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = finalLabel;
if (finalLabel !== undefined && finalLabel !== '') {
stopBtn.textContent = finalLabel;
} else {
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成';
}
}
progressTaskState.delete(progressId);
}
@@ -76,7 +137,7 @@ async function requestCancel(conversationId) {
});
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || '取消失败');
throw new Error(result.error || (typeof window.t === 'function' ? window.t('tasks.cancelFailed') : '取消失败'));
}
return result;
}
@@ -94,12 +155,15 @@ function addProgressMessage() {
const bubble = document.createElement('div');
bubble.className = 'message-bubble progress-container';
const progressTitleText = typeof window.t === 'function' ? window.t('chat.progressInProgress') : '渗透测试进行中...';
const stopTaskText = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务';
const collapseDetailText = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
bubble.innerHTML = `
<div class="progress-header">
<span class="progress-title">🔍 渗透测试进行中...</span>
<span class="progress-title">🔍 ${progressTitleText}</span>
<div class="progress-actions">
<button class="progress-stop" id="${id}-stop-btn" onclick="cancelProgressTask('${id}')">停止任务</button>
<button class="progress-toggle" onclick="toggleProgressDetails('${id}')">收起详情</button>
<button class="progress-stop" id="${id}-stop-btn" onclick="cancelProgressTask('${id}')">${stopTaskText}</button>
<button class="progress-toggle" onclick="toggleProgressDetails('${id}')">${collapseDetailText}</button>
</div>
</div>
<div class="progress-timeline expanded" id="${id}-timeline"></div>
@@ -123,10 +187,10 @@ function toggleProgressDetails(progressId) {
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
toggleBtn.textContent = '展开详情';
toggleBtn.textContent = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
} else {
timeline.classList.add('expanded');
toggleBtn.textContent = '收起详情';
toggleBtn.textContent = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
}
}
@@ -143,7 +207,7 @@ function collapseAllProgressDetails(assistantMessageId, progressId) {
timeline.classList.remove('expanded');
const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`);
if (btn) {
btn.innerHTML = '<span>展开详情</span>';
btn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
}
}
}
@@ -158,7 +222,7 @@ function collapseAllProgressDetails(assistantMessageId, progressId) {
if (timeline) {
timeline.classList.remove('expanded');
if (toggleBtn) {
toggleBtn.textContent = '展开详情';
toggleBtn.textContent = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
}
}
});
@@ -170,7 +234,7 @@ function collapseAllProgressDetails(assistantMessageId, progressId) {
if (progressTimeline) {
progressTimeline.classList.remove('expanded');
if (progressToggleBtn) {
progressToggleBtn.textContent = '展开详情';
progressToggleBtn.textContent = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
}
}
}
@@ -246,7 +310,7 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) {
// 设置详情内容(如果有错误,默认折叠;否则默认折叠)
detailsContainer.innerHTML = `
<div class="process-details-content">
${hasContent ? `<div class="progress-timeline" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">暂无过程详情</div>'}
${hasContent ? `<div class="progress-timeline" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>'}
</div>
`;
@@ -258,10 +322,9 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) {
timeline.classList.remove('expanded');
}
// 更新按钮文本为"展开详情"(因为默认折叠)
const processDetailBtn = buttonsContainer.querySelector('.process-detail-btn');
if (processDetailBtn) {
processDetailBtn.innerHTML = '<span>展开详情</span>';
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
}
}
@@ -279,22 +342,23 @@ function toggleProcessDetails(progressId, assistantMessageId) {
const timeline = detailsContainer.querySelector('.progress-timeline');
const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`);
const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
if (content && timeline) {
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
if (btn) btn.innerHTML = '<span>展开详情</span>';
if (btn) btn.innerHTML = '<span>' + expandT + '</span>';
} else {
timeline.classList.add('expanded');
if (btn) btn.innerHTML = '<span>收起详情</span>';
if (btn) btn.innerHTML = '<span>' + collapseT + '</span>';
}
} else if (timeline) {
// 如果只有timeline,直接切换
if (timeline.classList.contains('expanded')) {
timeline.classList.remove('expanded');
if (btn) btn.innerHTML = '<span>展开详情</span>';
if (btn) btn.innerHTML = '<span>' + expandT + '</span>';
} else {
timeline.classList.add('expanded');
if (btn) btn.innerHTML = '<span>收起详情</span>';
if (btn) btn.innerHTML = '<span>' + collapseT + '</span>';
}
}
@@ -319,7 +383,7 @@ async function cancelProgressTask(progressId) {
stopBtn.disabled = false;
}, 1500);
}
alert('任务信息尚未同步,请稍后再试。');
alert(typeof window.t === 'function' ? window.t('tasks.taskInfoNotSynced') : '任务信息尚未同步,请稍后再试。');
return;
}
@@ -330,7 +394,7 @@ async function cancelProgressTask(progressId) {
markProgressCancelling(progressId);
if (stopBtn) {
stopBtn.disabled = true;
stopBtn.textContent = '取消中...';
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...';
}
try {
@@ -338,10 +402,10 @@ async function cancelProgressTask(progressId) {
loadActiveTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert('取消任务失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '取消任务失败') + ': ' + error.message);
if (stopBtn) {
stopBtn.disabled = false;
stopBtn.textContent = '停止任务';
stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务';
}
const currentState = progressTaskState.get(progressId);
if (currentState) {
@@ -391,15 +455,17 @@ function convertProgressToDetails(progressId, assistantMessageId) {
// 如果有错误,默认折叠;否则默认展开
const shouldExpand = !hasError;
const expandedClass = shouldExpand ? 'expanded' : '';
const toggleText = shouldExpand ? '收起详情' : '展开详情';
// 总是显示详情组件,即使没有内容也显示
const collapseDetailText = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情';
const expandDetailText = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情';
const toggleText = shouldExpand ? collapseDetailText : expandDetailText;
const penetrationDetailText = typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情';
const noProcessDetailText = typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)';
bubble.innerHTML = `
<div class="progress-header">
<span class="progress-title">📋 渗透测试详情</span>
<span class="progress-title">📋 ${penetrationDetailText}</span>
${hasContent ? `<button class="progress-toggle" onclick="toggleProgressDetails('${detailsId}')">${toggleText}</button>` : ''}
</div>
${hasContent ? `<div class="progress-timeline ${expandedClass}" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">暂无过程详情(可能执行过快或未触发详细事件)</div>'}
${hasContent ? `<div class="progress-timeline ${expandedClass}" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">' + noProcessDetailText + '</div>'}
`;
contentWrapper.appendChild(bubble);
@@ -464,43 +530,40 @@ function handleStreamEvent(event, progressElement, progressId,
}
break;
case 'iteration':
// 添加迭代标记
// 添加迭代标记data 属性供语言切换时重算标题)
addTimelineItem(timeline, 'iteration', {
title: `${event.data?.iteration || 1} 轮迭代`,
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;
case 'thinking':
// 显示AI思考内容
addTimelineItem(timeline, 'thinking', {
title: '🤔 AI思考',
title: '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考'),
message: event.message,
data: event.data
});
break;
case 'tool_calls_detected':
// 工具调用检测
addTimelineItem(timeline, 'tool_calls_detected', {
title: `🔧 检测到 ${event.data?.count || 0} 个工具调用`,
title: '🔧 ' + (typeof window.t === 'function' ? window.t('chat.toolCallsDetected', { count: event.data?.count || 0 }) : '检测到 ' + (event.data?.count || 0) + ' 个工具调用'),
message: event.message,
data: event.data
});
break;
case 'tool_call':
// 显示工具调用信息
const toolInfo = event.data || {};
const toolName = toolInfo.toolName || '未知工具';
const toolName = toolInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
const index = toolInfo.index || 0;
const total = toolInfo.total || 0;
const toolCallId = toolInfo.toolCallId || null;
// 添加工具调用项,并标记为执行中
const toolCallTitle = typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')';
const toolCallItemId = addTimelineItem(timeline, 'tool_call', {
title: `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`,
title: '🔧 ' + toolCallTitle,
message: event.message,
data: toolInfo,
expanded: false
@@ -519,22 +582,18 @@ function handleStreamEvent(event, progressElement, progressId,
break;
case 'tool_result':
// 显示工具执行结果
const resultInfo = event.data || {};
const resultToolName = resultInfo.toolName || '未知工具';
const resultToolName = resultInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具');
const success = resultInfo.success !== false;
const statusIcon = success ? '✅' : '❌';
const resultToolCallId = resultInfo.toolCallId || null;
// 如果有关联的toolCallId,更新工具调用项的状态
const resultExecText = success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行失败');
if (resultToolCallId && toolCallStatusMap.has(resultToolCallId)) {
updateToolCallStatus(resultToolCallId, success ? 'completed' : 'failed');
// 从映射中移除(已完成)
toolCallStatusMap.delete(resultToolCallId);
}
addTimelineItem(timeline, 'tool_result', {
title: `${statusIcon} 工具 ${escapeHtml(resultToolName)} 执行${success ? '完成' : '失败'}`,
title: statusIcon + ' ' + resultExecText,
message: event.message,
data: resultInfo,
expanded: false
@@ -542,36 +601,35 @@ function handleStreamEvent(event, progressElement, progressId,
break;
case 'progress':
// 更新进度状态
const progressTitle = document.querySelector(`#${progressId} .progress-title`);
if (progressTitle) {
progressTitle.textContent = '🔍 ' + event.message;
// 保存原文,语言切换时可用 translateProgressMessage 重新套当前语言
const progressEl = document.getElementById(progressId);
if (progressEl) {
progressEl.dataset.progressRawMessage = event.message || '';
}
const progressMsg = translateProgressMessage(event.message);
progressTitle.textContent = '🔍 ' + progressMsg;
}
break;
case 'cancelled':
// 显示错误
const taskCancelledText = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消';
addTimelineItem(timeline, 'cancelled', {
title: '⛔ 任务已取消',
title: '⛔ ' + taskCancelledText,
message: event.message,
data: event.data
});
// 更新进度标题为取消状态
const cancelTitle = document.querySelector(`#${progressId} .progress-title`);
if (cancelTitle) {
cancelTitle.textContent = '⛔ 任务已取消';
cancelTitle.textContent = '⛔ ' + taskCancelledText;
}
// 更新进度容器为已完成状态(添加completed类)
const cancelProgressContainer = document.querySelector(`#${progressId} .progress-container`);
if (cancelProgressContainer) {
cancelProgressContainer.classList.add('completed');
}
// 完成进度任务(标记为已取消)
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已取消');
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCancelled') : '已取消');
}
// 如果取消事件包含messageId,说明有助手消息,需要显示取消内容
@@ -670,7 +728,7 @@ function handleStreamEvent(event, progressElement, progressId,
case 'error':
// 显示错误
addTimelineItem(timeline, 'error', {
title: '❌ 错误',
title: '❌ ' + (typeof window.t === 'function' ? window.t('chat.error') : '错误'),
message: event.message,
data: event.data
});
@@ -678,7 +736,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 更新进度标题为错误状态
const errorTitle = document.querySelector(`#${progressId} .progress-title`);
if (errorTitle) {
errorTitle.textContent = '❌ 执行失败';
errorTitle.textContent = '❌ ' + (typeof window.t === 'function' ? window.t('chat.executionFailed') : '执行失败');
}
// 更新进度容器为已完成状态(添加completed类)
@@ -689,7 +747,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 完成进度任务(标记为失败)
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已失败');
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusFailed') : '执行失败');
}
// 如果错误事件包含messageId,说明有助手消息,需要显示错误内容
@@ -743,7 +801,7 @@ function handleStreamEvent(event, progressElement, progressId,
// 完成,更新进度标题(如果进度消息还存在)
const doneTitle = document.querySelector(`#${progressId} .progress-title`);
if (doneTitle) {
doneTitle.textContent = '✅ 渗透测试完成';
doneTitle.textContent = '✅ ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestComplete') : '渗透测试完成');
}
// 更新对话ID
if (event.data && event.data.conversationId) {
@@ -753,7 +811,7 @@ function handleStreamEvent(event, progressElement, progressId,
updateProgressConversation(progressId, event.data.conversationId);
}
if (progressTaskState.has(progressId)) {
finalizeProgressTask(progressId, '已完成');
finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成');
}
// 检查时间线中是否有错误项
@@ -807,17 +865,19 @@ function updateToolCallStatus(toolCallId, status) {
// 移除之前的状态类
item.classList.remove('tool-call-running', 'tool-call-completed', 'tool-call-failed');
// 根据状态更新样式和文本
const runningLabel = typeof window.t === 'function' ? window.t('timeline.running') : '执行中...';
const completedLabel = typeof window.t === 'function' ? window.t('timeline.completed') : '已完成';
const failedLabel = typeof window.t === 'function' ? window.t('timeline.execFailed') : '执行失败';
let statusText = '';
if (status === 'running') {
item.classList.add('tool-call-running');
statusText = ' <span class="tool-status-badge tool-status-running">执行中...</span>';
statusText = ' <span class="tool-status-badge tool-status-running">' + escapeHtml(runningLabel) + '</span>';
} else if (status === 'completed') {
item.classList.add('tool-call-completed');
statusText = ' <span class="tool-status-badge tool-status-completed">✅ 已完成</span>';
statusText = ' <span class="tool-status-badge tool-status-completed">✅ ' + escapeHtml(completedLabel) + '</span>';
} else if (status === 'failed') {
item.classList.add('tool-call-failed');
statusText = ' <span class="tool-status-badge tool-status-failed">❌ 执行失败</span>';
statusText = ' <span class="tool-status-badge tool-status-failed">❌ ' + escapeHtml(failedLabel) + '</span>';
}
// 更新标题(保留原有文本,追加状态)
@@ -834,6 +894,18 @@ 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' && options.iterationN != null) {
item.dataset.iterationN = String(options.iterationN);
}
if (type === 'tool_calls_detected' && options.data && options.data.count != null) {
item.dataset.toolCallsCount = String(options.data.count);
}
// 保存事件时间 ISO,语言切换时可重算时间格式
try {
item.dataset.createdAtIso = eventTime.toISOString();
} catch (e) { /* ignore */ }
// 使用传入的createdAt时间,如果没有则使用当前时间(向后兼容)
let eventTime;
@@ -854,7 +926,9 @@ function addTimelineItem(timeline, type, options) {
eventTime = new Date();
}
const time = eventTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const timeLocale = getCurrentTimeLocale();
const timeOpts = getTimeFormatOptions();
const time = eventTime.toLocaleTimeString(timeLocale, timeOpts);
let content = `
<div class="timeline-item-header">
@@ -869,11 +943,12 @@ function addTimelineItem(timeline, type, options) {
} else if (type === 'tool_call' && options.data) {
const data = options.data;
const args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : {});
const paramsLabel = typeof window.t === 'function' ? window.t('timeline.params') : '参数:';
content += `
<div class="timeline-item-content">
<div class="tool-details">
<div class="tool-arg-section">
<strong>参数:</strong>
<strong>${escapeHtml(paramsLabel)}</strong>
<pre class="tool-args">${escapeHtml(JSON.stringify(args, null, 2))}</pre>
</div>
</div>
@@ -882,22 +957,25 @@ function addTimelineItem(timeline, type, options) {
} else if (type === 'tool_result' && options.data) {
const data = options.data;
const isError = data.isError || !data.success;
const result = data.result || data.error || '无结果';
// 确保 result 是字符串
const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果';
const result = data.result || data.error || noResultText;
const resultStr = typeof result === 'string' ? result : JSON.stringify(result);
const execResultLabel = typeof window.t === 'function' ? window.t('timeline.executionResult') : '执行结果:';
const execIdLabel = typeof window.t === 'function' ? window.t('timeline.executionId') : '执行ID:';
content += `
<div class="timeline-item-content">
<div class="tool-result-section ${isError ? 'error' : 'success'}">
<strong>执行结果:</strong>
<strong>${escapeHtml(execResultLabel)}</strong>
<pre class="tool-result">${escapeHtml(resultStr)}</pre>
${data.executionId ? `<div class="tool-execution-id">执行ID: <code>${escapeHtml(data.executionId)}</code></div>` : ''}
${data.executionId ? `<div class="tool-execution-id">${escapeHtml(execIdLabel)} <code>${escapeHtml(data.executionId)}</code></div>` : ''}
</div>
</div>
`;
} else if (type === 'cancelled') {
const taskCancelledLabel = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消';
content += `
<div class="timeline-item-content">
${escapeHtml(options.message || '任务已取消')}
${escapeHtml(options.message || taskCancelledLabel)}
</div>
`;
}
@@ -923,7 +1001,7 @@ async function loadActiveTasks(showErrors = false) {
const result = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(result.error || '获取活跃任务失败');
throw new Error(result.error || (typeof window.t === 'function' ? window.t('tasks.loadActiveTasksFailed') : '获取活跃任务失败'));
}
renderActiveTasks(result.tasks || []);
@@ -931,7 +1009,8 @@ async function loadActiveTasks(showErrors = false) {
console.error('获取活跃任务失败:', error);
if (showErrors && bar) {
bar.style.display = 'block';
bar.innerHTML = `<div class="active-task-error">无法获取任务状态:${escapeHtml(error.message)}</div>`;
const cannotGetStatus = typeof window.t === 'function' ? window.t('tasks.cannotGetTaskStatus') : '无法获取任务状态:';
bar.innerHTML = `<div class="active-task-error">${escapeHtml(cannotGetStatus)}${escapeHtml(error.message)}</div>`;
}
}
}
@@ -960,30 +1039,34 @@ function renderActiveTasks(tasks) {
item.className = 'active-task-item';
const startedTime = task.startedAt ? new Date(task.startedAt) : null;
const taskTimeLocale = getCurrentTimeLocale();
const timeOpts = getTimeFormatOptions();
const timeText = startedTime && !isNaN(startedTime.getTime())
? startedTime.toLocaleTimeString('zh-CN', { 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; };
const statusMap = {
'running': '执行中',
'cancelling': '取消中',
'failed': '执行失败',
'timeout': '执行超时',
'cancelled': '已取消',
'completed': '已完成'
'running': _t('tasks.statusRunning'),
'cancelling': _t('tasks.statusCancelling'),
'failed': _t('tasks.statusFailed'),
'timeout': _t('tasks.statusTimeout'),
'cancelled': _t('tasks.statusCancelled'),
'completed': _t('tasks.statusCompleted')
};
const statusText = statusMap[task.status] || '执行中';
const statusText = statusMap[task.status] || _t('tasks.statusRunning');
const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed'].includes(task.status);
const unnamedTaskText = _t('tasks.unnamedTask');
const stopTaskBtnText = _t('tasks.stopTask');
item.innerHTML = `
<div class="active-task-info">
<span class="active-task-status">${statusText}</span>
<span class="active-task-message">${escapeHtml(task.message || '未命名任务')}</span>
<span class="active-task-message">${escapeHtml(task.message || unnamedTaskText)}</span>
</div>
<div class="active-task-actions">
${timeText ? `<span class="active-task-time">${timeText}</span>` : ''}
${!isFinalStatus ? '<button class="active-task-cancel">停止任务</button>' : ''}
${!isFinalStatus ? '<button class="active-task-cancel">' + stopTaskBtnText + '</button>' : ''}
</div>
`;
@@ -994,7 +1077,7 @@ function renderActiveTasks(tasks) {
cancelBtn.onclick = () => cancelActiveTask(task.conversationId, cancelBtn);
if (task.status === 'cancelling') {
cancelBtn.disabled = true;
cancelBtn.textContent = '取消中...';
cancelBtn.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...';
}
}
}
@@ -1007,14 +1090,14 @@ async function cancelActiveTask(conversationId, button) {
if (!conversationId) return;
const originalText = button.textContent;
button.disabled = true;
button.textContent = '取消中...';
button.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...';
try {
await requestCancel(conversationId);
loadActiveTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert('取消任务失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '取消任务失败') + ': ' + error.message);
button.disabled = false;
button.textContent = originalText;
}
@@ -1522,14 +1605,14 @@ function updateBatchActionsState() {
if (batchActions) {
batchActions.style.display = 'flex';
}
if (selectedCountSpan) {
selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : `已选择 ${selectedCount}`;
}
} else {
if (batchActions) {
batchActions.style.display = 'none';
}
}
if (selectedCountSpan) {
selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : '已选择 ' + selectedCount + ' 项';
}
// 更新全选复选框状态
const selectAllCheckbox = document.getElementById('monitor-select-all');
@@ -1655,3 +1738,77 @@ 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);
}
});
// 时间线项:按类型重算标题,并重绘时间戳
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 });
}
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();
});
+44 -36
View File
@@ -1,4 +1,7 @@
// 角色管理相关功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
let currentRole = localStorage.getItem('currentRole') || '';
let roles = [];
let rolesSearchKeyword = ''; // 角色搜索关键词
@@ -54,7 +57,7 @@ async function loadRoles() {
return roles;
} catch (error) {
console.error('加载角色失败:', error);
showNotification('加载角色失败: ' + error.message, 'error');
showNotification(_t('roles.loadFailed') + ': ' + error.message, 'error');
return [];
}
}
@@ -167,9 +170,9 @@ function renderRoleSelectionSidebar() {
const icon = getRoleIcon(role);
// 处理默认角色的描述
let description = role.description || '暂无描述';
let description = role.description || _t('roles.noDescription');
if (isDefaultRole && !role.description) {
description = '默认角色,不额外携带用户提示词,使用默认MCP';
description = _t('roles.defaultRoleDescription');
}
roleItem.innerHTML = `
@@ -282,7 +285,7 @@ function renderRolesList() {
if (filteredRoles.length === 0) {
rolesList.innerHTML = '<div class="empty-state">' +
(rolesSearchKeyword ? '没有找到匹配的角色' : '暂无角色') +
(rolesSearchKeyword ? _t('roles.noMatchingRoles') : _t('roles.noRoles')) +
'</div>';
return;
}
@@ -312,7 +315,7 @@ function renderRolesList() {
let toolsDisplay = '';
let toolsCount = 0;
if (role.name === '默认') {
toolsDisplay = '使用所有工具';
toolsDisplay = _t('roleModal.usingAllTools');
} else if (role.tools && role.tools.length > 0) {
toolsCount = role.tools.length;
// 显示前5个工具名称
@@ -324,13 +327,13 @@ function renderRolesList() {
if (toolsCount <= 5) {
toolsDisplay = toolNames.join(', ');
} else {
toolsDisplay = toolNames.join(', ') + `${toolsCount}`;
toolsDisplay = toolNames.join(', ') + _t('roleModal.andNMore', { count: toolsCount });
}
} else if (role.mcps && role.mcps.length > 0) {
toolsCount = role.mcps.length;
toolsDisplay = `${toolsCount}`;
toolsDisplay = _t('roleModal.andNMore', { count: toolsCount });
} else {
toolsDisplay = '使用所有工具';
toolsDisplay = _t('roleModal.usingAllTools');
}
return `
@@ -341,17 +344,17 @@ function renderRolesList() {
${escapeHtml(role.name)}
</h3>
<span class="role-card-badge ${role.enabled !== false ? 'enabled' : 'disabled'}">
${role.enabled !== false ? '已启用' : '已禁用'}
${role.enabled !== false ? _t('roles.enabled') : _t('roles.disabled')}
</span>
</div>
<div class="role-card-description">${escapeHtml(role.description || '无描述')}</div>
<div class="role-card-description">${escapeHtml(role.description || _t('roles.noDescriptionShort'))}</div>
<div class="role-card-tools">
<span class="role-card-tools-label">工具:</span>
<span class="role-card-tools-label">${_t('roleModal.toolsLabel')}</span>
<span class="role-card-tools-value">${toolsDisplay}</span>
</div>
<div class="role-card-actions">
<button class="btn-secondary btn-small" onclick="editRole('${escapeHtml(role.name)}')">编辑</button>
${role.name !== '默认' ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteRole('${escapeHtml(role.name)}')">删除</button>` : ''}
<button class="btn-secondary btn-small" onclick="editRole('${escapeHtml(role.name)}')">${_t('common.edit')}</button>
${role.name !== '默认' ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteRole('${escapeHtml(role.name)}')">${_t('common.delete')}</button>` : ''}
</div>
</div>
`;
@@ -503,7 +506,7 @@ async function loadRoleTools(page = 1, searchKeyword = '') {
console.error('加载工具列表失败:', error);
const toolsList = document.getElementById('role-tools-list');
if (toolsList) {
toolsList.innerHTML = `<div class="tools-error">加载工具列表失败: ${escapeHtml(error.message)}</div>`;
toolsList.innerHTML = `<div class="tools-error">${_t('roleModal.loadToolsFailed')}: ${escapeHtml(error.message)}</div>`;
}
}
}
@@ -521,7 +524,7 @@ function renderRoleToolsList() {
listContainer.innerHTML = '';
if (allRoleTools.length === 0) {
listContainer.innerHTML = '<div class="tools-empty">暂无工具</div>';
listContainer.innerHTML = '<div class="tools-empty">' + _t('roleModal.noTools') + '</div>';
toolsList.appendChild(listContainer);
return;
}
@@ -594,16 +597,16 @@ function renderRoleToolsPagination() {
const startItem = (page - 1) * roleToolsPagination.pageSize + 1;
const endItem = Math.min(page * roleToolsPagination.pageSize, total);
const paginationShowText = _t('roleModal.paginationShow', { start: startItem, end: endItem, total: total }) +
(roleToolsSearchKeyword ? _t('roleModal.paginationSearch', { keyword: roleToolsSearchKeyword }) : '');
pagination.innerHTML = `
<div class="pagination-info">
显示 ${startItem}-${endItem} / ${total} 个工具${roleToolsSearchKeyword ? ` (搜索: "${escapeHtml(roleToolsSearchKeyword)}")` : ''}
</div>
<div class="pagination-info">${paginationShowText}</div>
<div class="pagination-controls">
<button class="btn-secondary" onclick="loadRoleTools(1, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="loadRoleTools(${page - 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page"> ${page} / ${totalPages} </span>
<button class="btn-secondary" onclick="loadRoleTools(${page + 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="loadRoleTools(${totalPages}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>末页</button>
<button class="btn-secondary" onclick="loadRoleTools(1, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${_t('roleModal.firstPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${page - 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${_t('roleModal.prevPage')}</button>
<span class="pagination-page">${_t('roleModal.pageOf', { page: page, total: totalPages })}</span>
<button class="btn-secondary" onclick="loadRoleTools(${page + 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${_t('roleModal.nextPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${totalPages}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${_t('roleModal.lastPage')}</button>
</div>
`;
@@ -727,8 +730,8 @@ function updateRoleToolsStats() {
// 总工具数(所有工具,包括已启用和未启用的)
const totalTools = roleToolsPagination.total || 0;
statsEl.innerHTML = `
<span title="当前页选中的工具数"> 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
<span title="所有已启用工具中选中的工具总数(基于MCP管理)">📊 总计已选中: <strong>${totalEnabled}</strong> / ${totalTools} <em>(使用所有已启用工具)</em></span>
<span title="${_t('roleModal.currentPageSelectedTitle')}"> ${_t('roleModal.currentPageSelected', { current: currentPageEnabled, total: currentPageTotal })}</span>
<span title="${_t('roleModal.totalSelectedTitle')}">📊 ${_t('roleModal.totalSelected', { current: totalEnabled, total: totalTools })} <em>${_t('roleModal.usingAllEnabledTools')}</em></span>
`;
return;
}
@@ -779,8 +782,8 @@ function updateRoleToolsStats() {
const totalTools = roleToolsPagination.total || 0;
statsEl.innerHTML = `
<span title="当前页选中的工具数(只统计已启用的工具)"> 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
<span title="角色已关联的工具总数(基于角色实际配置)">📊 总计已选中: <strong>${totalSelected}</strong> / ${totalTools}</span>
<span title="${_t('roleModal.currentPageSelectedTitle')}"> ${_t('roleModal.currentPageSelected', { current: currentPageEnabled, total: currentPageTotal })}</span>
<span title="${_t('roleModal.totalSelectedTitle')}">📊 ${_t('roleModal.totalSelected', { current: totalSelected, total: totalTools })}</span>
`;
}
@@ -838,7 +841,7 @@ async function showAddRoleModal() {
const modal = document.getElementById('role-modal');
if (!modal) return;
document.getElementById('role-modal-title').textContent = '添加角色';
document.getElementById('role-modal-title').textContent = _t('roleModal.addRole');
document.getElementById('role-name').value = '';
document.getElementById('role-name').disabled = false;
document.getElementById('role-description').value = '';
@@ -918,14 +921,14 @@ async function showAddRoleModal() {
async function editRole(roleName) {
const role = roles.find(r => r.name === roleName);
if (!role) {
showNotification('角色不存在', 'error');
showNotification(_t('roleModal.roleNotFound'), 'error');
return;
}
const modal = document.getElementById('role-modal');
if (!modal) return;
document.getElementById('role-modal-title').textContent = '编辑角色';
document.getElementById('role-modal-title').textContent = _t('roleModal.editRole');
document.getElementById('role-name').value = role.name;
document.getElementById('role-name').disabled = true; // 编辑时不允许修改名称
document.getElementById('role-description').value = role.description || '';
@@ -1186,7 +1189,7 @@ async function loadAllToolsToStateMap() {
async function saveRole() {
const name = document.getElementById('role-name').value.trim();
if (!name) {
showNotification('角色名称不能为空', 'error');
showNotification(_t('roleModal.roleNameRequired'), 'error');
return;
}
@@ -1227,7 +1230,7 @@ async function saveRole() {
// 如果是首次添加角色且没有选择工具,默认使用全部工具
if (isFirstUserRole && allSelectedTools.length === 0) {
roleUsesAllTools = true;
showNotification('检测到这是首次添加角色且未选择工具,将默认使用全部工具', 'info');
showNotification(_t('roleModal.firstRoleNoToolsHint'), 'info');
} else if (roleUsesAllTools) {
// 如果当前使用所有工具,需要检查用户是否取消了一些工具
// 检查状态映射中是否有未选中的已启用工具
@@ -1358,7 +1361,7 @@ async function saveRole() {
// 删除角色
async function deleteRole(roleName) {
if (roleName === '默认') {
showNotification('不能删除默认角色', 'error');
showNotification(_t('roleModal.cannotDeleteDefaultRole'), 'error');
return;
}
@@ -1430,6 +1433,11 @@ document.addEventListener('DOMContentLoaded', () => {
updateRoleSelectorDisplay();
});
// 语言切换后刷新角色选择器显示(默认/自定义角色名)
document.addEventListener('languagechange', () => {
updateRoleSelectorDisplay();
});
// 获取当前选中的角色(供chat.js使用)
function getCurrentRole() {
return currentRole || '';
@@ -1469,7 +1477,7 @@ async function loadRoleSkills() {
allRoleSkills = [];
const skillsList = document.getElementById('role-skills-list');
if (skillsList) {
skillsList.innerHTML = '<div class="skills-error">加载skills列表失败: ' + error.message + '</div>';
skillsList.innerHTML = '<div class="skills-error">' + _t('roleModal.loadSkillsFailed') + ': ' + error.message + '</div>';
}
}
}
@@ -1490,7 +1498,7 @@ function renderRoleSkills() {
if (filteredSkills.length === 0) {
skillsList.innerHTML = '<div class="skills-empty">' +
(roleSkillsSearchKeyword ? '没有找到匹配的skills' : '暂无可用skills') +
(roleSkillsSearchKeyword ? _t('roleModal.noMatchingSkills') : _t('roleModal.noSkillsAvailable')) +
'</div>';
updateRoleSkillsStats();
return;
@@ -1596,7 +1604,7 @@ function updateRoleSkillsStats() {
filteredSkills.includes(skill)
).length;
statsEl.textContent = `已选择 ${selectedCount} / ${filteredSkills.length}`;
statsEl.textContent = _t('roleModal.skillsSelectedCount', { count: selectedCount, total: filteredSkills.length });
}
// HTML转义函数
+27 -1
View File
@@ -243,7 +243,8 @@ function initPage(pageId) {
}
break;
case 'chat':
// 对话页面已由chat.js初始化
// 恢复对话列表折叠状态(从其他页返回时保持用户选择)
initConversationSidebarState();
break;
case 'info-collect':
// 信息收集页面
@@ -421,11 +422,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; };
+43 -23
View File
@@ -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();
@@ -1367,7 +1368,7 @@ function formatExternalMCPJSON() {
try {
const jsonStr = jsonTextarea.value.trim();
if (!jsonStr) {
errorDiv.textContent = 'JSON不能为空';
errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.jsonEmpty') : 'JSON不能为空');
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
@@ -1379,7 +1380,7 @@ function formatExternalMCPJSON() {
errorDiv.style.display = 'none';
jsonTextarea.classList.remove('error');
} catch (error) {
errorDiv.textContent = 'JSON格式错误: ' + error.message;
errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.jsonError') : 'JSON格式错误') + ': ' + error.message;
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
}
@@ -1387,6 +1388,7 @@ function formatExternalMCPJSON() {
// 加载示例
function loadExternalMCPExample() {
const desc = (typeof window.t === 'function' ? window.t('externalMcpModal.exampleDescription') : '示例描述');
const example = {
"hexstrike-ai": {
command: "python3",
@@ -1395,7 +1397,7 @@ function loadExternalMCPExample() {
"--server",
"http://example.com"
],
description: "示例描述",
description: desc,
timeout: 300
},
"cyberstrike-ai-http": {
@@ -1420,7 +1422,7 @@ async function saveExternalMCP() {
const errorDiv = document.getElementById('external-mcp-json-error');
if (!jsonStr) {
errorDiv.textContent = 'JSON配置不能为空';
errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.jsonEmpty') : 'JSON不能为空');
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
jsonTextarea.focus();
@@ -1431,16 +1433,17 @@ async function saveExternalMCP() {
try {
configObj = JSON.parse(jsonStr);
} catch (error) {
errorDiv.textContent = 'JSON格式错误: ' + error.message;
errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.jsonError') : 'JSON格式错误') + ': ' + error.message;
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
jsonTextarea.focus();
return;
}
const t = (typeof window.t === 'function' ? window.t : function (k, opts) { return k; });
// 验证必须是对象格式
if (typeof configObj !== 'object' || Array.isArray(configObj) || configObj === null) {
errorDiv.textContent = '配置错误: 必须是JSON对象格式,key为配置名称,value为配置内容';
errorDiv.textContent = t('mcp.configMustBeObject');
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
@@ -1449,7 +1452,7 @@ async function saveExternalMCP() {
// 获取所有配置名称
const names = Object.keys(configObj);
if (names.length === 0) {
errorDiv.textContent = '配置错误: 至少需要一个配置项';
errorDiv.textContent = t('mcp.configNeedOne');
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
@@ -1458,7 +1461,7 @@ async function saveExternalMCP() {
// 验证每个配置
for (const name of names) {
if (!name || name.trim() === '') {
errorDiv.textContent = '配置错误: 配置名称不能为空';
errorDiv.textContent = t('mcp.configNameEmpty');
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
@@ -1466,7 +1469,7 @@ async function saveExternalMCP() {
const config = configObj[name];
if (typeof config !== 'object' || Array.isArray(config) || config === null) {
errorDiv.textContent = `配置错误: "${name}" 的配置必须是对象`;
errorDiv.textContent = t('mcp.configMustBeObj', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
@@ -1478,28 +1481,28 @@ async function saveExternalMCP() {
// 验证配置内容
const transport = config.transport || (config.command ? 'stdio' : config.url ? 'http' : '');
if (!transport) {
errorDiv.textContent = `配置错误: "${name}" 需要指定commandstdio模式)或urlhttp/sse模式)`;
errorDiv.textContent = t('mcp.configNeedCommand', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
}
if (transport === 'stdio' && !config.command) {
errorDiv.textContent = `配置错误: "${name}" stdio模式需要command字段`;
errorDiv.textContent = t('mcp.configStdioNeedCommand', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
}
if (transport === 'http' && !config.url) {
errorDiv.textContent = `配置错误: "${name}" http模式需要url字段`;
errorDiv.textContent = t('mcp.configHttpNeedUrl', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
}
if (transport === 'sse' && !config.url) {
errorDiv.textContent = `配置错误: "${name}" sse模式需要url字段`;
errorDiv.textContent = t('mcp.configSseNeedUrl', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
@@ -1514,7 +1517,7 @@ async function saveExternalMCP() {
// 如果是编辑模式,只更新当前编辑的配置
if (currentEditingMCPName) {
if (!configObj[currentEditingMCPName]) {
errorDiv.textContent = `配置错误: 编辑模式下,JSON必须包含配置名称 "${currentEditingMCPName}"`;
errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.configEditMustContainName', { name: currentEditingMCPName }) : '配置错误: 编辑模式下,JSON必须包含配置名称 "' + currentEditingMCPName + '"');
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
@@ -1561,7 +1564,7 @@ async function saveExternalMCP() {
alert(typeof window.t === 'function' ? window.t('mcp.saveSuccess') : '保存成功');
} catch (error) {
console.error('保存外部MCP失败:', error);
errorDiv.textContent = '保存失败: ' + error.message;
errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.operationFailed') : '保存失败') + ': ' + error.message;
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
}
@@ -1740,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);
}
});
+94 -66
View File
@@ -1,4 +1,7 @@
// Skills管理相关功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
let skillsList = [];
let currentEditingSkillName = null;
let isSavingSkill = false; // 防止重复提交
@@ -65,7 +68,7 @@ async function loadSkills(page = 1, pageSize = null) {
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取skills列表失败');
throw new Error(_t('skills.loadListFailed'));
}
const data = await response.json();
skillsList = data.skills || [];
@@ -76,10 +79,10 @@ async function loadSkills(page = 1, pageSize = null) {
updateSkillsManagementStats();
} catch (error) {
console.error('加载skills列表失败:', error);
showNotification('加载skills列表失败: ' + error.message, 'error');
showNotification(_t('skills.loadListFailed') + ': ' + error.message, 'error');
const skillsListEl = document.getElementById('skills-list');
if (skillsListEl) {
skillsListEl.innerHTML = '<div class="empty-state">加载失败: ' + error.message + '</div>';
skillsListEl.innerHTML = '<div class="empty-state">' + _t('skills.loadFailedShort') + ': ' + escapeHtml(error.message) + '</div>';
}
}
}
@@ -94,7 +97,7 @@ function renderSkillsList() {
if (filteredSkills.length === 0) {
skillsListEl.innerHTML = '<div class="empty-state">' +
(skillsSearchKeyword ? '没有找到匹配的skills' : '暂无skills,点击"创建Skill"创建第一个skill') +
(skillsSearchKeyword ? _t('skills.noMatch') : _t('skills.noSkills')) +
'</div>';
// 搜索时隐藏分页
const paginationContainer = document.getElementById('skills-pagination');
@@ -109,12 +112,12 @@ function renderSkillsList() {
<div class="skill-card">
<div class="skill-card-header">
<h3 class="skill-card-title">${escapeHtml(skill.name || '')}</h3>
<div class="skill-card-description">${escapeHtml(skill.description || '无描述')}</div>
<div class="skill-card-description">${escapeHtml(skill.description || _t('skills.noDescription'))}</div>
</div>
<div class="skill-card-actions">
<button class="btn-secondary btn-small" onclick="viewSkill('${escapeHtml(skill.name)}')">查看</button>
<button class="btn-secondary btn-small" onclick="editSkill('${escapeHtml(skill.name)}')">编辑</button>
<button class="btn-secondary btn-small btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')">删除</button>
<button class="btn-secondary btn-small" onclick="viewSkill('${escapeHtml(skill.name)}')">${_t('common.view')}</button>
<button class="btn-secondary btn-small" onclick="editSkill('${escapeHtml(skill.name)}')">${_t('common.edit')}</button>
<button class="btn-secondary btn-small btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')">${_t('common.delete')}</button>
</div>
</div>
`;
@@ -154,12 +157,19 @@ function renderSkillsPagination() {
let paginationHTML = '<div class="pagination">';
const paginationShowText = _t('skillsPage.paginationShow', { start, end, total });
const perPageLabelText = _t('skillsPage.perPageLabel');
const firstPageText = _t('skillsPage.firstPage');
const prevPageText = _t('skillsPage.prevPage');
const pageOfText = _t('skillsPage.pageOf', { current: currentPage, total: totalPages || 1 });
const nextPageText = _t('skillsPage.nextPage');
const lastPageText = _t('skillsPage.lastPage');
// 左侧:显示范围信息和每页数量选择器(参考MCP样式)
paginationHTML += `
<div class="pagination-info">
<span>显示 ${start}-${end} / ${total} </span>
<span>${escapeHtml(paginationShowText)}</span>
<label class="pagination-page-size">
每页显示
${escapeHtml(perPageLabelText)}
<select id="skills-page-size-pagination" onchange="changeSkillsPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
@@ -173,11 +183,11 @@ function renderSkillsPagination() {
// 右侧:分页按钮(参考MCP样式:首页、上一页、第X/Y页、下一页、末页)
paginationHTML += `
<div class="pagination-controls">
<button class="btn-secondary" onclick="loadSkills(1, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="loadSkills(${currentPage - 1}, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page"> ${currentPage} / ${totalPages || 1} </span>
<button class="btn-secondary" onclick="loadSkills(${currentPage + 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="loadSkills(${totalPages || 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
<button class="btn-secondary" onclick="loadSkills(1, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(firstPageText)}</button>
<button class="btn-secondary" onclick="loadSkills(${currentPage - 1}, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(prevPageText)}</button>
<span class="pagination-page">${escapeHtml(pageOfText)}</span>
<button class="btn-secondary" onclick="loadSkills(${currentPage + 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(nextPageText)}</button>
<button class="btn-secondary" onclick="loadSkills(${totalPages || 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(lastPageText)}</button>
</div>
`;
@@ -291,7 +301,7 @@ async function searchSkills() {
try {
const response = await apiFetch(`/api/skills?search=${encodeURIComponent(skillsSearchKeyword)}&limit=10000&offset=0`);
if (!response.ok) {
throw new Error('获取skills列表失败');
throw new Error(_t('skills.loadListFailed'));
}
const data = await response.json();
skillsList = data.skills || [];
@@ -306,7 +316,7 @@ async function searchSkills() {
updateSkillsManagementStats();
} catch (error) {
console.error('搜索skills失败:', error);
showNotification('搜索失败: ' + error.message, 'error');
showNotification(_t('skills.searchFailed') + ': ' + error.message, 'error');
}
} else {
// 没有搜索关键词时,恢复分页加载
@@ -332,7 +342,7 @@ function clearSkillsSearch() {
// 刷新skills
async function refreshSkills() {
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
showNotification('已刷新', 'success');
showNotification(_t('skills.refreshed'), 'success');
}
// 显示添加skill模态框
@@ -340,7 +350,7 @@ function showAddSkillModal() {
const modal = document.getElementById('skill-modal');
if (!modal) return;
document.getElementById('skill-modal-title').textContent = '添加Skill';
document.getElementById('skill-modal-title').textContent = _t('skills.addSkill');
document.getElementById('skill-name').value = '';
document.getElementById('skill-name').disabled = false;
document.getElementById('skill-description').value = '';
@@ -354,7 +364,7 @@ async function editSkill(skillName) {
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) {
throw new Error('获取skill详情失败');
throw new Error(_t('skills.loadDetailFailed'));
}
const data = await response.json();
const skill = data.skill;
@@ -362,7 +372,7 @@ async function editSkill(skillName) {
const modal = document.getElementById('skill-modal');
if (!modal) return;
document.getElementById('skill-modal-title').textContent = '编辑Skill';
document.getElementById('skill-modal-title').textContent = _t('skills.editSkill');
document.getElementById('skill-name').value = skill.name;
document.getElementById('skill-name').disabled = true; // 编辑时不允许修改名称
document.getElementById('skill-description').value = skill.description || '';
@@ -372,7 +382,7 @@ async function editSkill(skillName) {
modal.style.display = 'flex';
} catch (error) {
console.error('加载skill详情失败:', error);
showNotification('加载skill详情失败: ' + error.message, 'error');
showNotification(_t('skills.loadDetailFailed') + ': ' + error.message, 'error');
}
}
@@ -381,7 +391,7 @@ async function viewSkill(skillName) {
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) {
throw new Error('获取skill详情失败');
throw new Error(_t('skills.loadDetailFailed'));
}
const data = await response.json();
const skill = data.skill;
@@ -390,22 +400,29 @@ async function viewSkill(skillName) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'skill-view-modal';
const viewTitle = _t('skills.viewSkillTitle', { name: skill.name });
const descLabel = _t('skills.descriptionLabel');
const pathLabel = _t('skills.pathLabel');
const modTimeLabel = _t('skills.modTimeLabel');
const contentLabel = _t('skills.contentLabel');
const closeBtn = _t('common.close');
const editBtn = _t('common.edit');
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; max-height: 90vh;">
<div class="modal-header">
<h2>查看Skill: ${escapeHtml(skill.name)}</h2>
<h2>${escapeHtml(viewTitle)}</h2>
<span class="modal-close" onclick="closeSkillViewModal()">&times;</span>
</div>
<div class="modal-body" style="overflow-y: auto; max-height: calc(90vh - 120px);">
${skill.description ? `<div style="margin-bottom: 16px;"><strong>描述:</strong> ${escapeHtml(skill.description)}</div>` : ''}
<div style="margin-bottom: 8px;"><strong>路径:</strong> ${escapeHtml(skill.path || '')}</div>
<div style="margin-bottom: 16px;"><strong>修改时间:</strong> ${escapeHtml(skill.mod_time || '')}</div>
<div style="margin-bottom: 8px;"><strong>内容:</strong></div>
${skill.description ? `<div style="margin-bottom: 16px;"><strong>${escapeHtml(descLabel)}</strong> ${escapeHtml(skill.description)}</div>` : ''}
<div style="margin-bottom: 8px;"><strong>${escapeHtml(pathLabel)}</strong> ${escapeHtml(skill.path || '')}</div>
<div style="margin-bottom: 16px;"><strong>${escapeHtml(modTimeLabel)}</strong> ${escapeHtml(skill.mod_time || '')}</div>
<div style="margin-bottom: 8px;"><strong>${escapeHtml(contentLabel)}</strong></div>
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(skill.content || '')}</pre>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillViewModal()">关闭</button>
<button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">编辑</button>
<button class="btn-secondary" onclick="closeSkillViewModal()">${escapeHtml(closeBtn)}</button>
<button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">${escapeHtml(editBtn)}</button>
</div>
</div>
`;
@@ -413,7 +430,7 @@ async function viewSkill(skillName) {
modal.style.display = 'flex';
} catch (error) {
console.error('查看skill失败:', error);
showNotification('查看skill失败: ' + error.message, 'error');
showNotification(_t('skills.viewFailed') + ': ' + error.message, 'error');
}
}
@@ -443,18 +460,18 @@ async function saveSkill() {
const content = document.getElementById('skill-content').value.trim();
if (!name) {
showNotification('skill名称不能为空', 'error');
showNotification(_t('skills.nameRequired'), 'error');
return;
}
if (!content) {
showNotification('skill内容不能为空', 'error');
showNotification(_t('skills.contentRequired'), 'error');
return;
}
// 验证skill名称
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
showNotification('skill名称只能包含字母、数字、连字符和下划线', 'error');
showNotification(_t('skills.nameInvalid'), 'error');
return;
}
@@ -462,7 +479,7 @@ async function saveSkill() {
const saveBtn = document.querySelector('#skill-modal .btn-primary');
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
saveBtn.textContent = _t('skills.saving');
}
try {
@@ -484,20 +501,20 @@ async function saveSkill() {
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '保存skill失败');
throw new Error(error.error || _t('skills.saveFailed'));
}
showNotification(isEdit ? 'skill已更新' : 'skill已创建', 'success');
showNotification(isEdit ? _t('skills.saveSuccess') : _t('skills.createdSuccess'), 'success');
closeSkillModal();
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
} catch (error) {
console.error('保存skill失败:', error);
showNotification('保存skill失败: ' + error.message, 'error');
showNotification(_t('skills.saveFailed') + ': ' + error.message, 'error');
} finally {
isSavingSkill = false;
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = '保存';
saveBtn.textContent = _t('common.save');
}
}
}
@@ -518,10 +535,10 @@ async function deleteSkill(skillName) {
}
// 构建确认消息
let confirmMessage = `确定要删除skill "${skillName}" 吗?此操作不可恢复。`;
let confirmMessage = _t('skills.deleteConfirm', { name: skillName });
if (boundRoles.length > 0) {
const rolesList = boundRoles.join('、');
confirmMessage = `确定要删除skill "${skillName}" 吗?\n\n⚠️ 该skill当前已被以下 ${boundRoles.length} 个角色绑定:\n${rolesList}\n\n删除后,系统将自动从这些角色中移除该skill的绑定。\n\n此操作不可恢复,是否继续?`;
confirmMessage = _t('skills.deleteConfirmWithRoles', { name: skillName, count: boundRoles.length, roles: rolesList });
}
if (!confirm(confirmMessage)) {
@@ -535,14 +552,14 @@ async function deleteSkill(skillName) {
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '删除skill失败');
throw new Error(error.error || _t('skills.deleteFailed'));
}
const data = await response.json();
let successMessage = 'skill已删除';
let successMessage = _t('skills.deleteSuccess');
if (data.affected_roles && data.affected_roles.length > 0) {
const rolesList = data.affected_roles.join('、');
successMessage = `skill已删除,已自动从 ${data.affected_roles.length} 个角色中移除绑定:${rolesList}`;
successMessage = _t('skills.deleteSuccessWithRoles', { count: data.affected_roles.length, roles: rolesList });
}
showNotification(successMessage, 'success');
@@ -554,7 +571,7 @@ async function deleteSkill(skillName) {
await loadSkills(pageToLoad, skillsPagination.pageSize);
} catch (error) {
console.error('删除skill失败:', error);
showNotification('删除skill失败: ' + error.message, 'error');
showNotification(_t('skills.deleteFailed') + ': ' + error.message, 'error');
}
}
@@ -565,7 +582,7 @@ async function loadSkillsMonitor() {
try {
const response = await apiFetch('/api/skills/stats');
if (!response.ok) {
throw new Error('获取skills统计信息失败');
throw new Error(_t('skills.loadStatsFailed'));
}
const data = await response.json();
@@ -581,14 +598,14 @@ async function loadSkillsMonitor() {
renderSkillsMonitor();
} catch (error) {
console.error('加载skills监控数据失败:', error);
showNotification('加载skills监控数据失败: ' + error.message, 'error');
showNotification(_t('skills.loadStatsFailed') + ': ' + error.message, 'error');
const statsEl = document.getElementById('skills-stats');
if (statsEl) {
statsEl.innerHTML = '<div class="monitor-error">无法加载统计信息:' + escapeHtml(error.message) + '</div>';
statsEl.innerHTML = '<div class="monitor-error">' + _t('skills.loadStatsErrorShort') + ': ' + escapeHtml(error.message) + '</div>';
}
const monitorListEl = document.getElementById('skills-monitor-list');
if (monitorListEl) {
monitorListEl.innerHTML = '<div class="monitor-error">无法加载调用统计:' + escapeHtml(error.message) + '</div>';
monitorListEl.innerHTML = '<div class="monitor-error">' + _t('skills.loadCallStatsError') + ': ' + escapeHtml(error.message) + '</div>';
}
}
}
@@ -604,23 +621,23 @@ function renderSkillsMonitor() {
statsEl.innerHTML = `
<div class="monitor-stat-card">
<div class="monitor-stat-label">总Skills数</div>
<div class="monitor-stat-label">${_t('skills.totalSkillsCount')}</div>
<div class="monitor-stat-value">${skillsStats.total}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">总调用次数</div>
<div class="monitor-stat-label">${_t('skills.totalCallsCount')}</div>
<div class="monitor-stat-value">${skillsStats.totalCalls}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">成功调用</div>
<div class="monitor-stat-label">${_t('skills.successfulCalls')}</div>
<div class="monitor-stat-value" style="color: #28a745;">${skillsStats.totalSuccess}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">失败调用</div>
<div class="monitor-stat-label">${_t('skills.failedCalls')}</div>
<div class="monitor-stat-value" style="color: #dc3545;">${skillsStats.totalFailed}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">成功率</div>
<div class="monitor-stat-label">${_t('skills.successRate')}</div>
<div class="monitor-stat-value">${successRate}%</div>
</div>
`;
@@ -634,7 +651,7 @@ function renderSkillsMonitor() {
// 如果没有统计数据,显示空状态
if (stats.length === 0) {
monitorListEl.innerHTML = '<div class="monitor-empty">暂无Skills调用记录</div>';
monitorListEl.innerHTML = '<div class="monitor-empty">' + _t('skills.noCallRecords') + '</div>';
return;
}
@@ -652,12 +669,12 @@ function renderSkillsMonitor() {
<table class="monitor-table">
<thead>
<tr>
<th style="text-align: left !important;">Skill名称</th>
<th style="text-align: center;">总调用</th>
<th style="text-align: center;">成功</th>
<th style="text-align: center;">失败</th>
<th style="text-align: center;">成功率</th>
<th style="text-align: left;">最后调用时间</th>
<th style="text-align: left !important;">${_t('skills.skillName')}</th>
<th style="text-align: center;">${_t('skills.totalCalls')}</th>
<th style="text-align: center;">${_t('skills.success')}</th>
<th style="text-align: center;">${_t('skills.failure')}</th>
<th style="text-align: center;">${_t('skills.successRate')}</th>
<th style="text-align: left;">${_t('skills.lastCallTime')}</th>
</tr>
</thead>
<tbody>
@@ -687,12 +704,12 @@ function renderSkillsMonitor() {
// 刷新skills监控
async function refreshSkillsMonitor() {
await loadSkillsMonitor();
showNotification('已刷新', 'success');
showNotification(_t('skills.refreshed'), 'success');
}
// 清空skills统计数据
async function clearSkillsStats() {
if (!confirm('确定要清空所有Skills统计数据吗?此操作不可恢复。')) {
if (!confirm(_t('skills.clearStatsConfirm'))) {
return;
}
@@ -703,15 +720,15 @@ async function clearSkillsStats() {
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '清空统计数据失败');
throw new Error(error.error || _t('skills.clearStatsFailed'));
}
showNotification('已清空所有Skills统计数据', 'success');
showNotification(_t('skills.statsCleared'), 'success');
// 重新加载统计数据
await loadSkillsMonitor();
} catch (error) {
console.error('清空统计数据失败:', error);
showNotification('清空统计数据失败: ' + error.message, 'error');
showNotification(_t('skills.clearStatsFailed') + ': ' + error.message, 'error');
}
}
@@ -722,3 +739,14 @@ function escapeHtml(text) {
div.textContent = text;
return div.innerHTML;
}
// 语言切换时重新渲染当前页(技能列表与分页使用 _t,需随语言更新)
document.addEventListener('languagechange', function () {
const page = document.getElementById('page-skills-management');
if (page && page.classList.contains('active')) {
renderSkillsList();
if (!skillsSearchKeyword) {
renderSkillsPagination();
}
}
});
+76 -11
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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);
+30 -15
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 文档 - CyberStrikeAI</title>
<title data-i18n="apiDocs.pageTitle">API 文档 - CyberStrikeAI</title>
<link rel="icon" type="image/png" href="/static/logo.png">
<link rel="stylesheet" href="/static/css/style.css">
<style>
@@ -22,6 +22,7 @@
}
.api-docs-header {
position: relative;
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 2px solid var(--border-color);
@@ -833,9 +834,21 @@
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
API 文档
<span data-i18n="apiDocs.title">API 文档</span>
</h1>
<p>CyberStrikeAI 平台 API 接口文档,支持在线测试</p>
<p data-i18n="apiDocs.subtitle">CyberStrikeAI 平台 API 接口文档,支持在线测试</p>
<div class="api-docs-lang-switcher" style="position: absolute; top: 24px; right: 24px;">
<div class="lang-switcher">
<button type="button" class="btn-secondary lang-switcher-btn" onclick="typeof toggleLangDropdown === 'function' && toggleLangDropdown()" title="界面语言">
<span class="lang-switcher-icon">🌐</span>
<span id="current-lang-label">中文</span>
</button>
<div id="lang-dropdown" class="lang-dropdown" style="display: none;">
<div class="lang-option" data-lang="zh-CN" onclick="typeof onLanguageSelect === 'function' && onLanguageSelect('zh-CN')">中文</div>
<div class="lang-option" data-lang="en-US" onclick="typeof onLanguageSelect === 'function' && onLanguageSelect('en-US')">English</div>
</div>
</div>
</div>
</div>
<div id="auth-info-section" class="auth-info-section" style="display: none;">
@@ -846,17 +859,17 @@
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<h3 style="margin: 0; font-size: 1rem; font-weight: 600; color: var(--text-primary);">API 认证说明</h3>
<h3 style="margin: 0; font-size: 1rem; font-weight: 600; color: var(--text-primary);" data-i18n="apiDocs.authTitle">API 认证说明</h3>
</div>
<svg id="auth-info-arrow" class="auth-info-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="transition: transform 0.2s ease;">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
<div id="auth-info-body" class="auth-info-body" style="display: none; color: var(--text-secondary); font-size: 0.875rem; line-height: 1.6; margin-top: 16px;">
<p style="margin: 0 0 12px 0;"><strong>所有 API 接口都需要 Token 认证。</strong></p>
<p style="margin: 0 0 12px 0;"><strong data-i18n="apiDocs.authAllNeedToken">所有 API 接口都需要 Token 认证。</strong></p>
<div style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin-bottom: 12px;">
<p style="margin: 0 0 8px 0; font-weight: 500;">1. 获取 Token</p>
<p style="margin: 0 0 8px 0;">在前端页面登录后,Token 会自动保存。您也可以通过以下方式获取:</p>
<p style="margin: 0 0 8px 0; font-weight: 500;" data-i18n="apiDocs.authGetToken">1. 获取 Token</p>
<p style="margin: 0 0 8px 0;" data-i18n="apiDocs.authGetTokenDesc">在前端页面登录后,Token 会自动保存。您也可以通过以下方式获取:</p>
<pre style="background: var(--bg-primary); padding: 8px; border-radius: 4px; margin: 8px 0; overflow-x: auto; font-size: 0.8125rem;"><code>POST /api/auth/login
Content-Type: application/json
@@ -871,13 +884,13 @@ Content-Type: application/json
}</code></pre>
</div>
<div style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin-bottom: 12px;">
<p style="margin: 0 0 8px 0; font-weight: 500;">2. 使用 Token</p>
<p style="margin: 0 0 8px 0;">在请求头中添加 Authorization 字段:</p>
<p style="margin: 0 0 8px 0; font-weight: 500;" data-i18n="apiDocs.authUseToken">2. 使用 Token</p>
<p style="margin: 0 0 8px 0;" data-i18n="apiDocs.authUseTokenDesc">在请求头中添加 Authorization 字段:</p>
<pre style="background: var(--bg-primary); padding: 8px; border-radius: 4px; margin: 8px 0; overflow-x: auto; font-size: 0.8125rem;"><code>Authorization: Bearer your_token_here</code></pre>
<p style="margin: 8px 0 0 0; font-size: 0.8125rem; color: var(--text-muted);">💡 提示:本页面会自动使用您已登录的 Token,无需手动填写。</p>
<p style="margin: 8px 0 0 0; font-size: 0.8125rem; color: var(--text-muted);" data-i18n="apiDocs.authTip">💡 提示:本页面会自动使用您已登录的 Token,无需手动填写。</p>
</div>
<div id="token-status" style="display: none; background: rgba(0, 102, 255, 0.1); padding: 8px 12px; border-radius: 6px; border-left: 3px solid var(--accent-color);">
<p style="margin: 0; font-size: 0.8125rem; color: var(--accent-color);">
<p style="margin: 0; font-size: 0.8125rem; color: var(--accent-color);" data-i18n="apiDocs.tokenDetected">
<strong>✓ 已检测到 Token</strong> - 您可以直接测试 API 接口
</p>
</div>
@@ -899,10 +912,10 @@ Content-Type: application/json
<div class="api-docs-content">
<div class="api-docs-sidebar">
<h3>API 分组</h3>
<h3 data-i18n="apiDocs.sidebarGroupTitle">API 分组</h3>
<ul class="api-group-list" id="api-group-list">
<li class="api-group-item">
<a href="#" class="api-group-link active" data-group="all">全部接口</a>
<a href="#" class="api-group-link active" data-group="all" data-i18n="apiDocs.allApis">全部接口</a>
</li>
</ul>
</div>
@@ -914,13 +927,15 @@ Content-Type: application/json
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<h3>加载中...</h3>
<p>正在加载 API 文档</p>
<h3 data-i18n="apiDocs.loading">加载中...</h3>
<p data-i18n="apiDocs.loadingDesc">正在加载 API 文档</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/i18next.min.js"></script>
<script src="/static/js/i18n.js"></script>
<script src="/static/js/api-docs.js"></script>
</body>
</html>
+116 -110
View File
@@ -400,12 +400,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">
<!-- 全局搜索 -->
@@ -774,18 +780,18 @@
<label for="fofa-query" data-i18n="infoCollectPage.fofaQuerySyntax">FOFA 查询语法</label>
<textarea id="fofa-query" class="info-collect-query-input" rows="1" data-i18n="infoCollect.queryPlaceholder" data-i18n-attr="placeholder" placeholder='例如:app="Apache" && country="CN"'></textarea>
<small class="form-hint" data-i18n="infoCollectPage.formHint">查询语法参考 FOFA 文档,支持 && / || / () 等。</small>
<div class="info-collect-presets" aria-label="FOFA 查询示例">
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('app=&quot;Apache&quot; &amp;&amp; country=&quot;CN&quot;')" data-i18n="infoCollectPage.presetApache" data-i18n-attr="title" title="填入示例">Apache + 中国</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('title=&quot;登录&quot; &amp;&amp; country=&quot;CN&quot;')" data-i18n="infoCollectPage.presetLogin" data-i18n-attr="title" title="填入示例">登录页 + 中国</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('domain=&quot;example.com&quot;')" data-i18n="infoCollectPage.presetDomain" data-i18n-attr="title" title="填入示例">指定域名</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('ip=&quot;1.1.1.1&quot;')" data-i18n="infoCollectPage.presetIp" data-i18n-attr="title" title="填入示例">指定 IP</button>
<div class="info-collect-presets" aria-label="FOFA 查询示例" data-i18n="infoCollectPage.queryPresetsAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('app=&quot;Apache&quot; &amp;&amp; country=&quot;CN&quot;')" data-i18n="infoCollectPage.presetApache" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">Apache + 中国</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('title=&quot;登录&quot; &amp;&amp; country=&quot;CN&quot;')" data-i18n="infoCollectPage.presetLogin" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">登录页 + 中国</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('domain=&quot;example.com&quot;')" data-i18n="infoCollectPage.presetDomain" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">指定域名</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('ip=&quot;1.1.1.1&quot;')" data-i18n="infoCollectPage.presetIp" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">指定 IP</button>
</div>
</div>
<div class="form-group">
<label for="fofa-nl" data-i18n="infoCollectPage.naturalLanguage">自然语言(AI 解析为 FOFA 语法)</label>
<div class="info-collect-nl-row">
<textarea id="fofa-nl" class="info-collect-query-input" rows="1" data-i18n="infoCollectPage.nlPlaceholder" data-i18n-attr="placeholder" placeholder="例如:找美国 Missouri 的 Apache 站点,标题包含 Home"></textarea>
<button id="fofa-nl-parse-btn" class="btn-secondary" type="button" onclick="parseFofaNaturalLanguage()" data-i18n="infoCollectPage.parseBtn" data-i18n-attr="title" title="将自然语言解析为 FOFA 查询语法">AI 解析</button>
<button id="fofa-nl-parse-btn" class="btn-secondary" type="button" onclick="parseFofaNaturalLanguage()" data-i18n="infoCollectPage.parseBtn" data-i18n-attr="title" data-i18n-title="infoCollectPage.parseBtnTitle" title="将自然语言解析为 FOFA 查询语法">AI 解析</button>
</div>
<div id="fofa-nl-status" class="fofa-nl-status muted" style="display: none;" aria-live="polite"></div>
<small class="form-hint" data-i18n="infoCollectPage.parseHint">解析后会弹窗展示 FOFA 语法(可编辑),确认无误后再填入查询框并执行查询。</small>
@@ -803,17 +809,17 @@
<label class="checkbox-label" style="margin-top: 24px;">
<input type="checkbox" id="fofa-full" class="modern-checkbox" />
<span class="checkbox-custom"></span>
<span class="checkbox-text">full</span>
<span class="checkbox-text" data-i18n="infoCollectPage.fullLabel">full</span>
</label>
</div>
</div>
<div class="form-group">
<label for="fofa-fields" data-i18n="infoCollectPage.returnFields">返回字段名(逗号分隔)</label>
<input type="text" id="fofa-fields" value="host,ip,port,domain,title,protocol,country,province,city,server" />
<div class="info-collect-presets" aria-label="FOFA 字段模板">
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain')" data-i18n="infoCollectPage.minFields" data-i18n-attr="title" title="适合快速导出目标">最小字段</button>
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,title,ip,port,domain,protocol,server,icp,country,province,city')" data-i18n="infoCollectPage.webCommon" data-i18n-attr="title" title="适合浏览和筛选">Web 常用</button>
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain,title,protocol,country,province,city,server,as_number,as_organization,icp,header,banner')" data-i18n="infoCollectPage.intelEnhanced" data-i18n-attr="title" title="更偏指纹/情报">情报增强</button>
<div class="info-collect-presets" aria-label="FOFA 字段模板" data-i18n="infoCollectPage.fieldsPresetsAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain')" data-i18n="infoCollectPage.minFields" data-i18n-attr="title" data-i18n-title="infoCollectPage.minFieldsTitle" title="适合快速导出目标">最小字段</button>
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,title,ip,port,domain,protocol,server,icp,country,province,city')" data-i18n="infoCollectPage.webCommon" data-i18n-attr="title" data-i18n-title="infoCollectPage.webCommonTitle" title="适合浏览和筛选">Web 常用</button>
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain,title,protocol,country,province,city,server,as_number,as_organization,icp,header,banner')" data-i18n="infoCollectPage.intelEnhanced" data-i18n-attr="title" data-i18n-title="infoCollectPage.intelEnhancedTitle" title="更偏指纹/情报">情报增强</button>
</div>
</div>
</div>
@@ -825,7 +831,7 @@
<div class="info-collect-results-title" data-i18n="infoCollectPage.queryResults">查询结果</div>
<div class="info-collect-results-meta" id="fofa-results-meta">-</div>
</div>
<div class="info-collect-results-toolbar" aria-label="结果工具条">
<div class="info-collect-results-toolbar" aria-label="结果工具条" data-i18n="infoCollectPage.resultsToolbarAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<div class="info-collect-selected" id="fofa-selected-meta" data-i18n="infoCollectPage.selectedRowsZero">已选择 0 条</div>
<button class="btn-secondary btn-small" type="button" onclick="toggleFofaColumnsPanel()" data-i18n="infoCollectPage.showHideColumns" data-i18n-attr="title" title="显示/隐藏字段"><span data-i18n="infoCollectPage.columns"></span></button>
<button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('csv')" data-i18n="infoCollectPage.exportCsvTitle" data-i18n-attr="title" title="导出当前结果为 CSVUTF-8,兼容中文)"><span data-i18n="infoCollectPage.exportCsv">导出 CSV</span></button>
@@ -1281,24 +1287,24 @@
</label>
</div>
<div class="form-group">
<label for="robot-wecom-token">Token</label>
<input type="text" id="robot-wecom-token" placeholder="Token" autocomplete="off" />
<label for="robot-wecom-token" data-i18n="settings.robots.wecom.token">Token</label>
<input type="text" id="robot-wecom-token" data-i18n="settings.robots.wecom.tokenPlaceholder" data-i18n-attr="placeholder" placeholder="Token" autocomplete="off" />
</div>
<div class="form-group">
<label for="robot-wecom-encoding-aes-key">EncodingAESKey</label>
<input type="text" id="robot-wecom-encoding-aes-key" placeholder="EncodingAESKey(明文模式可留空)" autocomplete="off" />
<label for="robot-wecom-encoding-aes-key" data-i18n="settings.robots.wecom.encodingAesKey">EncodingAESKey</label>
<input type="text" id="robot-wecom-encoding-aes-key" data-i18n="settings.robots.wecom.encodingAesKeyPlaceholder" data-i18n-attr="placeholder" placeholder="EncodingAESKey(明文模式可留空)" autocomplete="off" />
</div>
<div class="form-group">
<label for="robot-wecom-corp-id">CorpID</label>
<input type="text" id="robot-wecom-corp-id" placeholder="企业 ID" autocomplete="off" />
<label for="robot-wecom-corp-id" data-i18n="settings.robots.wecom.corpId">CorpID</label>
<input type="text" id="robot-wecom-corp-id" data-i18n="settings.robots.wecom.corpIdPlaceholder" data-i18n-attr="placeholder" placeholder="企业 ID" autocomplete="off" />
</div>
<div class="form-group">
<label for="robot-wecom-secret">Secret</label>
<input type="password" id="robot-wecom-secret" placeholder="应用 Secret" autocomplete="off" />
<label for="robot-wecom-secret" data-i18n="settings.robots.wecom.secret">Secret</label>
<input type="password" id="robot-wecom-secret" data-i18n="settings.robots.wecom.secretPlaceholder" data-i18n-attr="placeholder" placeholder="应用 Secret" autocomplete="off" />
</div>
<div class="form-group">
<label for="robot-wecom-agent-id">AgentID</label>
<input type="number" id="robot-wecom-agent-id" placeholder="应用 AgentId" />
<label for="robot-wecom-agent-id" data-i18n="settings.robots.wecom.agentId">AgentID</label>
<input type="number" id="robot-wecom-agent-id" data-i18n="settings.robots.wecom.agentIdPlaceholder" data-i18n-attr="placeholder" placeholder="应用 AgentId" />
</div>
</div>
</div>
@@ -1315,13 +1321,13 @@
</label>
</div>
<div class="form-group">
<label for="robot-dingtalk-client-id">Client ID (AppKey)</label>
<input type="text" id="robot-dingtalk-client-id" placeholder="钉钉应用 AppKey" autocomplete="off" />
<label for="robot-dingtalk-client-id" data-i18n="settings.robots.dingtalk.clientIdLabel">Client ID (AppKey)</label>
<input type="text" id="robot-dingtalk-client-id" data-i18n="settings.robots.dingtalk.clientIdPlaceholder" data-i18n-attr="placeholder" placeholder="钉钉应用 AppKey" autocomplete="off" />
</div>
<div class="form-group">
<label for="robot-dingtalk-client-secret">Client Secret</label>
<input type="password" id="robot-dingtalk-client-secret" placeholder="钉钉应用 Secret" autocomplete="off" />
<small class="form-hint">需开启机器人能力并配置流式接入</small>
<label for="robot-dingtalk-client-secret" data-i18n="settings.robots.dingtalk.clientSecretLabel">Client Secret</label>
<input type="password" id="robot-dingtalk-client-secret" data-i18n="settings.robots.dingtalk.clientSecretPlaceholder" data-i18n-attr="placeholder" placeholder="钉钉应用 Secret" autocomplete="off" />
<small class="form-hint" data-i18n="settings.robots.dingtalk.streamHint">需开启机器人能力并配置流式接入</small>
</div>
</div>
</div>
@@ -1338,41 +1344,41 @@
</label>
</div>
<div class="form-group">
<label for="robot-lark-app-id">App ID</label>
<input type="text" id="robot-lark-app-id" placeholder="飞书应用 App ID" autocomplete="off" />
<label for="robot-lark-app-id" data-i18n="settings.robots.lark.appIdLabel">App ID</label>
<input type="text" id="robot-lark-app-id" data-i18n="settings.robots.lark.appIdPlaceholder" data-i18n-attr="placeholder" placeholder="飞书应用 App ID" autocomplete="off" />
</div>
<div class="form-group">
<label for="robot-lark-app-secret">App Secret</label>
<input type="password" id="robot-lark-app-secret" placeholder="飞书应用 App Secret" autocomplete="off" />
<label for="robot-lark-app-secret" data-i18n="settings.robots.lark.appSecretLabel">App Secret</label>
<input type="password" id="robot-lark-app-secret" data-i18n="settings.robots.lark.appSecretPlaceholder" data-i18n-attr="placeholder" placeholder="飞书应用 App Secret" autocomplete="off" />
</div>
<div class="form-group">
<label for="robot-lark-verify-token">Verify Token(可选)</label>
<input type="text" id="robot-lark-verify-token" placeholder="事件订阅 Verification Token" autocomplete="off" />
<label for="robot-lark-verify-token" data-i18n="settings.robots.lark.verifyTokenLabel">Verify Token(可选)</label>
<input type="text" id="robot-lark-verify-token" data-i18n="settings.robots.lark.verifyTokenPlaceholder" data-i18n-attr="placeholder" placeholder="事件订阅 Verification Token" autocomplete="off" />
</div>
</div>
</div>
<div class="settings-subsection">
<h4>机器人命令说明</h4>
<p class="settings-description">在对话中可发送以下命令(支持中英文):</p>
<h4 data-i18n="settingsRobotsExtra.botCommandsTitle">机器人命令说明</h4>
<p class="settings-description" data-i18n="settingsRobotsExtra.botCommandsDesc">在对话中可发送以下命令(支持中英文):</p>
<ul style="color: var(--text-muted); font-size: 13px; line-height: 1.8; margin: 8px 0 0 16px;">
<li><code>帮助</code> <code>help</code> — 显示本帮助 | Show this help</li>
<li><code>列表</code> <code>list</code> — 列出所有对话标题与 ID | List conversations</li>
<li><code>切换 &lt;ID&gt;</code> <code>switch &lt;ID&gt;</code> — 指定对话继续 | Switch to conversation</li>
<li><code>新对话</code> <code>new</code> — 开启新对话 | Start new conversation</li>
<li><code>清空</code> <code>clear</code> — 清空当前上下文 | Clear context</li>
<li><code>当前</code> <code>current</code> — 显示当前对话 ID 与标题 | Show current conversation</li>
<li><code>停止</code> <code>stop</code> — 中断当前任务 | Stop running task</li>
<li><code>角色</code> <code>roles</code> — 列出所有可用角色 | List roles</li>
<li><code>角色 &lt;&gt;</code> <code>role &lt;name&gt;</code> — 切换当前角色 | Switch role</li>
<li><code>删除 &lt;ID&gt;</code> <code>delete &lt;ID&gt;</code> — 删除指定对话 | Delete conversation</li>
<li><code>版本</code> <code>version</code> — 显示当前版本号 | Show version</li>
<li><code>帮助</code> <code>help</code><span data-i18n="settingsRobotsExtra.botCmdHelp">显示本帮助 | Show this help</span></li>
<li><code>列表</code> <code>list</code><span data-i18n="settingsRobotsExtra.botCmdList">列出所有对话标题与 ID | List conversations</span></li>
<li><code>切换 &lt;ID&gt;</code> <code>switch &lt;ID&gt;</code><span data-i18n="settingsRobotsExtra.botCmdSwitch">指定对话继续 | Switch to conversation</span></li>
<li><code>新对话</code> <code>new</code><span data-i18n="settingsRobotsExtra.botCmdNew">开启新对话 | Start new conversation</span></li>
<li><code>清空</code> <code>clear</code><span data-i18n="settingsRobotsExtra.botCmdClear">清空当前上下文 | Clear context</span></li>
<li><code>当前</code> <code>current</code><span data-i18n="settingsRobotsExtra.botCmdCurrent">显示当前对话 ID 与标题 | Show current conversation</span></li>
<li><code>停止</code> <code>stop</code><span data-i18n="settingsRobotsExtra.botCmdStop">中断当前任务 | Stop running task</span></li>
<li><code>角色</code> <code>roles</code><span data-i18n="settingsRobotsExtra.botCmdRoles">列出所有可用角色 | List roles</span></li>
<li><code>角色 &lt;&gt;</code> <code>role &lt;name&gt;</code><span data-i18n="settingsRobotsExtra.botCmdRole">切换当前角色 | Switch role</span></li>
<li><code>删除 &lt;ID&gt;</code> <code>delete &lt;ID&gt;</code><span data-i18n="settingsRobotsExtra.botCmdDelete">删除指定对话 | Delete conversation</span></li>
<li><code>版本</code> <code>version</code><span data-i18n="settingsRobotsExtra.botCmdVersion">显示当前版本号 | Show version</span></li>
</ul>
<p class="settings-description" style="margin-top: 8px;">除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis.</p>
<p class="settings-description" style="margin-top: 8px;" data-i18n="settingsRobotsExtra.botCommandsFooter">除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis.</p>
</div>
<div class="settings-actions">
<button class="btn-primary" onclick="applySettings()">应用配置</button>
<button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
</div>
</div>
@@ -1507,18 +1513,18 @@
<div id="external-mcp-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2 id="external-mcp-modal-title">添加外部MCP</h2>
<h2 id="external-mcp-modal-title" data-i18n="mcp.addExternalMCP">添加外部MCP</h2>
<span class="modal-close" onclick="closeExternalMCPModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="external-mcp-json">配置JSON <span style="color: red;">*</span></label>
<textarea id="external-mcp-json" rows="15" placeholder='{\n "hexstrike-ai": {\n "command": "python3",\n "args": ["/path/to/script.py"],\n "description": "描述",\n "timeout": 300\n }\n}' style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea>
<label for="external-mcp-json"><span data-i18n="externalMcpModal.configJson">配置JSON</span> <span style="color: red;">*</span></label>
<textarea id="external-mcp-json" rows="15" data-i18n="externalMcpModal.placeholder" data-i18n-attr="placeholder" style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea>
<div class="password-hint">
<strong>配置格式:</strong>JSON对象,key为配置名称,value为配置内容。状态通过"启动/停止"按钮控制,无需在JSON中配置。<br>
<strong>配置示例:</strong><br>
<strong>stdio模式:</strong><br>
<code style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
<strong data-i18n="externalMcpModal.formatLabel">配置格式:</strong><span data-i18n="externalMcpModal.formatDesc">JSON对象,key为配置名称,value为配置内容。状态通过"启动/停止"按钮控制,无需在JSON中配置。</span><br>
<strong data-i18n="externalMcpModal.configExample">配置示例:</strong><br>
<strong data-i18n="externalMcpModal.stdioMode">stdio模式:</strong><br>
<code data-i18n="externalMcpModal.exampleStdio" style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
"hexstrike-ai": {
"command": "python3",
"args": ["/path/to/script.py", "--server", "http://example.com"],
@@ -1526,15 +1532,15 @@
"timeout": 300
}
}</code>
<strong>HTTP模式:</strong><br>
<code style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
<strong data-i18n="externalMcpModal.httpMode">HTTP模式:</strong><br>
<code data-i18n="externalMcpModal.exampleHttp" style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
"cyberstrike-ai-http": {
"transport": "http",
"url": "http://127.0.0.1:8081/mcp"
}
}</code>
<strong>SSE模式:</strong><br>
<code style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
<strong data-i18n="externalMcpModal.sseMode">SSE模式:</strong><br>
<code data-i18n="externalMcpModal.exampleSse" style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
"cyberstrike-ai-sse": {
"transport": "sse",
"url": "http://127.0.0.1:8081/mcp/sse"
@@ -1544,13 +1550,13 @@
<div id="external-mcp-json-error" class="error-message" style="display: none; margin-top: 8px; padding: 8px; background: rgba(220, 53, 69, 0.1); border: 1px solid rgba(220, 53, 69, 0.3); border-radius: 4px; color: var(--error-color); font-size: 0.875rem;"></div>
</div>
<div class="form-group">
<button type="button" class="btn-secondary" onclick="formatExternalMCPJSON()" style="margin-top: 8px;">格式化JSON</button>
<button type="button" class="btn-secondary" onclick="loadExternalMCPExample()" style="margin-top: 8px; margin-left: 8px;">加载示例</button>
<button type="button" class="btn-secondary" onclick="formatExternalMCPJSON()" style="margin-top: 8px;" data-i18n="externalMcpModal.formatJson">格式化JSON</button>
<button type="button" class="btn-secondary" onclick="loadExternalMCPExample()" style="margin-top: 8px; margin-left: 8px;" data-i18n="externalMcpModal.loadExample">加载示例</button>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeExternalMCPModal()">取消</button>
<button class="btn-primary" onclick="saveExternalMCP()">保存</button>
<button class="btn-secondary" onclick="closeExternalMCPModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveExternalMCP()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -1581,7 +1587,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="搜索节点..."
@@ -1687,23 +1693,23 @@
<div id="skill-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2 id="skill-modal-title">添加Skill</h2>
<h2 id="skill-modal-title" data-i18n="skillModal.addSkill">添加Skill</h2>
<span class="modal-close" onclick="closeSkillModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="skill-name">Skill名称 <span style="color: red;">*</span></label>
<input type="text" id="skill-name" placeholder="例如: sql-injection-testing" required />
<small class="form-hint">只能包含字母、数字、连字符和下划线</small>
<label for="skill-name"><span data-i18n="skillModal.skillName">Skill名称</span> <span style="color: red;">*</span></label>
<input type="text" id="skill-name" data-i18n="skillModal.skillNamePlaceholder" data-i18n-attr="placeholder" placeholder="例如: sql-injection-testing" required />
<small class="form-hint" data-i18n="skillModal.skillNameHint">只能包含字母、数字、连字符和下划线</small>
</div>
<div class="form-group">
<label for="skill-description">描述</label>
<input type="text" id="skill-description" placeholder="Skill的简短描述" />
<label for="skill-description" data-i18n="skillModal.description">描述</label>
<input type="text" id="skill-description" data-i18n="skillModal.descriptionPlaceholder" data-i18n-attr="placeholder" placeholder="Skill的简短描述" />
</div>
<div class="form-group">
<label for="skill-content">内容(Markdown格式) <span style="color: red;">*</span></label>
<textarea id="skill-content" rows="20" placeholder="输入skill内容,支持Markdown格式..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;" required></textarea>
<small class="form-hint">支持YAML front matter格式(可选),例如:<br>
<label for="skill-content"><span data-i18n="skillModal.contentLabel">内容(Markdown格式)</span> <span style="color: red;">*</span></label>
<textarea id="skill-content" rows="20" data-i18n="skillModal.contentPlaceholder" data-i18n-attr="placeholder" placeholder="输入skill内容,支持Markdown格式..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;" required></textarea>
<small class="form-hint"><span data-i18n="skillModal.contentHint">支持YAML front matter格式(可选),例如:</span><br>
---<br>
name: skill-name<br>
description: Skill描述<br>
@@ -1714,8 +1720,8 @@ version: 1.0.0<br>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillModal()">取消</button>
<button class="btn-primary" onclick="saveSkill()">保存</button>
<button class="btn-secondary" onclick="closeSkillModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveSkill()" data-i18n="common.save">保存</button>
</div>
</div>
</div>
@@ -2094,7 +2100,7 @@ version: 1.0.0<br>
<div id="role-select-modal" class="modal">
<div class="modal-content role-select-modal-content">
<div class="modal-header">
<h2>选择角色</h2>
<h2 data-i18n="chatGroup.rolePanelTitle">选择角色</h2>
<span class="modal-close" onclick="closeRoleSelectModal()">&times;</span>
</div>
<div class="modal-body role-select-body">
@@ -2107,49 +2113,49 @@ version: 1.0.0<br>
<div id="role-modal" class="modal">
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2 id="role-modal-title">添加角色</h2>
<h2 id="role-modal-title" data-i18n="roleModal.addRole">添加角色</h2>
<span class="modal-close" onclick="closeRoleModal()">&times;</span>
</div>
<div class="modal-body">
<div class="form-group">
<label for="role-name">角色名称 <span style="color: red;">*</span></label>
<input type="text" id="role-name" placeholder="输入角色名称" required />
<label for="role-name"><span data-i18n="roleModal.roleName">角色名称</span> <span style="color: red;">*</span></label>
<input type="text" id="role-name" data-i18n="roleModal.roleNamePlaceholder" data-i18n-attr="placeholder" placeholder="输入角色名称" required />
</div>
<div class="form-group">
<label for="role-description">角色描述</label>
<input type="text" id="role-description" placeholder="输入角色描述" />
<label for="role-description" data-i18n="roleModal.roleDescription">角色描述</label>
<input type="text" id="role-description" data-i18n="roleModal.roleDescriptionPlaceholder" data-i18n-attr="placeholder" placeholder="输入角色描述" />
</div>
<div class="form-group">
<label for="role-icon">角色图标</label>
<input type="text" id="role-icon" placeholder="输入emoji图标,例如: 🏆" maxlength="10" />
<small class="form-hint">输入一个emoji作为角色的图标,将显示在角色选择器中。</small>
<label for="role-icon" data-i18n="roleModal.roleIcon">角色图标</label>
<input type="text" id="role-icon" data-i18n="roleModal.roleIconPlaceholder" data-i18n-attr="placeholder" placeholder="输入emoji图标,例如: 🏆" maxlength="10" />
<small class="form-hint" data-i18n="roleModal.roleIconHint">输入一个emoji作为角色的图标,将显示在角色选择器中。</small>
</div>
<div class="form-group">
<label for="role-user-prompt">用户提示词</label>
<textarea id="role-user-prompt" rows="10" placeholder="输入用户提示词,会在用户消息前追加此提示词..."></textarea>
<small class="form-hint">此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。</small>
<label for="role-user-prompt" data-i18n="roleModal.userPrompt">用户提示词</label>
<textarea id="role-user-prompt" rows="10" data-i18n="roleModal.userPromptPlaceholder" data-i18n-attr="placeholder" placeholder="输入用户提示词,会在用户消息前追加此提示词..."></textarea>
<small class="form-hint" data-i18n="roleModal.userPromptHint">此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。</small>
</div>
<div class="form-group" id="role-tools-section">
<label>关联的工具(可选)</label>
<label data-i18n="roleModal.relatedTools">关联的工具(可选)</label>
<div id="role-tools-default-hint" class="role-tools-default-hint" style="display: none;">
<div class="role-tools-default-info">
<span class="role-tools-default-icon"></span>
<div class="role-tools-default-content">
<div class="role-tools-default-title">默认角色使用所有工具</div>
<div class="role-tools-default-desc">默认角色会自动使用MCP管理中启用的所有工具,无需单独配置。</div>
<div class="role-tools-default-title" data-i18n="roleModal.defaultRoleToolsTitle">默认角色使用所有工具</div>
<div class="role-tools-default-desc" data-i18n="roleModal.defaultRoleToolsDesc">默认角色会自动使用MCP管理中启用的所有工具,无需单独配置。</div>
</div>
</div>
</div>
<div class="role-tools-controls">
<div class="role-tools-actions">
<button type="button" class="btn-secondary" onclick="selectAllRoleTools()">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleTools()">全不选</button>
<button type="button" class="btn-secondary" onclick="selectAllRoleTools()" data-i18n="roleModal.selectAll">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleTools()" data-i18n="roleModal.deselectAll">全不选</button>
<div class="role-tools-search-box">
<input type="text" id="role-tools-search" placeholder="搜索工具..."
<input type="text" id="role-tools-search" data-i18n="roleModal.searchToolsPlaceholder" data-i18n-attr="placeholder" placeholder="搜索工具..."
oninput="searchRoleTools(this.value)"
onkeypress="if(event.key === 'Enter') searchRoleTools(this.value)" />
<button class="role-tools-search-clear" id="role-tools-search-clear"
onclick="clearRoleToolsSearch()" style="display: none;" title="清除搜索">
onclick="clearRoleToolsSearch()" style="display: none;" data-i18n="common.clearSearch" data-i18n-attr="title" title="清除搜索">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -2160,22 +2166,22 @@ version: 1.0.0<br>
<div id="role-tools-stats" class="role-tools-stats"></div>
</div>
<div id="role-tools-list" class="role-tools-list">
<div class="tools-loading">正在加载工具列表...</div>
<div class="tools-loading" data-i18n="roleModal.loadingTools">正在加载工具列表...</div>
</div>
<small class="form-hint">勾选要关联的工具,留空则使用MCP管理中的全部工具配置。</small>
<small class="form-hint" data-i18n="roleModal.relatedToolsHint">勾选要关联的工具,留空则使用MCP管理中的全部工具配置。</small>
</div>
<div class="form-group" id="role-skills-section">
<label>关联的Skills(可选)</label>
<label data-i18n="roleModal.relatedSkills">关联的Skills(可选)</label>
<div class="role-skills-controls">
<div class="role-skills-actions">
<button type="button" class="btn-secondary" onclick="selectAllRoleSkills()">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleSkills()">全不选</button>
<button type="button" class="btn-secondary" onclick="selectAllRoleSkills()" data-i18n="roleModal.selectAll">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleSkills()" data-i18n="roleModal.deselectAll">全不选</button>
<div class="role-skills-search-box">
<input type="text" id="role-skills-search" placeholder="搜索skill..."
<input type="text" id="role-skills-search" data-i18n="roleModal.searchSkillsPlaceholder" data-i18n-attr="placeholder" placeholder="搜索skill..."
oninput="searchRoleSkills(this.value)"
onkeypress="if(event.key === 'Enter') searchRoleSkills(this.value)" />
<button class="role-skills-search-clear" id="role-skills-search-clear"
onclick="clearRoleSkillsSearch()" style="display: none;" title="清除搜索">
onclick="clearRoleSkillsSearch()" style="display: none;" data-i18n="common.clearSearch" data-i18n-attr="title" title="清除搜索">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -2186,21 +2192,21 @@ version: 1.0.0<br>
<div id="role-skills-stats" class="role-skills-stats"></div>
</div>
<div id="role-skills-list" class="role-skills-list">
<div class="skills-loading">正在加载skills列表...</div>
<div class="skills-loading" data-i18n="roleModal.loadingSkills">正在加载skills列表...</div>
</div>
<small class="form-hint">勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。</small>
<small class="form-hint" data-i18n="roleModal.relatedSkillsHint">勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="role-enabled" class="modern-checkbox" checked />
<span class="checkbox-custom"></span>
<span class="checkbox-text">启用此角色</span>
<span class="checkbox-text" data-i18n="roleModal.enableRole">启用此角色</span>
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeRoleModal()">取消</button>
<button class="btn-primary" onclick="saveRole()">保存</button>
<button class="btn-secondary" onclick="closeRoleModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveRole()" data-i18n="common.save">保存</button>
</div>
</div>
</div>