Compare commits

...

13 Commits

Author SHA1 Message Date
公明 dd7d15845c Update config.yaml 2026-03-12 21:17:57 +08:00
公明 ee9559e074 Add files via upload 2026-03-12 21:17:22 +08:00
公明 872e570518 Add files via upload 2026-03-12 20:54:09 +08:00
公明 a5ffafba77 Delete .github directory 2026-03-12 17:33:37 +08:00
公明 3da7f77e1c Update zh-CN.json 2026-03-12 15:40:49 +08:00
公明 26ad9646be Update en-US.json 2026-03-12 15:40:00 +08:00
公明 959a97870b Update en-US.json 2026-03-12 13:51:42 +08:00
公明 c8bbfcd171 Update zh-CN.json 2026-03-12 13:51:08 +08:00
公明 5f2862b629 Delete tools/nmap-advanced.yaml 2026-03-11 21:13:12 +08:00
公明 ee6c4b6f19 Add files via upload 2026-03-11 21:12:36 +08:00
公明 55b8decbaa Add files via upload 2026-03-11 21:01:45 +08:00
公明 1222adc485 Add files via upload 2026-03-11 20:02:21 +08:00
公明 38972bf93b Add files via upload 2026-03-11 19:47:43 +08:00
19 changed files with 745 additions and 417 deletions
-78
View File
@@ -1,78 +0,0 @@
---
name: 🐛 Bug / 异常问题反馈
about: 报告一个 Bug 或异常问题
title: '[BUG] '
labels: ['bug', '待确认']
assignees: ''
---
## 📋 问题描述
<!-- 请清晰、简洁地描述遇到的问题 -->
## 🔄 复现步骤
<!-- 请详细描述如何复现这个问题 -->
1.
2.
3.
4.
## ✅ 期望行为
<!-- 描述你期望的正确行为是什么 -->
## ❌ 实际行为
<!-- 描述实际发生了什么 -->
## 📸 截图/录屏
<!--
⚠️ 重要:请提供完整的截图或录屏,确保包含:
- 完整的错误信息
- 相关的界面元素
- 浏览器控制台错误(如有)
- 终端输出(如有)
如果截图不完整,issue 可能会被关闭。
-->
<!-- 请在此处拖拽或粘贴截图 -->
## 📝 报错日志(脱敏后)
<!--
⚠️ 重要:请提供完整的、脱敏后的报错日志。
脱敏要求:
- 移除所有敏感信息(API Key、密码、Token、真实IP地址、域名等)
- 使用占位符替换,如:`sk-xxx``password: ***``192.168.x.x``example.com`
- 保留完整的错误堆栈信息
- 保留时间戳和日志级别
请从以下位置收集日志:
1. MCP状态监控 页面
2. 服务器终端输出
3. 日志文件(如果配置了文件输出)
4. 浏览器控制台(F12 → Console
-->
```
请在此处粘贴脱敏后的完整报错日志
```
## ✅ 检查清单
<!-- 提交前请确认以下项目 -->
- [ ] 我已阅读并理解项目的 Issue 规范
- [ ] 我已提供完整的、脱敏后的报错日志
- [ ] 我已提供完整的截图(如适用)
- [ ] 我已提供详细的复现步骤
- [ ] 我已填写所有必要的环境信息
- [ ] 我已脱敏所有敏感信息(API Key、密码、IP 等)
- [ ] 我已确认这不是重复的 issue
---
**注意**:如果缺少必要的日志或截图,此 issue 可能会被标记为 `需要更多信息` 或直接关闭。请确保提供完整的信息以便我们能够快速定位和解决问题。
-68
View File
@@ -1,68 +0,0 @@
---
name: ✨ 功能优化建议
about: 提出新功能或优化建议
title: '[FEATURE] '
labels: ['enhancement', '待讨论']
assignees: ''
---
## 💡 功能描述
<!-- 请清晰、简洁地描述你希望添加或优化的功能 -->
## 🎯 使用场景
<!-- 描述这个功能的使用场景,解决什么问题 -->
<!-- 例如:在什么情况下会用到这个功能?它如何改善用户体验? -->
## 🔄 当前行为
<!-- 描述当前系统是如何处理相关需求的,或者为什么需要这个功能 -->
## ✨ 期望行为
<!-- 详细描述你期望的新功能或优化后的行为 -->
## 📸 参考示例(如有)
<!--
如果有其他项目的类似功能实现,可以在此提供截图或链接作为参考
⚠️ 请确保截图完整,包含所有相关界面元素
-->
<!-- 请在此处拖拽或粘贴参考截图 -->
## 🛠️ 实现建议(可选)
<!-- 如果你有具体的实现思路或技术建议,可以在此描述 -->
## 📊 优先级评估
<!-- 请选择你认为的优先级 -->
- [ ] 🔴 高优先级(严重影响使用体验或功能缺失)
- [ ] 🟡 中优先级(能显著改善体验)
- [ ] 🟢 低优先级(锦上添花的功能)
## 🔍 相关功能
<!-- 这个功能是否与现有功能相关? -->
<!-- 例如:是否与工具管理、攻击链分析、知识库等功能相关? -->
## 📝 额外信息
<!-- 任何其他有助于理解需求的信息 -->
- 是否已有替代方案?
- 这个功能是否会影响现有功能?
- 是否有相关的其他 issue 或讨论?
## ✅ 检查清单
<!-- 提交前请确认以下项目 -->
- [ ] 我已清晰描述了功能需求和使用场景
- [ ] 我已提供完整的参考截图(如有)
- [ ] 我已评估了功能的优先级
- [ ] 我已确认这不是重复的 issue
- [ ] 我已考虑了对现有功能的影响
---
**注意**:请提供尽可能详细的信息,包括使用场景、参考示例等,这将有助于我们更好地理解和实现你的需求。
+1 -1
View File
@@ -10,7 +10,7 @@
# ============================================
# 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.3.23"
version: "v1.3.24"
# 服务器配置
server:
+8 -3
View File
@@ -1,5 +1,8 @@
name: "dalfox"
command: "dalfox"
# dalfox v2+ 使用子命令,单目标模式为 `dalfox url <target>`,不再支持根级的 -u
args:
- "url"
enabled: true
short_description: "高级XSS漏洞扫描器"
description: |
@@ -19,10 +22,12 @@ description: |
parameters:
- name: "url"
type: "string"
description: "目标URL"
description: |
目标URL。dalfox 单目标模式为子命令 url,此处作为 url 后的第一个位置参数传入。
示例等价 CLIdalfox url "http://target/page?q=test"
required: true
flag: "-u"
format: "flag"
position: 0
format: "positional"
- name: "pipe_mode"
type: "bool"
description: "使用管道模式输入"
-86
View File
@@ -1,86 +0,0 @@
name: "nmap-advanced"
command: "nmap"
enabled: true
short_description: "高级Nmap扫描,支持自定义NSE脚本和优化时序"
description: |
高级Nmap扫描工具,支持自定义NSE脚本、优化时序和多种扫描技术。
**主要功能:**
- 多种扫描技术(SYN, TCP, UDP等)
- 自定义NSE脚本
- 时序优化
- OS检测和版本检测
**使用场景:**
- 高级网络扫描
- 深度安全评估
- 渗透测试
- 网络侦察
parameters:
- name: "target"
type: "string"
description: "目标IP地址或主机名"
required: true
position: 0
format: "positional"
- name: "scan_type"
type: "string"
description: "扫描类型(-sS, -sT, -sU等)"
required: false
format: "template"
template: "{value}"
default: "-sS"
- name: "ports"
type: "string"
description: "要扫描的端口"
required: false
flag: "-p"
format: "flag"
- name: "timing"
type: "string"
description: "时序模板(T0-T5"
required: false
format: "template"
template: "-T{value}"
default: "4"
- name: "nse_scripts"
type: "string"
description: "要运行的自定义NSE脚本"
required: false
flag: "--script"
format: "flag"
- name: "os_detection"
type: "bool"
description: "启用OS检测"
required: false
flag: "-O"
format: "flag"
default: false
- name: "version_detection"
type: "bool"
description: "启用版本检测"
required: false
flag: "-sV"
format: "flag"
default: false
- name: "aggressive"
type: "bool"
description: "启用激进扫描"
required: false
flag: "-A"
format: "flag"
default: false
- name: "additional_args"
type: "string"
description: |
额外的nmap-advanced参数。用于传递未在参数列表中定义的nmap-advanced选项。
**示例值:**
- 根据工具特性添加常用参数示例
**注意事项:**
- 多个参数用空格分隔
- 确保参数格式正确,避免命令注入
- 此参数会直接追加到命令末尾
required: false
format: "positional"
+53 -76
View File
@@ -1,108 +1,85 @@
name: "nmap"
command: "nmap"
args: ["-sT", "-sV", "-sC"] # 固定参数TCP连接扫描版本检测、默认脚本
# 默认TCP 连接扫描 + 版本检测 + 默认 NSE 脚本(无 root 也可用)
args: ["-sT", "-sV", "-sC"]
enabled: true
# 简短描述(用于工具列表,减少token消耗)- 一句话说明工具用途
short_description: "网络扫描工具,用于发现网络主机、开放端口和服务"
# 工具详细描述 - 帮助大模型理解工具用途和使用场景
short_description: "网络扫描:端口/服务/脚本;可选时序、自定义 NSE、OS 检测(需 root"
description: |
网络映射端口扫描工具,用于发现网络中的主机、服务和开放端口
网络映射端口扫描,合并了原「nmap」与「nmap-advanced」的能力
**主要功能**
- 主机发现:检测网络中的活动主机
- 端口扫描:识别目标主机上开放的端口
- 服务识别:检测运行在端口上的服务类型和版
- 操作系统检测:识别目标主机的操作系统类型
- 漏洞检测:使用NSE脚本检测常见漏洞
**默认行为(只传 target/ports 即可)**
- `-sT` TCP 连接扫描(无需 root
- `-sV` 版本检测
- `-sC` 默认 NSE 脚
**使用场景**
- 网络资产发现和枚举
- 安全评估和渗透测试
- 网络故障排查
- 端口和服务审计
**可选增强**
- `timing``-T0``-T5` 时序
- `nse_scripts``--script` 自定义脚本(如 `vuln`、`http-*`
- `os_detection``-O` **必须 root**,否则 nmap 会 QUITTING
- `aggressive``-A` **必须 root**(含 OS 检测)
- `scan_type`:若传入则**整段替换**上述默认 `-sT -sV -sC`,需自行写上需要的选项(如 `-sT -sV`)
**注意事项:**
- 使用 -sT (TCP连接扫描) 而不是 -sS (SYN扫描),因为 -sS 需要root权限
- 扫描速度取决于网络延迟和目标响应
- 某些扫描可能被防火墙或IDS检测到
- 请确保有权限扫描目标网络
# 参数定义
- `-sS` SYN 扫描需要 root;无 root 请用默认或 `-sT`
- 扫描全端口 `1-65535` 非常慢,建议先常用端口
- 请确保有权限扫描目标
parameters:
- name: "target"
type: "string"
description: |
目标IP地址或域名。可以是单个IP、IP范围、CIDR格式或域名。
目标 IP、主机名、CIDR 或域名URL 会自动提取主机部分
**示例**
- 单个IP: "192.168.1.1"
- IP范围: "192.168.1.1-100"
- CIDR: "192.168.1.0/24"
- 域名: "example.com"
- URL: "https://example.com" (会自动提取域名部分)
**注意事项:**
- 如果提供URL,会自动提取域名部分
- 确保目标地址格式正确
- 必需参数,不能为空
**示例:** `192.168.1.1`、`10.0.0.0/24`、`example.com`
required: true
position: 1 # 位置参数,必须放在命令最后(nmap [options] target),用 1 确保在 flag 之后、最后添加
position: 1
format: "positional"
- name: "ports"
type: "string"
description: |
要扫描的端口范围。可以是单个端口、端口范围、逗号分隔的端口列表,或特殊值。
**示例值:**
- 单个端口: "80"
- 端口范围: "1-1000"
- 多个端口: "80,443,8080,8443"
- 组合: "80,443,8000-9000"
- 常用端口: "1-1024"
- 所有端口: "1-65535"
- 快速扫描: "80,443,22,21,25,53,110,143,993,995"
**注意事项:**
- 如果不指定,将扫描默认的1000个常用端口
- 扫描所有端口(1-65535)会非常耗时
- 建议先扫描常用端口,再根据结果决定是否扫描全部端口
端口范围。示例:`80`、`1-1000`、`80,443,8080`、`1-65535`(全端口很慢)
required: false
flag: "-p"
format: "flag"
- name: "timing"
type: "string"
description: "时序模板 T0–T5,数字越大越快。示例:`4` 生成 `-T4`"
required: false
format: "template"
template: "-T{value}"
- name: "nse_scripts"
type: "string"
description: "NSE 脚本,传给 `--script`。示例:`vuln`、`http-title,http-headers`"
required: false
flag: "--script"
format: "flag"
- name: "os_detection"
type: "bool"
description: |
启用 `-O` OS 检测。**必须 root**;无 root 请保持 false。
required: false
flag: "-O"
format: "flag"
default: false
- name: "aggressive"
type: "bool"
description: |
启用 `-A` 激进扫描(含 OS 检测)。**必须 root**;无 root 请保持 false。
required: false
flag: "-A"
format: "flag"
default: false
- name: "scan_type"
type: "string"
description: |
扫描类型选项。可以覆盖默认的扫描类型
**常用选项:**
- "-sV": 版本检测
- "-sC": 默认脚本扫描
- "-sS": SYN扫描(需要root权限)
- "-sT": TCP连接扫描(默认)
- "-sU": UDP扫描
- "-A": 全面扫描(OS检测、版本检测、脚本扫描、路由追踪)
**注意事项:**
- 多个选项可以组合,用空格分隔,例如: "-sV -sC"
- 默认已包含 "-sT -sV -sC"
- 如果指定此参数,将替换默认的扫描类型
扫描类型选项;**若填写则替换默认的 `-sT -sV -sC`**,只保留你写的选项
多选项用空格分隔,例如:`-sT -sV`、`-sU`UDP)。
required: false
format: "template"
template: "{value}"
- name: "additional_args"
type: "string"
description: |
额外的Nmap参数。用于传递未在参数列表中定义的Nmap选项
**示例值:**
- "--script vuln": 运行漏洞检测脚本
- "-O": 操作系统检测
- "-T4": 时间模板(0-5,数字越大越快)
- "--max-retries 3": 最大重试次数
- "-v": 详细输出
**注意事项:**
- 多个参数用空格分隔
- 确保参数格式正确,避免命令注入
- 此参数会直接追加到命令末尾
额外参数,按空格追加到命令末尾
示例:`--max-retries 3`、`-v`、`-Pn`
required: false
format: "positional"
+119 -25
View File
@@ -1,58 +1,152 @@
name: "rustscan"
command: "rustscan"
enabled: true
short_description: "超快速端口扫描工具,使用Rust编写"
short_description: "超快速端口扫描Rust);可选 greppable、批量与脚本级别"
description: |
Rustscan是一个用Rust编写的超快速端口扫描工具,可以快速扫描大量端口
RustScan 2.x:快速端口发现,可选再调 Nmap 脚本
**主要功能**
- 超快速端口扫描
- 可配置的扫描速度
- 支持Nmap脚本集成
- 批量扫描支持
**与 `rustscan -h` 对应关系**
- `-a` / `--addresses`:扫描目标列表(逗号分隔或文件)
- `-p`:逗号分隔端口;`-r`:端口范围 `start-end`(二选一或与 `-p` 配合以 CLI 为准)
- `-g` / `--greppable`:只输出端口,便于 grep/管道
- `--scripts`**官方默认是 default**(会跑 Nmap);设为 **none** 可只做端口发现、更快
- `-b` batch-size、`-t` timeout、`--scan-order` 等可微调速度与顺序
**使用场景**
- 快速端口扫描
- 大规模网络扫描
- 渗透测试信息收集
**使用建议**
- 快速端口、不要 Nmap`scripts` 用 `none`,必要时加 `-g`
- 需要服务识别/脚本:用 `default` 或 `custom`,并确保本机有 nmap
parameters:
# -a, --addresses
- name: "target"
type: "string"
description: "目标IP地址或主机名"
description: |
对应 `-a`:逗号分隔的 CIDR、IP 或主机名;也可为含换行/列表的文件路径。
示例:`192.168.1.1`、`10.0.0.1,10.0.0.2`、`192.168.0.0/24`
required: true
flag: "-a"
format: "flag"
# -p, --ports;范围请用 range (-r),勿把 1-1000 填进 ports
- name: "ports"
type: "string"
description: "要扫描的端口(如:22,80,443或1-1000"
description: |
要扫描的端口,**仅**逗号分隔列表,对应 `-p`。
示例:`22,80,443`、`80,443,8080`。
若要范围如 `1-1000`,请用参数 **range**`-r`),不要写在本参数。
required: false
flag: "-p"
format: "flag"
# -r, --range(与 ports 列表二选一或按官方说明组合)
- name: "range"
type: "string"
description: |
端口范围,格式 start-end,对应 `-r`。
示例:`1-1000`、`1-65535`(全端口很慢)。
离散端口如 `22,80,443` 请用 **ports**`-p`),不要写在本参数。
required: false
flag: "-r"
format: "flag"
# -u, --ulimit
- name: "ulimit"
type: "int"
description: "文件描述符限制"
description: "提升扫描用的 ulimit;依系统 fd 限制调整"
required: false
flag: "-u"
format: "flag"
default: 5000
# --scriptsnone | default | customCLI 默认 default
- name: "scripts"
type: "bool"
description: "在发现的端口上运行Nmap脚本"
type: "string"
description: |
脚本级别;**必须**传字符串,勿传 true/false。
- **none**:不跑 Nmap,仅端口发现(最快)
- **default**:与 rustscan 官方默认一致,会调 Nmap
- **custom**:自定义脚本,常需配合 additional_args
required: false
flag: "--scripts"
format: "flag"
default: "none"
options:
- "none"
- "default"
- "custom"
# -g, --greppable:仅端口列表,无 Nmap 输出
- name: "greppable"
type: "bool"
description: "Greppable 模式:只输出端口,适合脚本解析或写入文件"
required: false
flag: "-g"
format: "flag"
default: false
# -b, --batch-size [default: 4500]
- name: "batch_size"
type: "int"
description: "每批并发端口数;越大越快,受 OS 打开文件数限制。官方默认 4500"
required: false
flag: "-b"
format: "flag"
# -t, --timeout ms [default: 1500]
- name: "timeout_ms"
type: "int"
description: "单端口超时(毫秒)。官方默认 1500"
required: false
flag: "-t"
format: "flag"
# --scan-order serial | random [default: serial]
- name: "scan_order"
type: "string"
description: "扫描顺序:serial 升序;random 随机"
required: false
flag: "--scan-order"
format: "flag"
options:
- "serial"
- "random"
# --toptop 1000 端口
- name: "top_ports"
type: "bool"
description: "使用内置 top 1000 端口(等价于传 `--top`"
required: false
flag: "--top"
format: "flag"
default: false
# -e, --exclude-ports
- name: "exclude_ports"
type: "string"
description: "排除端口,逗号分隔。示例:`80,443`"
required: false
flag: "-e"
format: "flag"
# -x, --exclude-addresses
- name: "exclude_addresses"
type: "string"
description: "排除地址,逗号分隔 CIDR/IP/主机"
required: false
flag: "-x"
format: "flag"
# --tries [default: 1]
- name: "tries"
type: "int"
description: "判定关闭前的重试次数;0 会被纠正为 1"
required: false
flag: "--tries"
format: "flag"
- name: "additional_args"
type: "string"
description: |
额外的rustscan参数。用于传递未在参数列表中定义的rustscan选项
**示例值:**
- 根据工具特性添加常用参数示例
**注意事项:**
- 多个参数用空格分隔
- 确保参数格式正确,避免命令注入
- 此参数会直接追加到命令末尾
未列在上面的选项可写在这里,空格分隔
示例:`--no-banner`、`--udp`、`-n`(忽略配置文件)、`-c /path/config.toml`、`--resolver 8.8.8.8`
required: false
format: "positional"
+103
View File
@@ -457,6 +457,104 @@ body {
flex-shrink: 0;
height: 100%;
overflow: hidden;
position: relative;
transition: width 0.2s ease;
}
/* 对话页左侧列表折叠(腾出空间给主对话区) */
.conversation-sidebar.collapsed {
width: 56px;
}
/* 对话列表头部:折叠 + 新对话同一行,不重叠 */
.conversation-sidebar-header {
display: flex;
flex-direction: row;
align-items: stretch;
gap: 8px;
flex-shrink: 0;
}
.conversation-sidebar-header .new-chat-btn {
flex: 1;
min-width: 0;
width: auto; /* 覆盖 .new-chat-btn 的 width:100%,让 flex 分配剩余空间 */
}
.conversation-sidebar-collapse-btn {
/* 不再使用 absolute,避免盖住「新对话」 */
position: static;
flex-shrink: 0;
align-self: center;
width: 36px;
height: 36px;
min-width: 36px;
padding: 0;
display: inline-flex;
align-items: center;
justify-content: center;
cursor: pointer;
border-radius: 8px;
background: var(--bg-primary);
border: 1px solid var(--border-color);
color: var(--text-secondary);
transition: transform 0.2s ease, background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
}
.conversation-sidebar-collapse-btn:hover {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--accent-color);
}
/* 折叠后箭头朝右表示「展开」 */
.conversation-sidebar.collapsed .conversation-sidebar-collapse-btn {
transform: rotate(180deg);
}
.conversation-sidebar-collapse-btn svg {
width: 16px;
height: 16px;
stroke: currentColor;
}
.conversation-sidebar.collapsed .sidebar-content {
display: none;
}
.conversation-sidebar.collapsed .conversation-sidebar-header {
flex-direction: column;
align-items: center;
padding: 12px 8px;
gap: 10px;
border-bottom: none;
}
.conversation-sidebar.collapsed .conversation-sidebar-collapse-btn {
order: 2;
}
.conversation-sidebar.collapsed .new-chat-btn {
order: 1; /* 窄条:先「+」再折叠,纵向排列 */
width: 40px;
height: 40px;
min-width: 40px;
padding: 0;
margin: 0;
border-radius: 8px;
gap: 0;
flex: none;
}
/* 折叠时只保留「+」,隐藏「新对话」文案 */
.conversation-sidebar.collapsed .new-chat-btn span:last-child {
display: none;
}
.conversation-sidebar.collapsed .new-chat-btn span:first-child {
font-size: 1.5em;
margin: 0;
}
header {
@@ -2593,6 +2691,11 @@ header {
height: 100%;
overflow: hidden;
}
/* 对话列表折叠态在窄屏下仍保持窄条,避免被 240px 覆盖 */
.conversation-sidebar.collapsed {
width: 56px;
}
.sidebar-content {
min-height: 0;
+16 -3
View File
@@ -104,6 +104,7 @@
},
"chat": {
"newChat": "New chat",
"toggleConversationPanel": "Collapse/expand conversation list",
"searchHistory": "Search history...",
"conversationGroups": "Conversation groups",
"addGroup": "New group",
@@ -404,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",
@@ -1022,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",
+16 -3
View File
@@ -104,6 +104,7 @@
},
"chat": {
"newChat": "新对话",
"toggleConversationPanel": "折叠/展开对话列表",
"searchHistory": "搜索历史记录...",
"conversationGroups": "对话分组",
"addGroup": "新建分组",
@@ -404,7 +405,12 @@
"totalCount": "总数",
"enabledCount": "已启用",
"disabledCount": "已停用",
"connectedCount": "已连接"
"connectedCount": "已连接",
"toolsCountValue": "🔧 {{count}} 个工具",
"connectionErrorLabel": "连接错误:",
"secondsUnit": "秒",
"urlLabel": "URL",
"loadExternalMCPFailed": "加载失败"
},
"settings": {
"title": "系统设置",
@@ -1022,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": "修改密码",
+8
View File
@@ -242,6 +242,14 @@ async function refreshAppData(showTaskErrors = false) {
async function bootstrapApp() {
if (!isAppInitialized) {
// 等待 i18n 首包加载完成后再插系统就绪消息,避免清除缓存后语言显示 English 气泡仍是中文
try {
if (window.i18nReady && typeof window.i18nReady.then === 'function') {
await window.i18nReady;
}
} catch (e) {
console.warn('等待 i18n 就绪失败,继续初始化聊天', e);
}
initializeChatUI();
isAppInitialized = true;
}
+109 -32
View File
@@ -953,7 +953,7 @@ function initializeChatUI() {
const messagesDiv = document.getElementById('chat-messages');
if (messagesDiv && messagesDiv.childElementCount === 0) {
const readyMsg = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
addMessage('assistant', readyMsg);
addMessage('assistant', readyMsg, null, null, null, { systemReadyMessage: true });
}
addAttackChainButton(currentConversationId);
@@ -989,8 +989,60 @@ function wrapTablesInBubble(bubble) {
});
}
// 添加消息
function addMessage(role, content, mcpExecutionIds = null, progressId = null, createdAt = null) {
/**
* 将「系统已就绪」类文案按当前语言重新渲染进气泡(与 addMessage 助手分支一致的安全处理)
*/
function refreshSystemReadyMessageBubbles() {
if (typeof window.t !== 'function') return;
const text = window.t('chat.systemReadyMessage');
const escapeHtmlLocal = (s) => {
if (!s) return '';
const div = document.createElement('div');
div.textContent = s;
return div.innerHTML;
};
const defaultSanitizeConfig = {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'],
ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'],
ALLOW_DATA_ATTR: false,
};
let formattedContent;
if (typeof marked !== 'undefined') {
try {
marked.setOptions({ breaks: true, gfm: true });
const parsed = marked.parse(text);
formattedContent = typeof DOMPurify !== 'undefined'
? DOMPurify.sanitize(parsed, defaultSanitizeConfig)
: parsed;
} catch (e) {
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
}
} else {
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
}
document.querySelectorAll('.message.assistant[data-system-ready-message]').forEach(function (messageDiv) {
const bubble = messageDiv.querySelector('.message-bubble');
if (!bubble) return;
const copyBtn = bubble.querySelector('.message-copy-btn');
if (copyBtn) copyBtn.remove();
bubble.innerHTML = formattedContent;
if (typeof wrapTablesInBubble === 'function') wrapTablesInBubble(bubble);
messageDiv.dataset.originalContent = text;
const copyBtnNew = document.createElement('button');
copyBtnNew.className = 'message-copy-btn';
copyBtnNew.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>' + window.t('common.copy') + '</span>';
copyBtnNew.title = window.t('chat.copyMessageTitle');
copyBtnNew.onclick = function (e) {
e.stopPropagation();
copyMessageToClipboard(messageDiv, this);
};
bubble.appendChild(copyBtnNew);
});
}
// 添加消息(options.systemReadyMessage 为 true 时,语言切换会刷新该条文案)
function addMessage(role, content, mcpExecutionIds = null, progressId = null, createdAt = null, options = null) {
const messagesDiv = document.getElementById('chat-messages');
const messageDiv = document.createElement('div');
messageCounter++;
@@ -1189,7 +1241,9 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
messageTime = new Date();
}
const msgTimeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
timeDiv.textContent = messageTime.toLocaleTimeString(msgTimeLocale, { hour: '2-digit', minute: '2-digit' });
const msgTimeOpts = { hour: '2-digit', minute: '2-digit' };
if (msgTimeLocale === 'zh-CN') msgTimeOpts.hour12 = false;
timeDiv.textContent = messageTime.toLocaleTimeString(msgTimeLocale, msgTimeOpts);
contentWrapper.appendChild(timeDiv);
// 如果有MCP执行ID或进度ID,添加查看详情区域(统一使用"渗透测试详情"样式)
@@ -1234,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;
@@ -1712,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();
// 刷新分组列表,清除分组高亮
@@ -1957,33 +2015,24 @@ function formatConversationTimestamp(dateObj, todayStart, yesterdayStart) {
const fmtLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
const yesterdayLabel = typeof window.t === 'function' ? window.t('chat.yesterday') : '昨天';
const timeOnlyOpts = { hour: '2-digit', minute: '2-digit' };
const dateTimeOpts = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
const fullDateOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' };
if (fmtLocale === 'zh-CN') {
timeOnlyOpts.hour12 = false;
dateTimeOpts.hour12 = false;
fullDateOpts.hour12 = false;
}
if (messageDate.getTime() === referenceToday.getTime()) {
return dateObj.toLocaleTimeString(fmtLocale, {
hour: '2-digit',
minute: '2-digit'
});
return dateObj.toLocaleTimeString(fmtLocale, timeOnlyOpts);
}
if (messageDate.getTime() === referenceYesterday.getTime()) {
return yesterdayLabel + ' ' + dateObj.toLocaleTimeString(fmtLocale, {
hour: '2-digit',
minute: '2-digit'
});
return yesterdayLabel + ' ' + dateObj.toLocaleTimeString(fmtLocale, timeOnlyOpts);
}
if (dateObj.getFullYear() === referenceToday.getFullYear()) {
return dateObj.toLocaleString(fmtLocale, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
return dateObj.toLocaleString(fmtLocale, dateTimeOpts);
}
return dateObj.toLocaleString(fmtLocale, {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
return dateObj.toLocaleString(fmtLocale, fullDateOpts);
}
function getConversationGroup(dateObj, todayStart, startOfWeek, yesterdayStart) {
@@ -2127,7 +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 });
}
// 滚动到底部
@@ -2168,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);
}
@@ -2256,7 +2305,9 @@ async function showAttackChain(conversationId) {
}
modal.style.display = 'block';
// 打开时立即按当前语言刷新统计(避免红框内仍显示硬编码中文)
updateAttackChainStats({ nodes: [], edges: [] });
// 清空容器
const container = document.getElementById('attack-chain-container');
if (container) {
@@ -3331,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');
@@ -5203,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();
}
});
// 显示创建分组模态框
+21 -6
View File
@@ -38,10 +38,25 @@ async function refreshDashboard() {
apiFetch('/api/skills/stats').then(r => r.ok ? r.json() : null).catch(() => null)
]);
// 运行中任务:Agent 循环任务 + 批量队列「执行中」数量统一统计,避免顶部 KPI 与运行概览不一致
let agentRunningCount = null;
if (tasksRes && Array.isArray(tasksRes.tasks)) {
if (runningEl) runningEl.textContent = String(tasksRes.tasks.length);
} else {
if (runningEl) runningEl.textContent = '-';
agentRunningCount = tasksRes.tasks.length;
}
let batchRunningCount = 0;
if (batchRes && Array.isArray(batchRes.queues)) {
batchRes.queues.forEach(q => {
if ((q.status || '').toLowerCase() === 'running') batchRunningCount++;
});
}
if (runningEl) {
if (agentRunningCount !== null) {
runningEl.textContent = String(agentRunningCount + batchRunningCount);
} else if (batchRes && Array.isArray(batchRes.queues)) {
runningEl.textContent = String(batchRunningCount);
} else {
runningEl.textContent = '-';
}
}
if (vulnRes && typeof vulnRes.total === 'number') {
@@ -63,14 +78,14 @@ async function refreshDashboard() {
});
}
// 批量任务队列:按状态统计(优化版)
// 批量任务队列:按状态统计(优化版running 与上方 batchRunningCount 一致
if (batchRes && Array.isArray(batchRes.queues)) {
const queues = batchRes.queues;
let pending = 0, running = 0, done = 0;
let pending = 0, running = batchRunningCount, done = 0;
queues.forEach(q => {
const s = (q.status || '').toLowerCase();
if (s === 'pending' || s === 'paused') pending++;
else if (s === 'running') running++;
else if (s === 'running') { /* already counted into batchRunningCount */ }
else if (s === 'completed' || s === 'cancelled') done++;
});
const total = pending + running + done;
+17
View File
@@ -6,6 +6,12 @@
const loadedLangs = {};
// 供 bootstrap 等逻辑等待:避免 chat 在 t() 未就绪时用中文硬编码渲染,导致与语言标签不一致
let i18nReadyResolve;
window.i18nReady = new Promise(function (resolve) {
i18nReadyResolve = resolve;
});
function detectInitialLang() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
@@ -159,6 +165,7 @@
async function initI18n() {
if (typeof i18next === 'undefined') {
console.warn('i18next 未加载,跳过前端国际化初始化');
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
return;
}
@@ -201,12 +208,22 @@
};
document.addEventListener('click', handleGlobalClickForLangDropdown);
// 若 chat 已在 i18n 完成前用后备中文渲染了系统就绪消息,这里按当前语言纠正一次
try {
if (typeof refreshSystemReadyMessageBubbles === 'function') {
refreshSystemReadyMessageBubbles();
}
} catch (e) { /* ignore */ }
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
}
document.addEventListener('DOMContentLoaded', function () {
// i18n 初始化在 DOM Ready 后执行
initI18n().catch(function (e) {
console.error('初始化国际化失败:', e);
if (typeof i18nReadyResolve === 'function') i18nReadyResolve();
});
});
})();
+134 -11
View File
@@ -3,22 +3,55 @@ let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
// 将后端下发的进度文案转为当前语言的翻译(已知中文 key 映射
// 当前界面语言对应的 BCP 47 标签(与时间格式化一致
function getCurrentTimeLocale() {
if (typeof window.__locale === 'string' && window.__locale.length) {
return window.__locale.startsWith('zh') ? 'zh-CN' : 'en-US';
}
if (typeof i18next !== 'undefined' && i18next.language) {
return (i18next.language || '').startsWith('zh') ? 'zh-CN' : 'en-US';
}
return 'zh-CN';
}
// toLocaleTimeString 选项:中文用 24 小时制,避免仍显示 AM/PM
function getTimeFormatOptions() {
const loc = getCurrentTimeLocale();
const base = { hour: '2-digit', minute: '2-digit', second: '2-digit' };
if (loc === 'zh-CN') {
base.hour12 = false;
}
return base;
}
// 将后端下发的进度文案转为当前语言的翻译(中英双向映射,切换语言后能跟上)
function translateProgressMessage(message) {
if (!message || typeof message !== 'string') return message;
if (typeof window.t !== 'function') return message;
const trim = message.trim();
const map = {
// 中文
'正在调用AI模型...': 'progress.callingAI',
'最后一次迭代:正在生成总结和下一步计划...': 'progress.lastIterSummary',
'总结生成完成': 'progress.summaryDone',
'正在生成最终回复...': 'progress.generatingFinalReply',
'达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary'
'达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary',
// 英文(与 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 callingToolPrefix = '正在调用工具: ';
if (trim.indexOf(callingToolPrefix) === 0) {
const name = trim.slice(callingToolPrefix.length);
const callingToolPrefixCn = '正在调用工具: ';
const callingToolPrefixEn = 'Calling tool: ';
if (trim.indexOf(callingToolPrefixCn) === 0) {
const name = trim.slice(callingToolPrefixCn.length);
return window.t('progress.callingTool', { name: name });
}
if (trim.indexOf(callingToolPrefixEn) === 0) {
const name = trim.slice(callingToolPrefixEn.length);
return window.t('progress.callingTool', { name: name });
}
return message;
@@ -497,11 +530,12 @@ function handleStreamEvent(event, progressElement, progressId,
}
break;
case 'iteration':
// 添加迭代标记
// 添加迭代标记data 属性供语言切换时重算标题)
addTimelineItem(timeline, 'iteration', {
title: typeof window.t === 'function' ? window.t('chat.iterationRound', { n: event.data?.iteration || 1 }) : '第 ' + (event.data?.iteration || 1) + ' 轮迭代',
message: event.message,
data: event.data
data: event.data,
iterationN: event.data?.iteration || 1
});
break;
@@ -569,6 +603,11 @@ function handleStreamEvent(event, progressElement, progressId,
case 'progress':
const progressTitle = document.querySelector(`#${progressId} .progress-title`);
if (progressTitle) {
// 保存原文,语言切换时可用 translateProgressMessage 重新套当前语言
const progressEl = document.getElementById(progressId);
if (progressEl) {
progressEl.dataset.progressRawMessage = event.message || '';
}
const progressMsg = translateProgressMessage(event.message);
progressTitle.textContent = '🔍 ' + progressMsg;
}
@@ -855,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;
@@ -875,8 +926,9 @@ function addTimelineItem(timeline, type, options) {
eventTime = new Date();
}
const timeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
const time = eventTime.toLocaleTimeString(timeLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const timeLocale = getCurrentTimeLocale();
const timeOpts = getTimeFormatOptions();
const time = eventTime.toLocaleTimeString(timeLocale, timeOpts);
let content = `
<div class="timeline-item-header">
@@ -987,9 +1039,10 @@ function renderActiveTasks(tasks) {
item.className = 'active-task-item';
const startedTime = task.startedAt ? new Date(task.startedAt) : null;
const taskTimeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US';
const taskTimeLocale = getCurrentTimeLocale();
const timeOpts = getTimeFormatOptions();
const timeText = startedTime && !isNaN(startedTime.getTime())
? startedTime.toLocaleTimeString(taskTimeLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit' })
? startedTime.toLocaleTimeString(taskTimeLocale, timeOpts)
: '';
const _t = function (k) { return typeof window.t === 'function' ? window.t(k) : k; };
@@ -1686,6 +1739,76 @@ 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();
});
+27 -1
View File
@@ -243,7 +243,8 @@ function initPage(pageId) {
}
break;
case 'chat':
// 对话页面已由chat.js初始化
// 恢复对话列表折叠状态(从其他页返回时保持用户选择)
initConversationSidebarState();
break;
case 'info-collect':
// 信息收集页面
@@ -421,11 +422,36 @@ function initSidebarState() {
sidebar.classList.add('collapsed');
}
}
initConversationSidebarState();
}
// 切换对话页左侧列表折叠/展开
function toggleConversationSidebar() {
const sidebar = document.getElementById('conversation-sidebar');
if (sidebar) {
sidebar.classList.toggle('collapsed');
const isCollapsed = sidebar.classList.contains('collapsed');
localStorage.setItem('conversationSidebarCollapsed', isCollapsed ? 'true' : 'false');
}
}
// 恢复对话列表折叠状态(进入对话页时生效)
function initConversationSidebarState() {
const sidebar = document.getElementById('conversation-sidebar');
if (sidebar) {
const savedState = localStorage.getItem('conversationSidebarCollapsed');
if (savedState === 'true') {
sidebar.classList.add('collapsed');
} else {
sidebar.classList.remove('collapsed');
}
}
}
// 导出函数供其他脚本使用
window.switchPage = switchPage;
window.toggleSubmenu = toggleSubmenu;
window.toggleSidebar = toggleSidebar;
window.toggleConversationSidebar = toggleConversationSidebar;
window.currentPage = function() { return currentPage; };
+26 -8
View File
@@ -1166,7 +1166,8 @@ async function loadExternalMCPs() {
console.error('加载外部MCP列表失败:', error);
const list = document.getElementById('external-mcp-list');
if (list) {
list.innerHTML = `<div class="error">加载失败: ${escapeHtml(error.message)}</div>`;
const errT = typeof window.t === 'function' ? window.t : (k) => k;
list.innerHTML = `<div class="error">${escapeHtml(errT('mcp.loadExternalMCPFailed'))}: ${escapeHtml(error.message)}</div>`;
}
}
}
@@ -1224,7 +1225,7 @@ function renderExternalMCPList(servers) {
<div class="external-mcp-item">
<div class="external-mcp-item-header">
<div class="external-mcp-item-info">
<h4>${transportIcon} ${escapeHtml(name)}${server.tool_count !== undefined && server.tool_count > 0 ? `<span class="tool-count-badge" title="工具数量">🔧 ${server.tool_count}</span>` : ''}</h4>
<h4>${transportIcon} ${escapeHtml(name)}${server.tool_count !== undefined && server.tool_count > 0 ? `<span class="tool-count-badge" title="${escapeHtml(statusT('mcp.toolCount'))}">🔧 ${server.tool_count}</span>` : ''}</h4>
<span class="external-mcp-status ${statusClass}">${statusText}</span>
</div>
<div class="external-mcp-item-actions">
@@ -1234,7 +1235,7 @@ function renderExternalMCPList(servers) {
</button>` :
status === 'connecting' ?
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" disabled style="opacity: 0.6; cursor: not-allowed;">
连接中...
${statusT('mcp.connecting')}
</button>` : ''}
<button class="btn-small" onclick="editExternalMCP('${escapeHtml(name)}')" title="${statusT('mcp.editConfig')}" ${status === 'connecting' ? 'disabled' : ''}> ${statusT('common.edit')}</button>
<button class="btn-small btn-danger" onclick="deleteExternalMCP('${escapeHtml(name)}')" title="${statusT('mcp.deleteConfig')}" ${status === 'connecting' ? 'disabled' : ''}>🗑 ${statusT('common.delete')}</button>
@@ -1242,7 +1243,7 @@ function renderExternalMCPList(servers) {
</div>
${status === 'error' && server.error ? `
<div class="external-mcp-error" style="margin: 12px 0; padding: 12px; background: #fee; border-left: 3px solid #f44; border-radius: 4px; color: #c33; font-size: 0.875rem;">
<strong> 连接错误</strong>${escapeHtml(server.error)}
<strong> ${statusT('mcp.connectionErrorLabel')}</strong>${escapeHtml(server.error)}
</div>` : ''}
<div class="external-mcp-item-details">
<div>
@@ -1252,7 +1253,7 @@ function renderExternalMCPList(servers) {
${server.tool_count !== undefined && server.tool_count > 0 ? `
<div>
<strong>${statusT('mcp.toolCount')}</strong>
<span style="font-weight: 600; color: var(--accent-color);">🔧 ${server.tool_count} 个工具</span>
<span style="font-weight: 600; color: var(--accent-color);">${statusT('mcp.toolsCountValue', { count: server.tool_count })}</span>
</div>` : server.tool_count === 0 && status === 'connected' ? `
<div>
<strong>${statusT('mcp.toolCount')}</strong>
@@ -1266,7 +1267,7 @@ function renderExternalMCPList(servers) {
${server.config.timeout ? `
<div>
<strong>${statusT('mcp.timeout')}</strong>
<span>${server.config.timeout} </span>
<span>${server.config.timeout} ${statusT('mcp.secondsUnit')}</span>
</div>` : ''}
${transport === 'stdio' && server.config.command ? `
<div>
@@ -1275,7 +1276,7 @@ function renderExternalMCPList(servers) {
</div>` : ''}
${transport === 'http' && server.config.url ? `
<div>
<strong>URL</strong>
<strong>${statusT('mcp.urlLabel')}</strong>
<span style="font-family: monospace; font-size: 0.8125rem; word-break: break-all;">${escapeHtml(server.config.url)}</span>
</div>` : ''}
</div>
@@ -1327,7 +1328,7 @@ async function editExternalMCP(name) {
try {
const response = await apiFetch(`/api/external-mcp/${encodeURIComponent(name)}`);
if (!response.ok) {
throw new Error('获取外部MCP配置失败');
throw new Error(typeof window.t === 'function' ? window.t('mcp.getConfigFailed') : '获取外部MCP配置失败');
}
const server = await response.json();
@@ -1742,3 +1743,20 @@ openSettings = async function() {
await originalOpenSettings();
await loadExternalMCPs();
};
// 语言切换后重新渲染 MCP 管理页中由 JS 写入的区块(innerHTML 不会随 data-i18n 自动更新)
document.addEventListener('languagechange', function () {
try {
const mcpPage = document.getElementById('page-mcp-management');
if (mcpPage && mcpPage.classList.contains('active')) {
if (typeof loadExternalMCPs === 'function') {
loadExternalMCPs().catch(function () { /* ignore */ });
}
if (typeof updateToolsStats === 'function') {
updateToolsStats().catch(function () { /* ignore */ });
}
}
} catch (e) {
console.warn('languagechange MCP refresh failed', e);
}
});
+76 -11
View File
@@ -26,7 +26,33 @@
return terminals[0] || null;
}
var WELCOME_LINE = 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏\r\n';
function tr(key, opts) {
if (typeof window !== 'undefined' && typeof window.t === 'function') {
return window.t(key, opts);
}
// i18n 未就绪时的后备(与 zh-CN 一致)
var fallbacks = {
'settingsTerminal.welcomeLine': 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏',
'settingsTerminal.sessionClosed': '[会话已关闭]',
'settingsTerminal.connectionError': '[终端连接出错]',
'settingsTerminal.connectFailed': '[无法连接终端服务: {{msg}}]',
'settingsTerminal.closeTabTitle': '关闭',
'settingsTerminal.containerClickTitle': '点击此处后输入命令',
'settingsTerminal.xtermNotLoaded': '未加载 xterm.js,请刷新页面或检查网络。',
'settingsTerminal.terminalTab': '终端 {{n}}'
};
var s = fallbacks[key] || key;
if (opts && typeof opts === 'object') {
Object.keys(opts).forEach(function (k) {
s = s.split('{{' + k + '}}').join(String(opts[k]));
});
}
return s;
}
function getWelcomeLine() {
return tr('settingsTerminal.welcomeLine') + '\r\n';
}
function writePrompt(tab) {
// 提示符交由后端 Shell 自行输出,这里仅保留占位函数,避免旧代码报错
@@ -35,7 +61,7 @@
function redrawTabDisplay(t) {
if (!t || !t.term) return;
t.term.clear();
t.term.write(WELCOME_LINE);
t.term.write(getWelcomeLine());
}
function writeln(tabOrS, s) {
@@ -121,19 +147,19 @@
ws.onclose = function () {
tab.running = false;
if (tab.term) {
tab.term.writeln('\r\n\x1b[2m[会话已关闭]\x1b[0m');
tab.term.writeln('\r\n\x1b[2m' + tr('settingsTerminal.sessionClosed') + '\x1b[0m');
}
};
ws.onerror = function () {
tab.running = false;
if (tab.term) {
tab.term.writeln('\r\n\x1b[31m[终端连接出错]\x1b[0m');
tab.term.writeln('\r\n\x1b[31m' + tr('settingsTerminal.connectionError') + '\x1b[0m');
}
};
} catch (e) {
if (tab.term) {
tab.term.writeln('\r\n\x1b[31m[无法连接终端服务: ' + String(e) + ']\x1b[0m');
tab.term.writeln('\r\n\x1b[31m' + tr('settingsTerminal.connectFailed', { msg: String(e) }) + '\x1b[0m');
}
}
}
@@ -182,13 +208,13 @@
term.loadAddon(fitAddon);
}
term.open(container);
term.write(WELCOME_LINE);
term.write(getWelcomeLine());
container.addEventListener('click', function () {
switchTerminalTab(tab.id);
if (term) term.focus();
});
container.setAttribute('tabindex', '0');
container.title = '点击此处后输入命令';
container.title = tr('settingsTerminal.containerClickTitle');
function sendToWS(data) {
ensureTerminalWS(tab);
@@ -211,6 +237,9 @@
tab.term = term;
tab.fitAddon = fitAddon;
// 立即建立 WebSocket,让后端 PTY/Shell 马上启动并输出提示符;
// 若等到首次按键才 connect,用户会感觉必须先按回车才能输入(实为连接尚未建立)。
ensureTerminalWS(tab);
return term;
}
@@ -253,12 +282,12 @@
tabDiv.setAttribute('data-tab-id', String(id));
var label = document.createElement('span');
label.className = 'terminal-tab-label';
label.textContent = '终端 ' + id;
label.textContent = tr('settingsTerminal.terminalTab', { n: id });
label.onclick = function () { switchTerminalTab(id); };
var closeBtn = document.createElement('button');
closeBtn.type = 'button';
closeBtn.className = 'terminal-tab-close';
closeBtn.title = '关闭';
closeBtn.title = tr('settingsTerminal.closeTabTitle');
closeBtn.textContent = '×';
closeBtn.onclick = function (e) { e.stopPropagation(); removeTerminalTab(id); };
tabDiv.appendChild(label);
@@ -340,7 +369,7 @@
var t = terminals[i];
tabDivs[i].setAttribute('data-tab-id', String(t.id));
var lbl = tabDivs[i].querySelector('.terminal-tab-label');
if (lbl) lbl.textContent = '终端 ' + t.id;
if (lbl) lbl.textContent = tr('settingsTerminal.terminalTab', { n: t.id });
if (lbl) lbl.onclick = (function (tid) { return function () { switchTerminalTab(tid); }; })(t.id);
var cb = tabDivs[i].querySelector('.terminal-tab-close');
if (cb) cb.onclick = (function (tid) { return function (e) { e.stopPropagation(); removeTerminalTab(tid); }; })(t.id);
@@ -364,6 +393,40 @@
}
}
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function refreshTerminalI18n() {
// 语言切换后更新标签与容器 title;已打开的终端内容不强制清屏,以免丢失会话输出
try {
var tabsEl = document.querySelector('.terminal-tabs');
if (tabsEl) {
var tabDivs = tabsEl.querySelectorAll('.terminal-tab');
for (var i = 0; i < tabDivs.length && i < terminals.length; i++) {
var tid = terminals[i].id;
var lbl = tabDivs[i].querySelector('.terminal-tab-label');
if (lbl) lbl.textContent = tr('settingsTerminal.terminalTab', { n: tid });
var cb = tabDivs[i].querySelector('.terminal-tab-close');
if (cb) cb.title = tr('settingsTerminal.closeTabTitle');
}
}
terminals.forEach(function (tab) {
if (!tab || !tab.term) return;
var cont = document.getElementById(tab.containerId);
if (cont) cont.title = tr('settingsTerminal.containerClickTitle');
});
} catch (e) { /* ignore */ }
}
document.addEventListener('languagechange', function () {
refreshTerminalI18n();
});
function initTerminal() {
var pane1 = document.getElementById('terminal-pane-1');
var container1 = document.getElementById('terminal-container-1');
@@ -377,7 +440,7 @@
inited = true;
if (typeof Terminal === 'undefined') {
container1.innerHTML = '<p class="terminal-error">未加载 xterm.js,请刷新页面或检查网络。</p>';
container1.innerHTML = '<p class="terminal-error">' + escapeHtml(tr('settingsTerminal.xtermNotLoaded')) + '</p>';
return;
}
@@ -388,6 +451,8 @@
updateTerminalTabCloseVisibility();
refreshTerminalI18n();
setTimeout(function () {
try { if (tab.fitAddon) tab.fitAddon.fit(); if (tab.term) tab.term.focus(); } catch (e) {}
}, 100);
+11 -5
View File
@@ -400,12 +400,18 @@
<!-- 对话页面 -->
<div id="page-chat" class="page">
<div class="chat-page-layout">
<!-- 历史对话侧边栏 -->
<aside class="conversation-sidebar">
<div class="sidebar-header">
<button class="new-chat-btn" onclick="startNewConversation()">
<!-- 历史对话侧边栏(可折叠,与主导航侧边栏类似) -->
<aside class="conversation-sidebar" id="conversation-sidebar">
<!-- 头部一行:折叠与「新对话」并排,避免绝对定位重叠(flex 为最佳实践) -->
<div class="sidebar-header conversation-sidebar-header">
<button type="button" class="new-chat-btn" onclick="startNewConversation()">
<span>+</span> <span data-i18n="chat.newChat">新对话</span>
</button>
<button type="button" class="conversation-sidebar-collapse-btn" onclick="toggleConversationSidebar()" data-i18n="chat.toggleConversationPanel" data-i18n-attr="title" data-i18n-skip-text="true" title="折叠/展开对话列表" aria-label="折叠/展开对话列表">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
<path d="M15 18l-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</button>
</div>
<div class="sidebar-content">
<!-- 全局搜索 -->
@@ -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="搜索节点..."