mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Compare commits
30 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dd7d15845c | |||
| ee9559e074 | |||
| 872e570518 | |||
| a5ffafba77 | |||
| 3da7f77e1c | |||
| 26ad9646be | |||
| 959a97870b | |||
| c8bbfcd171 | |||
| 5f2862b629 | |||
| ee6c4b6f19 | |||
| 55b8decbaa | |||
| 1222adc485 | |||
| 38972bf93b | |||
| 127a5dd5c3 | |||
| f5f73d41c0 | |||
| 9811209002 | |||
| f44bb42842 | |||
| d2e751e3d3 | |||
| a5c285c8f3 | |||
| 98938aef00 | |||
| 71f6a97a90 | |||
| 2fce15f82a | |||
| 52b70d8b16 | |||
| 5b3709b9ad | |||
| 639f65602d | |||
| 52b6c3fe1b | |||
| f26ee8e6e7 | |||
| 379486d36c | |||
| 317461e259 | |||
| b7e724407b |
@@ -1,78 +0,0 @@
|
||||
---
|
||||
name: 🐛 Bug / 异常问题反馈
|
||||
about: 报告一个 Bug 或异常问题
|
||||
title: '[BUG] '
|
||||
labels: ['bug', '待确认']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 📋 问题描述
|
||||
<!-- 请清晰、简洁地描述遇到的问题 -->
|
||||
|
||||
|
||||
## 🔄 复现步骤
|
||||
<!-- 请详细描述如何复现这个问题 -->
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
|
||||
## ✅ 期望行为
|
||||
<!-- 描述你期望的正确行为是什么 -->
|
||||
|
||||
|
||||
## ❌ 实际行为
|
||||
<!-- 描述实际发生了什么 -->
|
||||
|
||||
|
||||
## 📸 截图/录屏
|
||||
<!--
|
||||
⚠️ 重要:请提供完整的截图或录屏,确保包含:
|
||||
- 完整的错误信息
|
||||
- 相关的界面元素
|
||||
- 浏览器控制台错误(如有)
|
||||
- 终端输出(如有)
|
||||
|
||||
如果截图不完整,issue 可能会被关闭。
|
||||
-->
|
||||
|
||||
<!-- 请在此处拖拽或粘贴截图 -->
|
||||
|
||||
|
||||
## 📝 报错日志(脱敏后)
|
||||
<!--
|
||||
⚠️ 重要:请提供完整的、脱敏后的报错日志。
|
||||
|
||||
脱敏要求:
|
||||
- 移除所有敏感信息(API Key、密码、Token、真实IP地址、域名等)
|
||||
- 使用占位符替换,如:`sk-xxx`、`password: ***`、`192.168.x.x`、`example.com`
|
||||
- 保留完整的错误堆栈信息
|
||||
- 保留时间戳和日志级别
|
||||
|
||||
请从以下位置收集日志:
|
||||
1. MCP状态监控 页面
|
||||
2. 服务器终端输出
|
||||
3. 日志文件(如果配置了文件输出)
|
||||
4. 浏览器控制台(F12 → Console)
|
||||
-->
|
||||
|
||||
```
|
||||
请在此处粘贴脱敏后的完整报错日志
|
||||
```
|
||||
|
||||
|
||||
## ✅ 检查清单
|
||||
<!-- 提交前请确认以下项目 -->
|
||||
|
||||
- [ ] 我已阅读并理解项目的 Issue 规范
|
||||
- [ ] 我已提供完整的、脱敏后的报错日志
|
||||
- [ ] 我已提供完整的截图(如适用)
|
||||
- [ ] 我已提供详细的复现步骤
|
||||
- [ ] 我已填写所有必要的环境信息
|
||||
- [ ] 我已脱敏所有敏感信息(API Key、密码、IP 等)
|
||||
- [ ] 我已确认这不是重复的 issue
|
||||
|
||||
---
|
||||
|
||||
**注意**:如果缺少必要的日志或截图,此 issue 可能会被标记为 `需要更多信息` 或直接关闭。请确保提供完整的信息以便我们能够快速定位和解决问题。
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
name: ✨ 功能优化建议
|
||||
about: 提出新功能或优化建议
|
||||
title: '[FEATURE] '
|
||||
labels: ['enhancement', '待讨论']
|
||||
assignees: ''
|
||||
---
|
||||
|
||||
## 💡 功能描述
|
||||
<!-- 请清晰、简洁地描述你希望添加或优化的功能 -->
|
||||
|
||||
|
||||
## 🎯 使用场景
|
||||
<!-- 描述这个功能的使用场景,解决什么问题 -->
|
||||
<!-- 例如:在什么情况下会用到这个功能?它如何改善用户体验? -->
|
||||
|
||||
|
||||
## 🔄 当前行为
|
||||
<!-- 描述当前系统是如何处理相关需求的,或者为什么需要这个功能 -->
|
||||
|
||||
|
||||
## ✨ 期望行为
|
||||
<!-- 详细描述你期望的新功能或优化后的行为 -->
|
||||
|
||||
|
||||
## 📸 参考示例(如有)
|
||||
<!--
|
||||
如果有其他项目的类似功能实现,可以在此提供截图或链接作为参考
|
||||
⚠️ 请确保截图完整,包含所有相关界面元素
|
||||
-->
|
||||
|
||||
<!-- 请在此处拖拽或粘贴参考截图 -->
|
||||
|
||||
|
||||
## 🛠️ 实现建议(可选)
|
||||
<!-- 如果你有具体的实现思路或技术建议,可以在此描述 -->
|
||||
|
||||
|
||||
## 📊 优先级评估
|
||||
<!-- 请选择你认为的优先级 -->
|
||||
- [ ] 🔴 高优先级(严重影响使用体验或功能缺失)
|
||||
- [ ] 🟡 中优先级(能显著改善体验)
|
||||
- [ ] 🟢 低优先级(锦上添花的功能)
|
||||
|
||||
## 🔍 相关功能
|
||||
<!-- 这个功能是否与现有功能相关? -->
|
||||
<!-- 例如:是否与工具管理、攻击链分析、知识库等功能相关? -->
|
||||
|
||||
|
||||
## 📝 额外信息
|
||||
<!-- 任何其他有助于理解需求的信息 -->
|
||||
- 是否已有替代方案?
|
||||
- 这个功能是否会影响现有功能?
|
||||
- 是否有相关的其他 issue 或讨论?
|
||||
|
||||
## ✅ 检查清单
|
||||
<!-- 提交前请确认以下项目 -->
|
||||
|
||||
- [ ] 我已清晰描述了功能需求和使用场景
|
||||
- [ ] 我已提供完整的参考截图(如有)
|
||||
- [ ] 我已评估了功能的优先级
|
||||
- [ ] 我已确认这不是重复的 issue
|
||||
- [ ] 我已考虑了对现有功能的影响
|
||||
|
||||
---
|
||||
|
||||
**注意**:请提供尽可能详细的信息,包括使用场景、参考示例等,这将有助于我们更好地理解和实现你的需求。
|
||||
|
||||
@@ -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 project’s `.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
@@ -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"
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -4411,6 +4411,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
enrichSpecWithI18nKeys(spec)
|
||||
c.JSON(http.StatusOK, spec)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -1,5 +1,8 @@
|
||||
name: "dalfox"
|
||||
command: "dalfox"
|
||||
# dalfox v2+ 使用子命令,单目标模式为 `dalfox url <target>`,不再支持根级的 -u
|
||||
args:
|
||||
- "url"
|
||||
enabled: true
|
||||
short_description: "高级XSS漏洞扫描器"
|
||||
description: |
|
||||
@@ -19,10 +22,12 @@ description: |
|
||||
parameters:
|
||||
- name: "url"
|
||||
type: "string"
|
||||
description: "目标URL"
|
||||
description: |
|
||||
目标URL。dalfox 单目标模式为子命令 url,此处作为 url 后的第一个位置参数传入。
|
||||
示例等价 CLI:dalfox url "http://target/page?q=test"
|
||||
required: true
|
||||
flag: "-u"
|
||||
format: "flag"
|
||||
position: 0
|
||||
format: "positional"
|
||||
- name: "pipe_mode"
|
||||
type: "bool"
|
||||
description: "使用管道模式输入"
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
name: "nmap-advanced"
|
||||
command: "nmap"
|
||||
enabled: true
|
||||
short_description: "高级Nmap扫描,支持自定义NSE脚本和优化时序"
|
||||
description: |
|
||||
高级Nmap扫描工具,支持自定义NSE脚本、优化时序和多种扫描技术。
|
||||
|
||||
**主要功能:**
|
||||
- 多种扫描技术(SYN, TCP, UDP等)
|
||||
- 自定义NSE脚本
|
||||
- 时序优化
|
||||
- OS检测和版本检测
|
||||
|
||||
**使用场景:**
|
||||
- 高级网络扫描
|
||||
- 深度安全评估
|
||||
- 渗透测试
|
||||
- 网络侦察
|
||||
parameters:
|
||||
- name: "target"
|
||||
type: "string"
|
||||
description: "目标IP地址或主机名"
|
||||
required: true
|
||||
position: 0
|
||||
format: "positional"
|
||||
- name: "scan_type"
|
||||
type: "string"
|
||||
description: "扫描类型(-sS, -sT, -sU等)"
|
||||
required: false
|
||||
format: "template"
|
||||
template: "{value}"
|
||||
default: "-sS"
|
||||
- name: "ports"
|
||||
type: "string"
|
||||
description: "要扫描的端口"
|
||||
required: false
|
||||
flag: "-p"
|
||||
format: "flag"
|
||||
- name: "timing"
|
||||
type: "string"
|
||||
description: "时序模板(T0-T5)"
|
||||
required: false
|
||||
format: "template"
|
||||
template: "-T{value}"
|
||||
default: "4"
|
||||
- name: "nse_scripts"
|
||||
type: "string"
|
||||
description: "要运行的自定义NSE脚本"
|
||||
required: false
|
||||
flag: "--script"
|
||||
format: "flag"
|
||||
- name: "os_detection"
|
||||
type: "bool"
|
||||
description: "启用OS检测"
|
||||
required: false
|
||||
flag: "-O"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "version_detection"
|
||||
type: "bool"
|
||||
description: "启用版本检测"
|
||||
required: false
|
||||
flag: "-sV"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "aggressive"
|
||||
type: "bool"
|
||||
description: "启用激进扫描"
|
||||
required: false
|
||||
flag: "-A"
|
||||
format: "flag"
|
||||
default: false
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: |
|
||||
额外的nmap-advanced参数。用于传递未在参数列表中定义的nmap-advanced选项。
|
||||
|
||||
**示例值:**
|
||||
- 根据工具特性添加常用参数示例
|
||||
|
||||
**注意事项:**
|
||||
- 多个参数用空格分隔
|
||||
- 确保参数格式正确,避免命令注入
|
||||
- 此参数会直接追加到命令末尾
|
||||
required: false
|
||||
format: "positional"
|
||||
+53
-76
@@ -1,108 +1,85 @@
|
||||
name: "nmap"
|
||||
command: "nmap"
|
||||
args: ["-sT", "-sV", "-sC"] # 固定参数:TCP连接扫描、版本检测、默认脚本
|
||||
# 默认:TCP 连接扫描 + 版本检测 + 默认 NSE 脚本(无 root 也可用)
|
||||
args: ["-sT", "-sV", "-sC"]
|
||||
enabled: true
|
||||
# 简短描述(用于工具列表,减少token消耗)- 一句话说明工具用途
|
||||
short_description: "网络扫描工具,用于发现网络主机、开放端口和服务"
|
||||
# 工具详细描述 - 帮助大模型理解工具用途和使用场景
|
||||
short_description: "网络扫描:端口/服务/脚本;可选时序、自定义 NSE、OS 检测(需 root)"
|
||||
description: |
|
||||
网络映射和端口扫描工具,用于发现网络中的主机、服务和开放端口。
|
||||
网络映射与端口扫描,合并了原「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
@@ -1,58 +1,152 @@
|
||||
name: "rustscan"
|
||||
command: "rustscan"
|
||||
enabled: true
|
||||
short_description: "超快速端口扫描工具,使用Rust编写"
|
||||
short_description: "超快速端口扫描(Rust);可选 greppable、批量与脚本级别"
|
||||
description: |
|
||||
Rustscan是一个用Rust编写的超快速端口扫描工具,可以快速扫描大量端口。
|
||||
RustScan 2.x:快速端口发现,可选再调 Nmap 脚本。
|
||||
|
||||
**主要功能:**
|
||||
- 超快速端口扫描
|
||||
- 可配置的扫描速度
|
||||
- 支持Nmap脚本集成
|
||||
- 批量扫描支持
|
||||
**与 `rustscan -h` 对应关系:**
|
||||
- `-a` / `--addresses`:扫描目标列表(逗号分隔或文件)
|
||||
- `-p`:逗号分隔端口;`-r`:端口范围 `start-end`(二选一或与 `-p` 配合以 CLI 为准)
|
||||
- `-g` / `--greppable`:只输出端口,便于 grep/管道
|
||||
- `--scripts`:**官方默认是 default**(会跑 Nmap);设为 **none** 可只做端口发现、更快
|
||||
- `-b` batch-size、`-t` timeout、`--scan-order` 等可微调速度与顺序
|
||||
|
||||
**使用场景:**
|
||||
- 快速端口扫描
|
||||
- 大规模网络扫描
|
||||
- 渗透测试信息收集
|
||||
**使用建议:**
|
||||
- 快速扫端口、不要 Nmap:`scripts` 用 `none`,必要时加 `-g`
|
||||
- 需要服务识别/脚本:用 `default` 或 `custom`,并确保本机有 nmap
|
||||
parameters:
|
||||
# -a, --addresses
|
||||
- name: "target"
|
||||
type: "string"
|
||||
description: "目标IP地址或主机名"
|
||||
description: |
|
||||
对应 `-a`:逗号分隔的 CIDR、IP 或主机名;也可为含换行/列表的文件路径。
|
||||
示例:`192.168.1.1`、`10.0.0.1,10.0.0.2`、`192.168.0.0/24`
|
||||
required: true
|
||||
flag: "-a"
|
||||
format: "flag"
|
||||
|
||||
# -p, --ports;范围请用 range (-r),勿把 1-1000 填进 ports
|
||||
- name: "ports"
|
||||
type: "string"
|
||||
description: "要扫描的端口(如:22,80,443或1-1000)"
|
||||
description: |
|
||||
要扫描的端口,**仅**逗号分隔列表,对应 `-p`。
|
||||
示例:`22,80,443`、`80,443,8080`。
|
||||
若要范围如 `1-1000`,请用参数 **range**(`-r`),不要写在本参数。
|
||||
required: false
|
||||
flag: "-p"
|
||||
format: "flag"
|
||||
|
||||
# -r, --range(与 ports 列表二选一或按官方说明组合)
|
||||
- name: "range"
|
||||
type: "string"
|
||||
description: |
|
||||
端口范围,格式 start-end,对应 `-r`。
|
||||
示例:`1-1000`、`1-65535`(全端口很慢)。
|
||||
离散端口如 `22,80,443` 请用 **ports**(`-p`),不要写在本参数。
|
||||
required: false
|
||||
flag: "-r"
|
||||
format: "flag"
|
||||
|
||||
# -u, --ulimit
|
||||
- name: "ulimit"
|
||||
type: "int"
|
||||
description: "文件描述符限制"
|
||||
description: "提升扫描用的 ulimit;依系统 fd 限制调整"
|
||||
required: false
|
||||
flag: "-u"
|
||||
format: "flag"
|
||||
default: 5000
|
||||
|
||||
# --scripts:none | default | custom(CLI 默认 default)
|
||||
- name: "scripts"
|
||||
type: "bool"
|
||||
description: "在发现的端口上运行Nmap脚本"
|
||||
type: "string"
|
||||
description: |
|
||||
脚本级别;**必须**传字符串,勿传 true/false。
|
||||
- **none**:不跑 Nmap,仅端口发现(最快)
|
||||
- **default**:与 rustscan 官方默认一致,会调 Nmap
|
||||
- **custom**:自定义脚本,常需配合 additional_args
|
||||
required: false
|
||||
flag: "--scripts"
|
||||
format: "flag"
|
||||
default: "none"
|
||||
options:
|
||||
- "none"
|
||||
- "default"
|
||||
- "custom"
|
||||
|
||||
# -g, --greppable:仅端口列表,无 Nmap 输出
|
||||
- name: "greppable"
|
||||
type: "bool"
|
||||
description: "Greppable 模式:只输出端口,适合脚本解析或写入文件"
|
||||
required: false
|
||||
flag: "-g"
|
||||
format: "flag"
|
||||
default: false
|
||||
|
||||
# -b, --batch-size [default: 4500]
|
||||
- name: "batch_size"
|
||||
type: "int"
|
||||
description: "每批并发端口数;越大越快,受 OS 打开文件数限制。官方默认 4500"
|
||||
required: false
|
||||
flag: "-b"
|
||||
format: "flag"
|
||||
|
||||
# -t, --timeout ms [default: 1500]
|
||||
- name: "timeout_ms"
|
||||
type: "int"
|
||||
description: "单端口超时(毫秒)。官方默认 1500"
|
||||
required: false
|
||||
flag: "-t"
|
||||
format: "flag"
|
||||
|
||||
# --scan-order serial | random [default: serial]
|
||||
- name: "scan_order"
|
||||
type: "string"
|
||||
description: "扫描顺序:serial 升序;random 随机"
|
||||
required: false
|
||||
flag: "--scan-order"
|
||||
format: "flag"
|
||||
options:
|
||||
- "serial"
|
||||
- "random"
|
||||
|
||||
# --top:top 1000 端口
|
||||
- name: "top_ports"
|
||||
type: "bool"
|
||||
description: "使用内置 top 1000 端口(等价于传 `--top`)"
|
||||
required: false
|
||||
flag: "--top"
|
||||
format: "flag"
|
||||
default: false
|
||||
|
||||
# -e, --exclude-ports
|
||||
- name: "exclude_ports"
|
||||
type: "string"
|
||||
description: "排除端口,逗号分隔。示例:`80,443`"
|
||||
required: false
|
||||
flag: "-e"
|
||||
format: "flag"
|
||||
|
||||
# -x, --exclude-addresses
|
||||
- name: "exclude_addresses"
|
||||
type: "string"
|
||||
description: "排除地址,逗号分隔 CIDR/IP/主机"
|
||||
required: false
|
||||
flag: "-x"
|
||||
format: "flag"
|
||||
|
||||
# --tries [default: 1]
|
||||
- name: "tries"
|
||||
type: "int"
|
||||
description: "判定关闭前的重试次数;0 会被纠正为 1"
|
||||
required: false
|
||||
flag: "--tries"
|
||||
format: "flag"
|
||||
|
||||
- name: "additional_args"
|
||||
type: "string"
|
||||
description: |
|
||||
额外的rustscan参数。用于传递未在参数列表中定义的rustscan选项。
|
||||
|
||||
**示例值:**
|
||||
- 根据工具特性添加常用参数示例
|
||||
|
||||
**注意事项:**
|
||||
- 多个参数用空格分隔
|
||||
- 确保参数格式正确,避免命令注入
|
||||
- 此参数会直接追加到命令末尾
|
||||
未列在上面的选项可写在这里,空格分隔。
|
||||
示例:`--no-banner`、`--udp`、`-n`(忽略配置文件)、`-c /path/config.toml`、`--resolver 8.8.8.8`
|
||||
required: false
|
||||
format: "positional"
|
||||
|
||||
@@ -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
@@ -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
@@ -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": "导出当前结果为 CSV(UTF-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
@@ -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
@@ -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
@@ -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',
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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}" 需要指定command(stdio模式)或url(http/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
@@ -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()">×</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
@@ -26,7 +26,33 @@
|
||||
return terminals[0] || null;
|
||||
}
|
||||
|
||||
var WELCOME_LINE = 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏\r\n';
|
||||
function tr(key, opts) {
|
||||
if (typeof window !== 'undefined' && typeof window.t === 'function') {
|
||||
return window.t(key, opts);
|
||||
}
|
||||
// i18n 未就绪时的后备(与 zh-CN 一致)
|
||||
var fallbacks = {
|
||||
'settingsTerminal.welcomeLine': 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏',
|
||||
'settingsTerminal.sessionClosed': '[会话已关闭]',
|
||||
'settingsTerminal.connectionError': '[终端连接出错]',
|
||||
'settingsTerminal.connectFailed': '[无法连接终端服务: {{msg}}]',
|
||||
'settingsTerminal.closeTabTitle': '关闭',
|
||||
'settingsTerminal.containerClickTitle': '点击此处后输入命令',
|
||||
'settingsTerminal.xtermNotLoaded': '未加载 xterm.js,请刷新页面或检查网络。',
|
||||
'settingsTerminal.terminalTab': '终端 {{n}}'
|
||||
};
|
||||
var s = fallbacks[key] || key;
|
||||
if (opts && typeof opts === 'object') {
|
||||
Object.keys(opts).forEach(function (k) {
|
||||
s = s.split('{{' + k + '}}').join(String(opts[k]));
|
||||
});
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function getWelcomeLine() {
|
||||
return tr('settingsTerminal.welcomeLine') + '\r\n';
|
||||
}
|
||||
|
||||
function writePrompt(tab) {
|
||||
// 提示符交由后端 Shell 自行输出,这里仅保留占位函数,避免旧代码报错
|
||||
@@ -35,7 +61,7 @@
|
||||
function redrawTabDisplay(t) {
|
||||
if (!t || !t.term) return;
|
||||
t.term.clear();
|
||||
t.term.write(WELCOME_LINE);
|
||||
t.term.write(getWelcomeLine());
|
||||
}
|
||||
|
||||
function writeln(tabOrS, s) {
|
||||
@@ -121,19 +147,19 @@
|
||||
ws.onclose = function () {
|
||||
tab.running = false;
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[2m[会话已关闭]\x1b[0m');
|
||||
tab.term.writeln('\r\n\x1b[2m' + tr('settingsTerminal.sessionClosed') + '\x1b[0m');
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = function () {
|
||||
tab.running = false;
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[31m[终端连接出错]\x1b[0m');
|
||||
tab.term.writeln('\r\n\x1b[31m' + tr('settingsTerminal.connectionError') + '\x1b[0m');
|
||||
}
|
||||
};
|
||||
} catch (e) {
|
||||
if (tab.term) {
|
||||
tab.term.writeln('\r\n\x1b[31m[无法连接终端服务: ' + String(e) + ']\x1b[0m');
|
||||
tab.term.writeln('\r\n\x1b[31m' + tr('settingsTerminal.connectFailed', { msg: String(e) }) + '\x1b[0m');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -182,13 +208,13 @@
|
||||
term.loadAddon(fitAddon);
|
||||
}
|
||||
term.open(container);
|
||||
term.write(WELCOME_LINE);
|
||||
term.write(getWelcomeLine());
|
||||
container.addEventListener('click', function () {
|
||||
switchTerminalTab(tab.id);
|
||||
if (term) term.focus();
|
||||
});
|
||||
container.setAttribute('tabindex', '0');
|
||||
container.title = '点击此处后输入命令';
|
||||
container.title = tr('settingsTerminal.containerClickTitle');
|
||||
|
||||
function sendToWS(data) {
|
||||
ensureTerminalWS(tab);
|
||||
@@ -211,6 +237,9 @@
|
||||
|
||||
tab.term = term;
|
||||
tab.fitAddon = fitAddon;
|
||||
// 立即建立 WebSocket,让后端 PTY/Shell 马上启动并输出提示符;
|
||||
// 若等到首次按键才 connect,用户会感觉必须先按回车才能输入(实为连接尚未建立)。
|
||||
ensureTerminalWS(tab);
|
||||
return term;
|
||||
}
|
||||
|
||||
@@ -253,12 +282,12 @@
|
||||
tabDiv.setAttribute('data-tab-id', String(id));
|
||||
var label = document.createElement('span');
|
||||
label.className = 'terminal-tab-label';
|
||||
label.textContent = '终端 ' + id;
|
||||
label.textContent = tr('settingsTerminal.terminalTab', { n: id });
|
||||
label.onclick = function () { switchTerminalTab(id); };
|
||||
var closeBtn = document.createElement('button');
|
||||
closeBtn.type = 'button';
|
||||
closeBtn.className = 'terminal-tab-close';
|
||||
closeBtn.title = '关闭';
|
||||
closeBtn.title = tr('settingsTerminal.closeTabTitle');
|
||||
closeBtn.textContent = '×';
|
||||
closeBtn.onclick = function (e) { e.stopPropagation(); removeTerminalTab(id); };
|
||||
tabDiv.appendChild(label);
|
||||
@@ -340,7 +369,7 @@
|
||||
var t = terminals[i];
|
||||
tabDivs[i].setAttribute('data-tab-id', String(t.id));
|
||||
var lbl = tabDivs[i].querySelector('.terminal-tab-label');
|
||||
if (lbl) lbl.textContent = '终端 ' + t.id;
|
||||
if (lbl) lbl.textContent = tr('settingsTerminal.terminalTab', { n: t.id });
|
||||
if (lbl) lbl.onclick = (function (tid) { return function () { switchTerminalTab(tid); }; })(t.id);
|
||||
var cb = tabDivs[i].querySelector('.terminal-tab-close');
|
||||
if (cb) cb.onclick = (function (tid) { return function (e) { e.stopPropagation(); removeTerminalTab(tid); }; })(t.id);
|
||||
@@ -364,6 +393,40 @@
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function refreshTerminalI18n() {
|
||||
// 语言切换后更新标签与容器 title;已打开的终端内容不强制清屏,以免丢失会话输出
|
||||
try {
|
||||
var tabsEl = document.querySelector('.terminal-tabs');
|
||||
if (tabsEl) {
|
||||
var tabDivs = tabsEl.querySelectorAll('.terminal-tab');
|
||||
for (var i = 0; i < tabDivs.length && i < terminals.length; i++) {
|
||||
var tid = terminals[i].id;
|
||||
var lbl = tabDivs[i].querySelector('.terminal-tab-label');
|
||||
if (lbl) lbl.textContent = tr('settingsTerminal.terminalTab', { n: tid });
|
||||
var cb = tabDivs[i].querySelector('.terminal-tab-close');
|
||||
if (cb) cb.title = tr('settingsTerminal.closeTabTitle');
|
||||
}
|
||||
}
|
||||
terminals.forEach(function (tab) {
|
||||
if (!tab || !tab.term) return;
|
||||
var cont = document.getElementById(tab.containerId);
|
||||
if (cont) cont.title = tr('settingsTerminal.containerClickTitle');
|
||||
});
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
refreshTerminalI18n();
|
||||
});
|
||||
|
||||
function initTerminal() {
|
||||
var pane1 = document.getElementById('terminal-pane-1');
|
||||
var container1 = document.getElementById('terminal-container-1');
|
||||
@@ -377,7 +440,7 @@
|
||||
inited = true;
|
||||
|
||||
if (typeof Terminal === 'undefined') {
|
||||
container1.innerHTML = '<p class="terminal-error">未加载 xterm.js,请刷新页面或检查网络。</p>';
|
||||
container1.innerHTML = '<p class="terminal-error">' + escapeHtml(tr('settingsTerminal.xtermNotLoaded')) + '</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -388,6 +451,8 @@
|
||||
|
||||
updateTerminalTabCloseVisibility();
|
||||
|
||||
refreshTerminalI18n();
|
||||
|
||||
setTimeout(function () {
|
||||
try { if (tab.fitAddon) tab.fitAddon.fit(); if (tab.term) tab.term.focus(); } catch (e) {}
|
||||
}, 100);
|
||||
|
||||
+30
-15
@@ -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
@@ -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="Apache" && country="CN"')" data-i18n="infoCollectPage.presetApache" data-i18n-attr="title" title="填入示例">Apache + 中国</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('title="登录" && country="CN"')" data-i18n="infoCollectPage.presetLogin" data-i18n-attr="title" title="填入示例">登录页 + 中国</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('domain="example.com"')" data-i18n="infoCollectPage.presetDomain" data-i18n-attr="title" title="填入示例">指定域名</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('ip="1.1.1.1"')" 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="Apache" && country="CN"')" 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="登录" && country="CN"')" data-i18n="infoCollectPage.presetLogin" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">登录页 + 中国</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('domain="example.com"')" data-i18n="infoCollectPage.presetDomain" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">指定域名</button>
|
||||
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('ip="1.1.1.1"')" 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="导出当前结果为 CSV(UTF-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>切换 <ID></code> <code>switch <ID></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>角色 <名></code> <code>role <name></code> — 切换当前角色 | Switch role</li>
|
||||
<li><code>删除 <ID></code> <code>delete <ID></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>切换 <ID></code> <code>switch <ID></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>角色 <名></code> <code>role <name></code> — <span data-i18n="settingsRobotsExtra.botCmdRole">切换当前角色 | Switch role</span></li>
|
||||
<li><code>删除 <ID></code> <code>delete <ID></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()">×</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()">×</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()">×</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()">×</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>
|
||||
|
||||
Reference in New Issue
Block a user