Compare commits

...

14 Commits

Author SHA1 Message Date
公明 a5c285c8f3 Update version number to v1.3.21 2026-03-10 00:06:37 +08:00
公明 98938aef00 Remove user message check for Qwen model
Removed the logic to ensure at least one user message is included in recent messages to avoid Qwen model error.
2026-03-10 00:04:19 +08:00
公明 71f6a97a90 Add files via upload 2026-03-09 23:00:24 +08:00
公明 2fce15f82a Enhance MCP config with authentication headers
Added authentication headers for MCP server configuration.
2026-03-09 22:40:06 +08:00
公明 52b70d8b16 Add files via upload 2026-03-09 22:38:24 +08:00
公明 5b3709b9ad Add files via upload 2026-03-09 22:37:37 +08:00
公明 639f65602d Add files via upload 2026-03-09 22:36:22 +08:00
公明 52b6c3fe1b Add files via upload 2026-03-09 22:35:39 +08:00
公明 f26ee8e6e7 Add files via upload 2026-03-09 22:19:22 +08:00
公明 379486d36c Add files via upload 2026-03-09 21:35:49 +08:00
公明 317461e259 Add files via upload 2026-03-09 21:30:32 +08:00
公明 b7e724407b Add files via upload 2026-03-09 20:40:36 +08:00
公明 e904dd3481 Update version number to v1.3.20 2026-03-09 02:10:48 +08:00
公明 7b1487383f Add files via upload 2026-03-09 02:06:39 +08:00
29 changed files with 5392 additions and 1461 deletions
+29 -15
View File
@@ -262,21 +262,33 @@ go build -o cyberstrike-ai cmd/server/main.go
```
Replace the paths with your local locations; Cursor will launch the stdio server automatically.
#### MCP HTTP quick start
1. Ensure `config.yaml` has `mcp.enabled: true` and adjust `mcp.host` / `mcp.port` if you need a non-default binding (localhost:8081 works well for local Cursor usage).
2. Start the main service (`./run.sh` or `go run cmd/server/main.go`); the MCP endpoint lives at `http://<host>:<port>/mcp`.
3. In Cursor, choose **Add Custom MCP → HTTP** and set `Base URL` to `http://127.0.0.1:8081/mcp`.
4. Prefer committing the setup via `.cursor/mcp.json` so teammates can reuse it:
```json
{
"mcpServers": {
"cyberstrike-ai-http": {
"transport": "http",
"url": "http://127.0.0.1:8081/mcp"
}
}
}
```
#### MCP HTTP quick start (Cursor / Claude Code)
The HTTP MCP server runs on a separate port (default `8081`) and supports **header-based authentication** so only clients that send the correct header can call tools.
1. **Enable MCP in config** In `config.yaml` set `mcp.enabled: true` and optionally `mcp.host` / `mcp.port`. For auth (recommended if the port is reachable from the network), set:
- `mcp.auth_header` header name (e.g. `X-MCP-Token`);
- `mcp.auth_header_value` secret value. **Leave it empty** if you want the server to **auto-generate** a random token on first start and write it back to the config.
2. **Start the service** Run `./run.sh` or `go run cmd/server/main.go`. The MCP endpoint is `http://<host>:<port>/mcp` (e.g. `http://localhost:8081/mcp`).
3. **Copy the JSON from the terminal** When MCP is enabled, the server prints a **ready-to-paste** JSON block. If `auth_header_value` was empty, it will have been generated and saved; the printed JSON includes the URL and headers.
4. **Use in Cursor or Claude Code**:
- **Cursor**: Paste the block into `~/.cursor/mcp.json` (or your projects `.cursor/mcp.json`) under `mcpServers`, or merge it into your existing `mcpServers`.
- **Claude Code**: Paste into `.mcp.json` or `~/.claude.json` under `mcpServers`.
Example of what the terminal prints (with auth enabled):
```json
{
"mcpServers": {
"cyberstrike-ai": {
"url": "http://localhost:8081/mcp",
"headers": {
"X-MCP-Token": "<auto-generated-or-your-value>"
},
"type": "http"
}
}
}
```
If you do not set `auth_header` / `auth_header_value`, the endpoint accepts requests without authentication (suitable only for localhost or trusted networks).
#### External MCP federation (HTTP/stdio/SSE)
CyberStrikeAI supports connecting to external MCP servers via three transport modes:
@@ -396,6 +408,8 @@ mcp:
enabled: true
host: "0.0.0.0"
port: 8081
auth_header: "X-MCP-Token" # optional; leave empty for no auth
auth_header_value: "" # optional; leave empty to auto-generate on first start
openai:
api_key: "sk-xxx"
base_url: "https://api.deepseek.com/v1"
+29 -15
View File
@@ -260,21 +260,33 @@ go build -o cyberstrike-ai cmd/server/main.go
```
将路径替换成你本地的实际地址,Cursor 会自动启动 stdio 版本的 MCP。
#### MCP HTTP 快速集成
1. 确认 `config.yaml` 中 `mcp.enabled: true`,按照需要调整 `mcp.host` / `mcp.port`(本地建议 `127.0.0.1:8081`
2. 启动主服务(`./run.sh` 或 `go run cmd/server/main.go`),MCP 端点默认暴露在 `http://<host>:<port>/mcp`。
3. 在 Cursor 内 `Add Custom MCP → HTTP`,将 `Base URL` 设置 `http://127.0.0.1:8081/mcp`。
4. 也可以在项目根目录创建 `.cursor/mcp.json` 以便团队共享:
```json
{
"mcpServers": {
"cyberstrike-ai-http": {
"transport": "http",
"url": "http://127.0.0.1:8081/mcp"
}
}
}
```
#### MCP HTTP 快速集成Cursor / Claude Code
HTTP MCP 服务在独立端口(默认 `8081`)运行,支持 **Header 鉴权**:仅携带正确 header 的客户端可调用工具
1. **在配置中启用 MCP** – 在 `config.yaml` 中设置 `mcp.enabled: true`,并按需设置 `mcp.host` / `mcp.port`。若需鉴权(端口对外暴露时建议开启),可设置:
- `mcp.auth_header`:鉴权用的 header 名(如 `X-MCP-Token`);
- `mcp.auth_header_value`:鉴权密钥。**留空**时,首次启动会自动生成随机密钥并写回配置文件。
2. **启动服务** 执行 `./run.sh` 或 `go run cmd/server/main.go`。MCP 端点为 `http://<host>:<port>/mcp`(例如 `http://localhost:8081/mcp`)。
3. **从终端复制 JSON** – 启用 MCP 后,启动时会在终端打印一段 **可直接复制的 JSON**。若 `auth_header_value` 留空,会自动生成并写入配置,打印内容中会包含 URL 与 headers。
4. **在 Cursor 或 Claude Code 中使用**
- **Cursor**:将整段 JSON 粘贴到 `~/.cursor/mcp.json` 或项目下的 `.cursor/mcp.json` 的 `mcpServers` 中(或合并进现有 `mcpServers`)。
- **Claude Code**:粘贴到 `.mcp.json` 或 `~/.claude.json` 的 `mcpServers` 中。
终端打印示例(开启鉴权时):
```json
{
"mcpServers": {
"cyberstrike-ai": {
"url": "http://localhost:8081/mcp",
"headers": {
"X-MCP-Token": "<自动生成或你配置的值>"
},
"type": "http"
}
}
}
```
若不配置 `auth_header` / `auth_header_value`,则端点不鉴权(仅适合本机或可信网络)。
#### 外部 MCP 联邦(HTTP/stdio/SSE
CyberStrikeAI 支持通过三种传输模式连接外部 MCP 服务器:
@@ -395,6 +407,8 @@ mcp:
enabled: true
host: "0.0.0.0"
port: 8081
auth_header: "X-MCP-Token" # 可选;留空则不鉴权
auth_header_value: "" # 可选;留空则首次启动自动生成并写回
openai:
api_key: "sk-xxx"
base_url: "https://api.deepseek.com/v1"
+9
View File
@@ -19,6 +19,15 @@ func main() {
return
}
// MCP 启用且 auth_header_value 为空时,自动生成随机密钥并写回配置
if err := config.EnsureMCPAuth(*configPath, cfg); err != nil {
fmt.Printf("MCP 鉴权配置失败: %v\n", err)
return
}
if cfg.MCP.Enabled {
config.PrintMCPConfigJSON(cfg.MCP)
}
// 初始化日志
log := logger.New(cfg.Log.Level, cfg.Log.Output)
+5 -3
View File
@@ -10,7 +10,7 @@
# ============================================
# 前端显示的版本号(可选,不填则显示默认版本)
version: "v1.3.19"
version: "v1.3.21"
# 服务器配置
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:
+335
View File
@@ -0,0 +1,335 @@
## CyberStrikeAI 前端国际化方案
本文档说明 CyberStrikeAI Web 前端(`web/templates/index.html` + `web/static/js/*.js`)的国际化设计与开发规范,确保在不引入打包工具和不改动后端路由的前提下,实现可扩展、低返工的多语言支持。
当前目标:
- **支持中英文切换(zh-CN / en-US**
- 后续可方便扩展更多语言(如 ja-JP、ko-KR 等)
---
## 一、总体设计原则
- **前端主导的客户端国际化**:所有 UI 文案在浏览器端根据当前语言动态渲染,后端 Go 仅负责结构和数据,不参与语言分发。
- **单一 HTML 模板**:继续使用一份 `index.html` 模板,不为不同语言复制模板文件。
- **文案与逻辑分离**:所有可见文本通过「键值表」管理(多语言 JSON),HTML / JS 只写 key,不直接写中文/英文常量。
- **渐进式改造**:先覆盖 header / 登录 / 侧边栏 / 系统设置等关键区域,其他页面按模块逐步迁移,避免一次性大改动。
- **可回退默认语言**:即使目标语言未完全翻译,也能回退到默认中文,不出现原始 key。
---
## 二、技术选型与目录结构
### 2.1 技术选型
- **i18n 引擎**:使用 [i18next](https://www.i18next.com/) 的浏览器 UMD 版本(通过 CDN 引入),无需打包器。
- **资源格式**:每种语言一份 JSON 文件,采用「域 + 语义」的层级 key 方案,例如:
- `common.ok`
- `nav.dashboard`
- `header.apiDocs`
- `settings.robot.wecom.token`
### 2.2 目录结构
- `web/templates/index.html`
- 页面骨架 + 所有静态文案位置,将逐步改为 `data-i18n` 标记。
- `web/static/js/i18n.js`
- 前端 i18n 初始化与 DOM 应用逻辑(本方案新增)。
- `web/static/i18n/`(新增目录)
- `zh-CN.json`:中文文案(默认语言)
- `en-US.json`:英文文案
- 未来可新增:`ja-JP.json``ko-KR.json` 等。
---
## 三、文案组织规范
### 3.1 Key 命名约定
- 采用「**模块.语义**」形式,最多 2–3 级,确保可读性:
- 导航:`nav.dashboard``nav.chat``nav.settings`
- 头部:`header.title``header.apiDocs``header.logout`
- 登录:`login.title``login.subtitle``login.passwordLabel``login.submit`
- 仪表盘:`dashboard.title``dashboard.refresh``dashboard.runningTasks`
- 系统设置:`settings.title``settings.nav.basic``settings.nav.robot``settings.apply`
- 机器人配置:`settings.robot.wecom.enabled``settings.robot.wecom.token` 等。
- 尽量按「界面区域」而不是「文件名」划分域,便于非开发人员理解。
### 3.2 JSON 示例
`web/static/i18n/zh-CN.json` 示例:
```json
{
"common": {
"ok": "确定",
"cancel": "取消"
},
"nav": {
"dashboard": "仪表盘",
"chat": "对话",
"infoCollect": "信息收集",
"tasks": "任务管理",
"vulnerabilities": "漏洞管理",
"settings": "系统设置"
},
"header": {
"title": "CyberStrikeAI",
"apiDocs": "API 文档",
"logout": "退出登录",
"language": "界面语言"
},
"login": {
"title": "登录 CyberStrikeAI",
"subtitle": "请输入配置中的访问密码",
"passwordLabel": "密码",
"passwordPlaceholder": "输入登录密码",
"submit": "登录"
}
}
```
英文文件 `en-US.json` 保持相同 key,不同 value
```json
{
"common": {
"ok": "OK",
"cancel": "Cancel"
},
"nav": {
"dashboard": "Dashboard",
"chat": "Chat",
"infoCollect": "Recon",
"tasks": "Tasks",
"vulnerabilities": "Vulnerabilities",
"settings": "Settings"
},
"header": {
"title": "CyberStrikeAI",
"apiDocs": "API Docs",
"logout": "Sign out",
"language": "Interface language"
},
"login": {
"title": "Sign in to CyberStrikeAI",
"subtitle": "Enter the access password from config",
"passwordLabel": "Password",
"passwordPlaceholder": "Enter password",
"submit": "Sign in"
}
}
```
> 约定:**新增界面时,必须先定义 i18n key,再在 HTML/JS 中使用 key**,禁止直接写死中文/英文。
---
## 四、HTML 标记规范(data-i18n
### 4.1 基本规则
- 使用 `data-i18n` 将元素文本与某个 key 绑定:
```html
<span data-i18n="nav.dashboard">仪表盘</span>
```
- 默认行为:脚本会替换元素的 `textContent`
- 同时翻译属性时,额外使用 `data-i18n-attr`,逗号分隔多个属性名:
```html
<button
class="openapi-doc-btn"
onclick="window.open('/api-docs', '_blank')"
data-i18n="header.apiDocs"
data-i18n-attr="title"
title="API 文档">
<span data-i18n="header.apiDocs">API 文档</span>
</button>
```
### 4.2 默认文本的作用
- HTML 内的中文默认值作为「**无 JS / 初始化前**」的占位内容:
- 页面在 JS 尚未加载完成时不会出现空白或 key。
- JS 初始化后会用当前语言覆盖这些文本。
---
## 五、JavaScript 中的文案规范
### 5.1 全局翻译函数 `t()`
`i18n.js` 暴露以下全局函数:
- `window.t(key: string): string`
- 返回当前语言下的翻译文本,若缺失则回退到默认语言,再不行则返回 key 本身。
- `window.changeLanguage(lang: string): Promise<void>`
- 切换语言并刷新页面文案(不会刷新整页)。
示例(以 `web/static/js/settings.js` 为例):
```js
// 之前
alert('加载配置失败: ' + error.message);
// 之后
alert(t('settings.loadConfigFailed') + ': ' + error.message);
```
> 规范:**JS 内所有面向用户的提示、按钮文字、对话框标题都应通过 `t()` 获取**,不直接写死中文/英文。
### 5.2 渐进迁移建议
- 优先改造:
- 频繁弹出的错误提示 / 成功提示;
- 登录相关、系统设置相关文案。
- 低优先级:
- 仅面向运维人员的调试提示,可以暂时保留英文/中文常量。
---
## 六、i18n 初始化与语言切换实现
### 6.1 语言选择策略
- 默认语言:`zh-CN`
- 优先级(从高到低):
1. `localStorage` 中的用户选择(key`csai_lang`)。
2. 浏览器 `navigator.language``zh` 开头 → `zh-CN`,否则 `en-US`)。
3. 默认 `zh-CN`
### 6.2 初始化流程(`i18n.js`
1. 读取初始语言。
2. 初始化 i18next
- `lng` 为当前语言;
- `fallbackLng``zh-CN`
- 资源先留空,采用按需加载。
3. 通过 `fetch` 拉取 `/static/i18n/{lng}.json``i18next.addResources`
4. 更新:
- `<html lang="...">` 属性;
- 所有带 `data-i18n` / `data-i18n-attr` 的元素。
5. 暴露 `window.t``window.changeLanguage`
### 6.3 DOM 应用逻辑
伪代码:
```js
function applyTranslations(root = document) {
const elements = root.querySelectorAll('[data-i18n]');
elements.forEach(el => {
const key = el.getAttribute('data-i18n');
if (!key) return;
const text = i18next.t(key);
if (text) {
el.textContent = text;
}
const attrList = el.getAttribute('data-i18n-attr');
if (attrList) {
attrList.split(',').map(s => s.trim()).forEach(attr => {
if (!attr) return;
const val = i18next.t(key);
if (val) el.setAttribute(attr, val);
});
}
});
}
```
> 对于由 JS 动态插入的元素,需要在插入后再次调用 `applyTranslations(新容器)`。
---
## 七、语言切换 UI 规范
### 7.1 位置与形态
- 位置:`index.html` header 右侧 `API 文档` 按钮附近(靠近用户头像)。
- 交互形式:
- 一个紧凑的语言切换组件,例如:
- `🌐` 图标 + 当前语言文本(`中文` / `English`)的下拉按钮;
- 下拉内容列出所有可用语言。
### 7.2 示例结构
```html
<div class="lang-switcher">
<button class="btn-secondary lang-switcher-btn" onclick="toggleLangDropdown()" data-i18n="header.language">
<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="onLanguageSelect('zh-CN')">中文</div>
<div class="lang-option" data-lang="en-US" onclick="onLanguageSelect('en-US')">English</div>
</div>
</div>
```
对应 JS(在 `i18n.js` 中):
```js
function onLanguageSelect(lang) {
changeLanguage(lang).then(updateLangLabel).catch(console.error);
closeLangDropdown();
}
function updateLangLabel() {
const labelEl = document.getElementById('current-lang-label');
if (!labelEl) return;
const lang = i18next.language || 'zh-CN';
labelEl.textContent = lang.startsWith('zh') ? '中文' : 'English';
}
```
> 规范:**语言切换只更新文案,不刷新整页,也不修改 URL hash**。
---
## 八、开发流程建议
### 8.1 新增 / 修改界面的流程
1. 设计界面时,先列出所有文案。
2. 在对应语言 JSON 中补充/修改 key 与翻译。
3. 在 HTML 中使用 `data-i18n`,在 JS 中使用 `t('...')`
4. 在浏览器中切换中英文,确认两种语言显示都正确。
### 8.2 渐进式改造顺序(推荐)
1. **阶段 1(已规划)**
- 引入 i18next 与 `i18n.js`
- 新建 `zh-CN.json` / `en-US.json`(先覆盖 header / 登录 / 左侧导航)。
- 实现 header 区域语言切换组件。
2. **阶段 2**(已完成)
- 系统设置页面(包括机器人配置页面)全部文案 i18n 化。
- `settings.js` 中的提示与错误信息改用 `t()`
3. **阶段 3**(进行中)
- 仪表盘、任务管理、漏洞管理、MCP、Skills、Roles 等页面按模块逐步迁移。
4. **阶段 4**
- 清理 JS / HTML 中残留的硬编码中文,统一通过 i18n。
---
## 九、后续扩展新语言
当需要新增语言时:
1.`web/static/i18n/` 中新增 `{lang}.json`,复制现有英文/中文文件结构,补充对应翻译。
2. 在语言切换下拉中添加对应选项,例如:
- `data-lang="ja-JP"` / 文本 `日本語`
3. 无需修改 `i18n.js` 或现有 HTML/JS 逻辑,即可支持新语言。
---
## 十、注意事项与坑点
- **不要复制多份 HTML 模板** 来做多语言,那样维护成本极高,本方案统一由前端 i18n 控制。
- **避免 key 直接用中文/英文句子**,统一采用「模块.语义」短 key,便于 diff 与搜索。
- 避免在 CSS 中写死文本(如 `content: "xxx"`),如确有需要,应通过 JS 设置并走 i18n。
- 对于后端返回的可本地化错误文本(未来可能支持),优先由后端根据 `Accept-Language` 返回对应语言,前端只负责展示。
+1 -22
View File
@@ -345,29 +345,8 @@ func (mc *MemoryCompressor) adjustRecentStartForToolCalls(msgs []ChatMessage, re
adjusted--
}
// Ensure at least one user message is included in recent messages to avoid Qwen model error
// Qwen models require a user message in the message array, otherwise they return:
// "No user query found in messages"
hasUserMessage := false
for i := adjusted; i < len(msgs); i++ {
if strings.EqualFold(msgs[i].Role, "user") {
hasUserMessage = true
break
}
}
// If no user message in recent messages, adjust backwards to include one
if !hasUserMessage {
for adjusted > 0 {
adjusted--
if strings.EqualFold(msgs[adjusted].Role, "user") {
break
}
}
}
if adjusted != recentStart {
mc.logger.Debug("adjusted recent window to keep tool call context and user message",
mc.logger.Debug("adjusted recent window to keep tool call context",
zap.Int("original_recent_start", recentStart),
zap.Int("adjusted_recent_start", adjusted),
)
+16 -1
View File
@@ -442,6 +442,21 @@ func New(cfg *config.Config, log *logger.Logger) (*App, error) {
}
// mcpHandlerWithAuth 在鉴权通过后转发到 MCP 处理;若配置了 auth_header 则校验请求头,否则直接放行
func (a *App) mcpHandlerWithAuth(w http.ResponseWriter, r *http.Request) {
cfg := a.config.MCP
if cfg.AuthHeader != "" {
if r.Header.Get(cfg.AuthHeader) != cfg.AuthHeaderValue {
a.logger.Logger.Debug("MCP 鉴权失败:header 缺失或值不匹配", zap.String("header", cfg.AuthHeader))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(`{"error":"unauthorized"}`))
return
}
}
a.mcpServer.HandleHTTP(w, r)
}
// Run 启动应用
func (a *App) Run() error {
// 启动MCP服务器(如果启用)
@@ -451,7 +466,7 @@ func (a *App) Run() error {
a.logger.Info("启动MCP服务器", zap.String("address", mcpAddr))
mux := http.NewServeMux()
mux.HandleFunc("/mcp", a.mcpServer.HandleHTTP)
mux.HandleFunc("/mcp", a.mcpHandlerWithAuth)
if err := http.ListenAndServe(mcpAddr, mux); err != nil {
a.logger.Error("MCP服务器启动失败", zap.Error(err))
+125 -3
View File
@@ -3,6 +3,8 @@ package config
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"os"
"path/filepath"
@@ -74,9 +76,11 @@ type LogConfig struct {
}
type MCPConfig struct {
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
AuthHeader string `yaml:"auth_header,omitempty"` // 鉴权 header 名,留空表示不鉴权
AuthHeaderValue string `yaml:"auth_header_value,omitempty"` // 鉴权 header 值,需与请求中该 header 一致
}
type OpenAIConfig struct {
@@ -384,6 +388,124 @@ func PrintGeneratedPasswordWarning(password string, persisted bool, persistErr s
fmt.Println("----------------------------------------------------------------")
}
// generateRandomToken 生成用于 MCP 鉴权的随机字符串(64 位十六进制)
func generateRandomToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
// persistMCPAuth 将 MCP 的 auth_header / auth_header_value 写回配置文件
func persistMCPAuth(path string, mcp *MCPConfig) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
lines := strings.Split(string(data), "\n")
inMcpBlock := false
mcpIndent := -1
for i, line := range lines {
trimmed := strings.TrimSpace(line)
if !inMcpBlock {
if strings.HasPrefix(trimmed, "mcp:") {
inMcpBlock = true
mcpIndent = len(line) - len(strings.TrimLeft(line, " "))
}
continue
}
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
continue
}
leadingSpaces := len(line) - len(strings.TrimLeft(line, " "))
if leadingSpaces <= mcpIndent {
inMcpBlock = false
mcpIndent = -1
if strings.HasPrefix(trimmed, "mcp:") {
inMcpBlock = true
mcpIndent = leadingSpaces
}
continue
}
prefix := line[:leadingSpaces]
rest := strings.TrimSpace(line[leadingSpaces:])
comment := ""
if idx := strings.Index(line, "#"); idx >= 0 {
comment = strings.TrimRight(line[idx:], " ")
}
withComment := ""
if comment != "" {
if !strings.HasPrefix(comment, " ") {
withComment = " "
}
withComment += comment
}
if strings.HasPrefix(rest, "auth_header_value:") {
lines[i] = fmt.Sprintf("%sauth_header_value: %q%s", prefix, mcp.AuthHeaderValue, withComment)
} else if strings.HasPrefix(rest, "auth_header:") {
lines[i] = fmt.Sprintf("%sauth_header: %q%s", prefix, mcp.AuthHeader, withComment)
}
}
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644)
}
// EnsureMCPAuth 在 MCP 启用且 auth_header_value 为空时,自动生成随机密钥并写回配置
func EnsureMCPAuth(path string, cfg *Config) error {
if !cfg.MCP.Enabled || strings.TrimSpace(cfg.MCP.AuthHeaderValue) != "" {
return nil
}
token, err := generateRandomToken()
if err != nil {
return fmt.Errorf("生成 MCP 鉴权密钥失败: %w", err)
}
cfg.MCP.AuthHeaderValue = token
if strings.TrimSpace(cfg.MCP.AuthHeader) == "" {
cfg.MCP.AuthHeader = "X-MCP-Token"
}
return persistMCPAuth(path, &cfg.MCP)
}
// PrintMCPConfigJSON 向终端输出 MCP 配置的 JSON,可直接复制到 Cursor / Claude Code 的 mcp 配置中使用
func PrintMCPConfigJSON(mcp MCPConfig) {
if !mcp.Enabled {
return
}
hostForURL := strings.TrimSpace(mcp.Host)
if hostForURL == "" || hostForURL == "0.0.0.0" {
hostForURL = "localhost"
}
url := fmt.Sprintf("http://%s:%d/mcp", hostForURL, mcp.Port)
headers := map[string]string{}
if mcp.AuthHeader != "" {
headers[mcp.AuthHeader] = mcp.AuthHeaderValue
}
serverEntry := map[string]interface{}{
"url": url,
}
if len(headers) > 0 {
serverEntry["headers"] = headers
}
// Claude Code 需要 type: "http"
serverEntry["type"] = "http"
out := map[string]interface{}{
"mcpServers": map[string]interface{}{
"cyberstrike-ai": serverEntry,
},
}
b, _ := json.MarshalIndent(out, "", " ")
fmt.Println("[CyberStrikeAI] MCP 配置(可复制到 Cursor / Claude Code 使用):")
fmt.Println(" Cursor: 放入 ~/.cursor/mcp.json 的 mcpServers,或项目 .cursor/mcp.json")
fmt.Println(" Claude Code: 放入 .mcp.json 或 ~/.claude.json 的 mcpServers")
fmt.Println("----------------------------------------------------------------")
fmt.Println(string(b))
fmt.Println("----------------------------------------------------------------")
}
// LoadToolsFromDir 从目录加载所有工具配置文件
func LoadToolsFromDir(dir string) ([]ToolConfig, error) {
var tools []ToolConfig
+1
View File
@@ -4411,6 +4411,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
},
}
enrichSpecWithI18nKeys(spec)
c.JSON(http.StatusOK, spec)
}
+139
View File
@@ -0,0 +1,139 @@
package handler
// apiDocI18n 为 OpenAPI 文档提供 x-i18n-* 扩展键,供前端 apiDocs 国际化使用。
// 前端通过 apiDocs.tags.* / apiDocs.summary.* / apiDocs.response.* 翻译。
var apiDocI18nTagToKey = map[string]string{
"认证": "auth", "对话管理": "conversationManagement", "对话交互": "conversationInteraction",
"批量任务": "batchTasks", "对话分组": "conversationGroups", "漏洞管理": "vulnerabilityManagement",
"角色管理": "roleManagement", "Skills管理": "skillsManagement", "监控": "monitoring",
"配置管理": "configManagement", "外部MCP管理": "externalMCPManagement", "攻击链": "attackChain",
"知识库": "knowledgeBase", "MCP": "mcp",
}
var apiDocI18nSummaryToKey = map[string]string{
"用户登录": "login", "用户登出": "logout", "修改密码": "changePassword", "验证Token": "validateToken",
"创建对话": "createConversation", "列出对话": "listConversations", "查看对话详情": "getConversationDetail",
"更新对话": "updateConversation", "删除对话": "deleteConversation", "获取对话结果": "getConversationResult",
"发送消息并获取AI回复(非流式)": "sendMessageNonStream", "发送消息并获取AI回复(流式)": "sendMessageStream",
"取消任务": "cancelTask", "列出运行中的任务": "listRunningTasks", "列出已完成的任务": "listCompletedTasks",
"创建批量任务队列": "createBatchQueue", "列出批量任务队列": "listBatchQueues", "获取批量任务队列": "getBatchQueue",
"删除批量任务队列": "deleteBatchQueue", "启动批量任务队列": "startBatchQueue", "暂停批量任务队列": "pauseBatchQueue",
"添加任务到队列": "addTaskToQueue", "SQL注入扫描": "sqlInjectionScan", "端口扫描": "portScan",
"更新批量任务": "updateBatchTask", "删除批量任务": "deleteBatchTask",
"创建分组": "createGroup", "列出分组": "listGroups", "获取分组": "getGroup", "更新分组": "updateGroup",
"删除分组": "deleteGroup", "获取分组中的对话": "getGroupConversations", "添加对话到分组": "addConversationToGroup",
"从分组移除对话": "removeConversationFromGroup",
"列出漏洞": "listVulnerabilities", "创建漏洞": "createVulnerability", "获取漏洞统计": "getVulnerabilityStats",
"获取漏洞": "getVulnerability", "更新漏洞": "updateVulnerability", "删除漏洞": "deleteVulnerability",
"列出角色": "listRoles", "创建角色": "createRole", "获取角色": "getRole", "更新角色": "updateRole", "删除角色": "deleteRole",
"获取可用Skills列表": "getAvailableSkills", "列出Skills": "listSkills", "创建Skill": "createSkill",
"获取Skill统计": "getSkillStats", "清空Skill统计": "clearSkillStats", "获取Skill": "getSkill",
"更新Skill": "updateSkill", "删除Skill": "deleteSkill", "获取绑定角色": "getBoundRoles",
"获取监控信息": "getMonitorInfo", "获取执行记录": "getExecutionRecords", "删除执行记录": "deleteExecutionRecord",
"批量删除执行记录": "batchDeleteExecutionRecords", "获取统计信息": "getStats",
"获取配置": "getConfig", "更新配置": "updateConfig", "获取工具配置": "getToolConfig", "应用配置": "applyConfig",
"列出外部MCP": "listExternalMCP", "获取外部MCP统计": "getExternalMCPStats", "获取外部MCP": "getExternalMCP",
"添加或更新外部MCP": "addOrUpdateExternalMCP", "stdio模式配置": "stdioModeConfig", "SSE模式配置": "sseModeConfig",
"删除外部MCP": "deleteExternalMCP", "启动外部MCP": "startExternalMCP", "停止外部MCP": "stopExternalMCP",
"获取攻击链": "getAttackChain", "重新生成攻击链": "regenerateAttackChain",
"设置对话置顶": "pinConversation", "设置分组置顶": "pinGroup", "设置分组中对话的置顶": "pinGroupConversation",
"获取分类": "getCategories", "列出知识项": "listKnowledgeItems", "创建知识项": "createKnowledgeItem",
"获取知识项": "getKnowledgeItem", "更新知识项": "updateKnowledgeItem", "删除知识项": "deleteKnowledgeItem",
"获取索引状态": "getIndexStatus", "重建索引": "rebuildIndex", "扫描知识库": "scanKnowledgeBase",
"搜索知识库": "searchKnowledgeBase", "基础搜索": "basicSearch", "按风险类型搜索": "searchByRiskType",
"获取检索日志": "getRetrievalLogs", "删除检索日志": "deleteRetrievalLog",
"MCP端点": "mcpEndpoint", "列出所有工具": "listAllTools", "调用工具": "invokeTool", "初始化连接": "initConnection",
"成功响应": "successResponse", "错误响应": "errorResponse",
}
var apiDocI18nResponseDescToKey = map[string]string{
"获取成功": "getSuccess", "未授权": "unauthorized", "未授权,需要有效的Token": "unauthorizedToken",
"创建成功": "createSuccess", "请求参数错误": "badRequest", "对话不存在": "conversationNotFound",
"对话不存在或结果不存在": "conversationOrResultNotFound", "请求参数错误(如task为空)": "badRequestTaskEmpty",
"请求参数错误或分组名称已存在": "badRequestGroupNameExists", "分组不存在": "groupNotFound",
"请求参数错误(如配置格式不正确、缺少必需字段等)": "badRequestConfig",
"请求参数错误(如query为空)": "badRequestQueryEmpty", "方法不允许(仅支持POST请求)": "methodNotAllowed",
"登录成功": "loginSuccess", "密码错误": "invalidPassword", "登出成功": "logoutSuccess",
"密码修改成功": "passwordChanged", "Token有效": "tokenValid", "Token无效或已过期": "tokenInvalid",
"对话创建成功": "conversationCreated", "服务器内部错误": "internalError", "更新成功": "updateSuccess",
"删除成功": "deleteSuccess", "队列不存在": "queueNotFound", "启动成功": "startSuccess",
"暂停成功": "pauseSuccess", "添加成功": "addSuccess",
"任务不存在": "taskNotFound", "对话或分组不存在": "conversationOrGroupNotFound",
"取消请求已提交": "cancelSubmitted", "未找到正在执行的任务": "noRunningTask",
"消息发送成功,返回AI回复": "messageSent", "流式响应(Server-Sent Events": "streamResponse",
}
// enrichSpecWithI18nKeys 在 spec 的每个 operation 上写入 x-i18n-tags、x-i18n-summary
// 在每个 response 上写入 x-i18n-description,供前端按 key 做国际化。
func enrichSpecWithI18nKeys(spec map[string]interface{}) {
paths, _ := spec["paths"].(map[string]interface{})
if paths == nil {
return
}
for _, pathItem := range paths {
pm, _ := pathItem.(map[string]interface{})
if pm == nil {
continue
}
for _, method := range []string{"get", "post", "put", "delete", "patch"} {
opVal, ok := pm[method]
if !ok {
continue
}
op, _ := opVal.(map[string]interface{})
if op == nil {
continue
}
// x-i18n-tags: 与 tags 一一对应的 i18n 键数组(spec 中 tags 为 []string
switch tags := op["tags"].(type) {
case []string:
if len(tags) > 0 {
keys := make([]string, 0, len(tags))
for _, s := range tags {
if k := apiDocI18nTagToKey[s]; k != "" {
keys = append(keys, k)
} else {
keys = append(keys, s)
}
}
op["x-i18n-tags"] = keys
}
case []interface{}:
if len(tags) > 0 {
keys := make([]interface{}, 0, len(tags))
for _, t := range tags {
if s, ok := t.(string); ok {
if k := apiDocI18nTagToKey[s]; k != "" {
keys = append(keys, k)
} else {
keys = append(keys, s)
}
}
}
if len(keys) > 0 {
op["x-i18n-tags"] = keys
}
}
}
// x-i18n-summary
if summary, _ := op["summary"].(string); summary != "" {
if k := apiDocI18nSummaryToKey[summary]; k != "" {
op["x-i18n-summary"] = k
}
}
// responses -> 每个 status -> x-i18n-description
if respMap, _ := op["responses"].(map[string]interface{}); respMap != nil {
for _, rv := range respMap {
if r, _ := rv.(map[string]interface{}); r != nil {
if desc, _ := r["description"].(string); desc != "" {
if k := apiDocI18nResponseDescToKey[desc]; k != "" {
r["x-i18n-description"] = k
}
}
}
}
}
}
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ parameters:
- 确保目标地址格式正确
- 必需参数,不能为空
required: true
position: 0 # 位置参数,放在命令最后
position: 1 # 位置参数,必须放在命令最后nmap [options] target),用 1 确保在 flag 之后、最后添加
format: "positional"
- name: "ports"
type: "string"
+55
View File
@@ -529,6 +529,60 @@ header {
gap: 12px;
}
.lang-switcher {
position: relative;
}
.lang-switcher-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border-radius: 6px;
border: 1px solid var(--border-color);
background: var(--bg-primary);
color: var(--text-primary);
cursor: pointer;
font-size: 0.8125rem;
font-weight: 400;
transition: all 0.2s ease;
}
.lang-switcher-btn:hover {
background: var(--bg-tertiary);
border-color: var(--accent-color);
color: var(--accent-color);
}
.lang-switcher-icon {
font-size: 0.9rem;
}
.lang-dropdown {
position: absolute;
right: 0;
top: calc(100% + 6px);
min-width: 120px;
background: var(--bg-primary);
border-radius: 8px;
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
border: 1px solid var(--border-color);
padding: 4px 0;
z-index: 100;
}
.lang-option {
padding: 6px 12px;
font-size: 0.8125rem;
cursor: pointer;
white-space: nowrap;
}
.lang-option:hover {
background: var(--bg-tertiary);
color: var(--accent-color);
}
.header-actions button {
display: inline-flex;
align-items: center;
@@ -1748,6 +1802,7 @@ header {
.chat-input-container textarea::placeholder {
color: var(--text-muted);
opacity: 0.85;
}
.chat-input-container .send-btn {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+146 -61
View File
@@ -3,13 +3,74 @@
let apiSpec = null;
let currentToken = null;
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
function waitForI18n() {
return new Promise(function (resolve) {
if (window.t) return resolve();
var n = 0;
var iv = setInterval(function () {
if (window.t) { clearInterval(iv); resolve(); return; }
n++;
if (n >= 100) { clearInterval(iv); resolve(); }
}, 50);
});
}
// 从 OpenAPI spec 的 x-i18n-tags 构建 tag -> i18n key 映射(方案 A:后端提供键)
var apiSpecTagToKey = {};
function buildApiSpecTagToKey() {
apiSpecTagToKey = {};
if (!apiSpec || !apiSpec.paths) return;
Object.keys(apiSpec.paths).forEach(function (path) {
var pathItem = apiSpec.paths[path];
if (!pathItem || typeof pathItem !== 'object') return;
['get', 'post', 'put', 'delete', 'patch'].forEach(function (method) {
var op = pathItem[method];
if (!op || !op.tags || !op['x-i18n-tags']) return;
var tags = op.tags;
var keys = op['x-i18n-tags'];
for (var i = 0; i < tags.length && i < keys.length; i++) {
apiSpecTagToKey[tags[i]] = typeof keys[i] === 'string' ? keys[i] : keys[i];
}
});
});
}
function translateApiDocTag(tag) {
if (!tag) return tag;
var key = apiSpecTagToKey[tag];
return key ? _t('apiDocs.tags.' + key) : tag;
}
function translateApiDocSummaryFromOp(op) {
var key = op && op['x-i18n-summary'];
if (key) return _t('apiDocs.summary.' + key);
return op && op.summary ? op.summary : '';
}
function translateApiDocResponseDescFromResp(resp) {
if (!resp) return '';
var key = resp['x-i18n-description'];
if (key) return _t('apiDocs.response.' + key);
return resp.description || '';
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
await waitForI18n();
await loadToken();
await loadAPISpec();
if (apiSpec) {
renderAPIDocs();
}
document.addEventListener('languagechange', function () {
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(document);
}
if (apiSpec) {
renderAPIDocs();
}
});
});
// 加载token
@@ -43,22 +104,25 @@ async function loadAPISpec() {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 401) {
showError('需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。');
showError(_t('apiDocs.errorLoginRequired'));
return;
}
throw new Error('加载API规范失败: ' + response.status);
throw new Error(_t('apiDocs.errorLoadSpec') + response.status);
}
apiSpec = await response.json();
buildApiSpecTagToKey();
} catch (error) {
console.error('加载API规范失败:', error);
showError('加载API文档失败: ' + error.message);
showError(_t('apiDocs.errorLoadFailed') + error.message);
}
}
// 显示错误
function showError(message) {
const main = document.getElementById('api-docs-main');
const loadFailed = _t('apiDocs.loadFailed');
const backToLogin = _t('apiDocs.backToLogin');
main.innerHTML = `
<div class="empty-state">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@@ -66,10 +130,10 @@ function showError(message) {
<line x1="15" y1="9" x2="9" y2="15"/>
<line x1="9" y1="9" x2="15" y2="15"/>
</svg>
<h3>加载失败</h3>
<p>${message}</p>
<h3>${escapeHtml(loadFailed)}</h3>
<p>${escapeHtml(message)}</p>
<div style="margin-top: 16px;">
<a href="/" style="color: var(--accent-color); text-decoration: none;">返回首页登录</a>
<a href="/" style="color: var(--accent-color); text-decoration: none;">${escapeHtml(backToLogin)}</a>
</div>
</div>
`;
@@ -78,7 +142,7 @@ function showError(message) {
// 渲染API文档
function renderAPIDocs() {
if (!apiSpec || !apiSpec.paths) {
showError('API规范格式错误');
showError(_t('apiDocs.errorSpecInvalid'));
return;
}
@@ -109,7 +173,7 @@ function renderAuthInfo() {
tokenStatus.style.display = 'block';
tokenStatus.style.background = 'rgba(255, 152, 0, 0.1)';
tokenStatus.style.borderLeftColor = '#ff9800';
tokenStatus.innerHTML = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;"><strong>⚠ 未检测到 Token</strong> - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token</p>';
tokenStatus.innerHTML = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;">' + escapeHtml(_t('apiDocs.tokenNotDetected')) + '</p>';
}
}
@@ -127,11 +191,14 @@ function renderSidebar() {
const groupList = document.getElementById('api-group-list');
const allGroups = Array.from(groups).sort();
while (groupList.children.length > 1) {
groupList.removeChild(groupList.lastChild);
}
allGroups.forEach(group => {
const li = document.createElement('li');
li.className = 'api-group-item';
li.innerHTML = `<a href="#" class="api-group-link" data-group="${group}">${group}</a>`;
const groupLabel = translateApiDocTag(group);
li.innerHTML = `<a href="#" class="api-group-link" data-group="${escapeHtml(group)}">${escapeHtml(groupLabel)}</a>`;
groupList.appendChild(li);
});
@@ -176,7 +243,7 @@ function renderEndpoints(filterGroup = null) {
});
if (endpoints.length === 0) {
main.innerHTML = '<div class="empty-state"><h3>暂无API</h3><p>该分组下没有API端点</p></div>';
main.innerHTML = '<div class="empty-state"><h3>' + escapeHtml(_t('apiDocs.noApis')) + '</h3><p>' + escapeHtml(_t('apiDocs.noEndpointsInGroup')) + '</p></div>';
return;
}
@@ -192,8 +259,8 @@ function createEndpointCard(endpoint) {
const methodClass = endpoint.method.toLowerCase();
const tags = endpoint.tags || [];
const tagHtml = tags.map(tag => `<span class="api-tag">${tag}</span>`).join('');
const tagHtml = tags.map(tag => `<span class="api-tag">${escapeHtml(translateApiDocTag(tag))}</span>`).join('');
const summaryText = translateApiDocSummaryFromOp(endpoint);
card.innerHTML = `
<div class="api-endpoint-header">
<div class="api-endpoint-title">
@@ -204,21 +271,21 @@ function createEndpointCard(endpoint) {
</div>
<div class="api-endpoint-body">
<div class="api-section">
<div class="api-section-title">描述</div>
${endpoint.summary ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(endpoint.summary)}</div>` : ''}
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionDescription'))}</div>
${summaryText ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(summaryText)}</div>` : ''}
${endpoint.description ? `
<div class="api-description-toggle">
<button class="description-toggle-btn" onclick="toggleDescription(this)">
<svg class="description-toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
<span>查看详细说明</span>
<span>${escapeHtml(_t('apiDocs.viewDetailDesc'))}</span>
</button>
<div class="api-description-detail" style="display: none;">
${formatDescription(endpoint.description)}
</div>
</div>
` : endpoint.summary ? '' : '<div class="api-description">无描述</div>'}
` : endpoint.summary ? '' : '<div class="api-description">' + escapeHtml(_t('apiDocs.noDescription')) + '</div>'}
</div>
${renderParameters(endpoint)}
@@ -236,8 +303,10 @@ function renderParameters(endpoint) {
const params = endpoint.parameters || [];
if (params.length === 0) return '';
const requiredLabel = escapeHtml(_t('apiDocs.required'));
const optionalLabel = escapeHtml(_t('apiDocs.optional'));
const rows = params.map(param => {
const required = param.required ? '<span class="api-param-required">必需</span>' : '<span class="api-param-optional">可选</span>';
const required = param.required ? '<span class="api-param-required">' + requiredLabel + '</span>' : '<span class="api-param-optional">' + optionalLabel + '</span>';
// 处理描述文本,将换行符转换为<br>
let descriptionHtml = '-';
if (param.description) {
@@ -255,17 +324,20 @@ function renderParameters(endpoint) {
`;
}).join('');
const paramName = escapeHtml(_t('apiDocs.paramName'));
const typeLabel = escapeHtml(_t('apiDocs.type'));
const descLabel = escapeHtml(_t('apiDocs.description'));
return `
<div class="api-section">
<div class="api-section-title">参数</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionParams'))}</div>
<div class="api-table-wrapper">
<table class="api-params-table">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>必需</th>
<th>${paramName}</th>
<th>${typeLabel}</th>
<th>${descLabel}</th>
<th>${requiredLabel}</th>
</tr>
</thead>
<tbody>
@@ -297,11 +369,13 @@ function renderRequestBody(endpoint) {
let paramsTable = '';
if (schema.properties) {
const requiredFields = schema.required || [];
const reqLabel = escapeHtml(_t('apiDocs.required'));
const optLabel = escapeHtml(_t('apiDocs.optional'));
const rows = Object.keys(schema.properties).map(key => {
const prop = schema.properties[key];
const required = requiredFields.includes(key)
? '<span class="api-param-required">必需</span>'
: '<span class="api-param-optional">可选</span>';
? '<span class="api-param-required">' + reqLabel + '</span>'
: '<span class="api-param-optional">' + optLabel + '</span>';
// 处理嵌套类型
let typeDisplay = prop.type || 'object';
@@ -338,16 +412,20 @@ function renderRequestBody(endpoint) {
}).join('');
if (rows) {
const pName = escapeHtml(_t('apiDocs.paramName'));
const tLabel = escapeHtml(_t('apiDocs.type'));
const dLabel = escapeHtml(_t('apiDocs.description'));
const exLabel = escapeHtml(_t('apiDocs.example'));
paramsTable = `
<div class="api-table-wrapper" style="margin-top: 12px;">
<table class="api-params-table">
<thead>
<tr>
<th>参数名</th>
<th>类型</th>
<th>描述</th>
<th>必需</th>
<th>示例</th>
<th>${pName}</th>
<th>${tLabel}</th>
<th>${dLabel}</th>
<th>${reqLabel}</th>
<th>${exLabel}</th>
</tr>
</thead>
<tbody>
@@ -389,12 +467,12 @@ function renderRequestBody(endpoint) {
return `
<div class="api-section">
<div class="api-section-title">请求体</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionRequestBody'))}</div>
${endpoint.requestBody.description ? `<div class="api-description">${endpoint.requestBody.description}</div>` : ''}
${paramsTable}
${example ? `
<div style="margin-top: 16px;">
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">示例JSON:</div>
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(_t('apiDocs.exampleJson'))}</div>
<div class="api-response-example">
<pre>${escapeHtml(example)}</pre>
</div>
@@ -414,11 +492,11 @@ function renderResponses(endpoint) {
if (schema.example) {
example = JSON.stringify(schema.example, null, 2);
}
const descText = translateApiDocResponseDescFromResp(response);
return `
<div style="margin-bottom: 16px;">
<strong style="color: ${status.startsWith('2') ? 'var(--success-color)' : status.startsWith('4') ? 'var(--error-color)' : 'var(--warning-color)'}">${status}</strong>
${response.description ? `<span style="color: var(--text-secondary); margin-left: 8px;">${response.description}</span>` : ''}
${descText ? `<span style="color: var(--text-secondary); margin-left: 8px;">${escapeHtml(descText)}</span>` : ''}
${example ? `
<div class="api-response-example" style="margin-top: 8px;">
<pre>${escapeHtml(example)}</pre>
@@ -432,7 +510,7 @@ function renderResponses(endpoint) {
return `
<div class="api-section">
<div class="api-section-title">响应</div>
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionResponse'))}</div>
${responseItems}
</div>
`;
@@ -462,8 +540,8 @@ function renderTestSection(endpoint) {
const bodyInputId = `test-body-${escapeId(path)}-${method}`;
bodyInput = `
<div class="api-test-input-group">
<label>请求体 (JSON)</label>
<textarea id="${bodyInputId}" class="test-body-input" placeholder='请输入JSON格式的请求体'>${defaultBody}</textarea>
<label>${escapeHtml(_t('apiDocs.requestBodyJson'))}</label>
<textarea id="${bodyInputId}" class="test-body-input" placeholder='${escapeHtml(_t('apiDocs.requestBodyPlaceholder'))}'>${defaultBody}</textarea>
</div>
`;
}
@@ -491,7 +569,7 @@ function renderTestSection(endpoint) {
const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`;
const defaultValue = param.schema?.default !== undefined ? param.schema.default : '';
const placeholder = param.description || param.name;
const required = param.required ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">可选</span>';
const required = param.required ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">' + escapeHtml(_t('apiDocs.optional')) + '</span>';
return `
<div class="api-test-input-group">
<label>${param.name} ${required}</label>
@@ -505,33 +583,40 @@ function renderTestSection(endpoint) {
}).join('');
}
const testSectionTitle = escapeHtml(_t('apiDocs.testSection'));
const queryParamsTitle = escapeHtml(_t('apiDocs.queryParams'));
const sendRequestLabel = escapeHtml(_t('apiDocs.sendRequest'));
const copyCurlLabel = escapeHtml(_t('apiDocs.copyCurl'));
const clearResultLabel = escapeHtml(_t('apiDocs.clearResult'));
const copyCurlTitle = escapeHtml(_t('apiDocs.copyCurlTitle'));
const clearResultTitle = escapeHtml(_t('apiDocs.clearResultTitle'));
return `
<div class="api-test-section">
<div class="api-section-title">测试接口</div>
<div class="api-section-title">${testSectionTitle}</div>
<div class="api-test-form">
${pathParamsInput}
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">查询参数:</div>${queryParamsInput}</div>` : ''}
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${queryParamsTitle}</div>${queryParamsInput}</div>` : ''}
${bodyInput}
<div class="api-test-buttons">
<button class="api-test-btn primary" onclick="testAPI('${method}', '${escapeHtml(path)}', '${endpoint.operationId || ''}')">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
发送请求
${sendRequestLabel}
</button>
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="复制curl命令">
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="${copyCurlTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
</svg>
复制curl
${copyCurlLabel}
</button>
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="清除测试结果">
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="${clearResultTitle}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
清除结果
${clearResultLabel}
</button>
</div>
<div id="test-result-${escapeId(path)}-${method}" class="api-test-result" style="display: none;"></div>
@@ -548,7 +633,7 @@ async function testAPI(method, path, operationId) {
resultDiv.style.display = 'block';
resultDiv.className = 'api-test-result loading';
resultDiv.textContent = '发送请求中...';
resultDiv.textContent = _t('apiDocs.sendingRequest');
try {
// 替换路径参数
@@ -561,7 +646,7 @@ async function testAPI(method, path, operationId) {
if (input && input.value) {
actualPath = actualPath.replace(param, encodeURIComponent(input.value));
} else {
throw new Error(`路径参数 ${paramName} 不能为空`);
throw new Error(_t('apiDocs.errorPathParamRequired', { name: paramName }));
}
});
@@ -580,7 +665,7 @@ async function testAPI(method, path, operationId) {
if (input && input.value !== '' && input.value !== null && input.value !== undefined) {
queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`);
} else if (param.required) {
throw new Error(`查询参数 ${param.name} 不能为空`);
throw new Error(_t('apiDocs.errorQueryParamRequired', { name: param.name }));
}
});
}
@@ -602,8 +687,7 @@ async function testAPI(method, path, operationId) {
if (currentToken) {
options.headers['Authorization'] = 'Bearer ' + currentToken;
} else {
// 如果没有token,提示用户
throw new Error('未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token');
throw new Error(_t('apiDocs.errorTokenRequired'));
}
// 添加请求体
@@ -614,7 +698,7 @@ async function testAPI(method, path, operationId) {
try {
options.body = JSON.stringify(JSON.parse(bodyInput.value.trim()));
} catch (e) {
throw new Error('请求体JSON格式错误: ' + e.message);
throw new Error(_t('apiDocs.errorJsonInvalid') + e.message);
}
}
}
@@ -636,7 +720,7 @@ async function testAPI(method, path, operationId) {
} catch (error) {
resultDiv.className = 'api-test-result error';
resultDiv.textContent = '请求失败: ' + error.message;
resultDiv.textContent = _t('apiDocs.requestFailed') + error.message;
}
}
@@ -727,17 +811,17 @@ function copyCurlCommand(event, method, path) {
// 复制到剪贴板
const button = event ? event.target.closest('button') : null;
navigator.clipboard.writeText(curlCommand).then(() => {
// 显示成功提示
if (button) {
const originalText = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
const copiedLabel = escapeHtml(_t('apiDocs.copied'));
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>' + copiedLabel;
button.style.color = 'var(--success-color)';
setTimeout(() => {
button.innerHTML = originalText;
button.style.color = '';
}, 2000);
} else {
alert('curl命令已复制到剪贴板!');
alert(_t('apiDocs.curlCopied'));
}
}).catch(err => {
console.error('复制失败:', err);
@@ -752,24 +836,25 @@ function copyCurlCommand(event, method, path) {
document.execCommand('copy');
if (button) {
const originalText = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
const copiedLabel = escapeHtml(_t('apiDocs.copied'));
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>' + copiedLabel;
button.style.color = 'var(--success-color)';
setTimeout(() => {
button.innerHTML = originalText;
button.style.color = '';
}, 2000);
} else {
alert('curl命令已复制到剪贴板!');
alert(_t('apiDocs.curlCopied'));
}
} catch (e) {
alert('复制失败,请手动复制:\n\n' + curlCommand);
alert(_t('apiDocs.copyFailedManual') + curlCommand);
}
document.body.removeChild(textarea);
});
} catch (error) {
console.error('生成curl命令失败:', error);
alert('生成curl命令失败: ' + error.message);
alert(_t('apiDocs.curlGenFailed') + error.message);
}
}
@@ -935,10 +1020,10 @@ function toggleDescription(button) {
if (detail.style.display === 'none') {
detail.style.display = 'block';
icon.style.transform = 'rotate(180deg)';
span.textContent = '隐藏详细说明';
span.textContent = typeof window.t === 'function' ? window.t('apiDocs.hideDetailDesc') : '隐藏详细说明';
} else {
detail.style.display = 'none';
icon.style.transform = 'rotate(0deg)';
span.textContent = '查看详细说明';
span.textContent = typeof window.t === 'function' ? window.t('apiDocs.viewDetailDesc') : '查看详细说明';
}
}
+34 -14
View File
@@ -123,12 +123,20 @@ async function ensureAuthenticated() {
return true;
}
function handleUnauthorized({ message = '认证已过期,请重新登录', silent = false } = {}) {
function handleUnauthorized({ message = null, silent = false } = {}) {
clearAuthStorage();
authPromise = null;
authPromiseResolvers = [];
let finalMessage = message;
if (!finalMessage) {
if (typeof window !== 'undefined' && typeof window.t === 'function') {
finalMessage = window.t('auth.sessionExpired');
} else {
finalMessage = '认证已过期,请重新登录';
}
}
if (!silent) {
showLoginOverlay(message);
showLoginOverlay(finalMessage);
} else {
showLoginOverlay();
}
@@ -147,7 +155,10 @@ async function apiFetch(url, options = {}) {
const response = await fetch(url, opts);
if (response.status === 401) {
handleUnauthorized();
throw new Error('未授权访问');
const msg = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('auth.unauthorized')
: '未授权访问';
throw new Error(msg);
}
return response;
}
@@ -165,7 +176,10 @@ async function submitLogin(event) {
const password = passwordInput.value.trim();
if (!password) {
if (errorBox) {
errorBox.textContent = '请输入密码';
const msgEmpty = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('auth.enterPassword')
: '请输入密码';
errorBox.textContent = msgEmpty;
errorBox.style.display = 'block';
}
return;
@@ -186,7 +200,10 @@ async function submitLogin(event) {
const result = await response.json().catch(() => ({}));
if (!response.ok || !result.token) {
if (errorBox) {
errorBox.textContent = result.error || '登录失败,请检查密码';
const fallback = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('auth.loginFailedCheck')
: '登录失败,请检查密码';
errorBox.textContent = result.error || fallback;
errorBox.style.display = 'block';
}
return;
@@ -203,7 +220,10 @@ async function submitLogin(event) {
} catch (error) {
console.error('登录失败:', error);
if (errorBox) {
errorBox.textContent = '登录失败,请稍后重试';
const fallback = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('auth.loginFailedRetry')
: '登录失败,请稍后重试';
errorBox.textContent = fallback;
errorBox.style.display = 'block';
}
} finally {
@@ -230,13 +250,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) {
@@ -375,7 +395,7 @@ async function logout() {
// 无论如何都清除本地认证信息
clearAuthStorage();
hideLoginOverlay();
showLoginOverlay('已退出登录');
showLoginOverlay(typeof window.t === 'function' ? window.t('auth.loggedOut') : '已退出登录');
}
}
+201 -108
View File
@@ -44,10 +44,15 @@ function saveChatDraftDebounced(content) {
// 保存输入框草稿到localStorage
function saveChatDraft(content) {
try {
if (content && content.trim().length > 0) {
const chatInput = document.getElementById('chat-input');
const placeholderText = chatInput ? (chatInput.getAttribute('placeholder') || '').trim() : '';
const trimmed = (content || '').trim();
// 不要把占位提示本身当作草稿保存
if (trimmed && (!placeholderText || trimmed !== placeholderText)) {
localStorage.setItem(DRAFT_STORAGE_KEY, content);
} else {
// 如果内容为空,清除保存的草稿
// 如果内容为空或等于占位提示,清除保存的草稿
localStorage.removeItem(DRAFT_STORAGE_KEY);
}
} catch (error) {
@@ -63,17 +68,27 @@ function restoreChatDraft() {
if (!chatInput) {
return;
}
const placeholderText = (chatInput.getAttribute('placeholder') || '').trim();
// 若当前 value 与 placeholder 相同,说明提示被误当作内容,清空以便正确显示占位符
if (placeholderText && chatInput.value.trim() === placeholderText) {
chatInput.value = '';
}
// 如果输入框已有内容,不恢复草稿(避免覆盖用户输入)
if (chatInput.value && chatInput.value.trim().length > 0) {
return;
}
const draft = localStorage.getItem(DRAFT_STORAGE_KEY);
if (draft && draft.trim().length > 0) {
const trimmedDraft = draft ? draft.trim() : '';
// 如果草稿内容和占位提示一样,则认为是无效草稿,不恢复
if (trimmedDraft && (!placeholderText || trimmedDraft !== placeholderText)) {
chatInput.value = draft;
// 调整输入框高度以适应内容
adjustTextareaHeight(chatInput);
} else if (trimmedDraft && placeholderText && trimmedDraft === placeholderText) {
// 清理掉无效草稿,避免之后继续干扰
localStorage.removeItem(DRAFT_STORAGE_KEY);
}
} catch (error) {
console.warn('恢复草稿失败:', error);
@@ -263,7 +278,7 @@ function renderChatFileChips() {
const remove = document.createElement('button');
remove.type = 'button';
remove.className = 'chat-file-chip-remove';
remove.title = '移除';
remove.title = typeof window.t === 'function' ? window.t('chatGroup.remove') : '移除';
remove.innerHTML = '×';
remove.setAttribute('aria-label', '移除 ' + a.fileName);
remove.addEventListener('click', () => removeChatAttachment(i));
@@ -720,14 +735,14 @@ function renderMentionSuggestions({ showLoading = false } = {}) {
const previousScrollTop = canPreserveScroll ? existingList.scrollTop : 0;
if (showLoading) {
mentionSuggestionsEl.innerHTML = '<div class="mention-empty">正在加载工具...</div>';
mentionSuggestionsEl.innerHTML = '<div class="mention-empty">' + (typeof window.t === 'function' ? window.t('chat.loadingTools') : '正在加载工具...') + '</div>';
mentionSuggestionsEl.style.display = 'block';
delete mentionSuggestionsEl.dataset.lastMentionQuery;
return;
}
if (!mentionFilteredTools.length) {
mentionSuggestionsEl.innerHTML = '<div class="mention-empty">没有匹配的工具</div>';
mentionSuggestionsEl.innerHTML = '<div class="mention-empty">' + (typeof window.t === 'function' ? window.t('chat.noMatchTools') : '没有匹配的工具') + '</div>';
mentionSuggestionsEl.style.display = 'block';
mentionSuggestionsEl.dataset.lastMentionQuery = currentQuery;
return;
@@ -740,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 ? '已禁用(当前角色)' : '已禁用');
@@ -937,7 +952,8 @@ function initializeChatUI() {
const messagesDiv = document.getElementById('chat-messages');
if (messagesDiv && messagesDiv.childElementCount === 0) {
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
const readyMsg = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
addMessage('assistant', readyMsg);
}
addAttackChainButton(currentConversationId);
@@ -1040,12 +1056,23 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
}
};
// 助手消息中的已知中文错误前缀做国际化替换(后端固定返回中文)
let displayContent = content;
if (role === 'assistant' && typeof displayContent === 'string' && typeof window.t === 'function') {
if (displayContent.indexOf('执行失败: ') === 0) {
displayContent = window.t('chat.executeFailed') + ': ' + displayContent.slice('执行失败: '.length);
}
if (displayContent.indexOf('调用OpenAI失败:') !== -1) {
displayContent = displayContent.replace(/调用OpenAI失败:/g, window.t('chat.callOpenAIFailed') + ':');
}
}
// 对于用户消息,直接转义HTML,不进行Markdown解析,以保留所有特殊字符
if (role === 'user') {
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
} else if (typeof DOMPurify !== 'undefined') {
// 直接解析Markdown(代码块会被包裹在<code>/<pre>中,DOMPurify会保留其文本内容)
let parsedContent = parseMarkdown(content);
let parsedContent = parseMarkdown(role === 'assistant' ? displayContent : content);
if (!parsedContent) {
parsedContent = content;
}
@@ -1087,14 +1114,16 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
formattedContent = DOMPurify.sanitize(parsedContent, defaultSanitizeConfig);
} else if (typeof marked !== 'undefined') {
const parsedContent = parseMarkdown(content);
const rawForParse = role === 'assistant' ? displayContent : content;
const parsedContent = parseMarkdown(rawForParse);
if (parsedContent) {
formattedContent = parsedContent;
} else {
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
formattedContent = escapeHtml(rawForParse).replace(/\n/g, '<br>');
}
} else {
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
const rawForEscape = role === 'assistant' ? displayContent : content;
formattedContent = escapeHtml(rawForEscape).replace(/\n/g, '<br>');
}
bubble.innerHTML = formattedContent;
@@ -1129,8 +1158,8 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
if (role === 'assistant') {
const copyBtn = document.createElement('button');
copyBtn.className = 'message-copy-btn';
copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>复制</span>';
copyBtn.title = '复制消息内容';
copyBtn.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>' + (typeof window.t === 'function' ? window.t('common.copy') : '复制') + '</span>';
copyBtn.title = typeof window.t === 'function' ? window.t('chat.copyMessageTitle') : '复制消息内容';
copyBtn.onclick = function(e) {
e.stopPropagation();
copyMessageToClipboard(messageDiv, this);
@@ -1159,7 +1188,8 @@ 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';
timeDiv.textContent = messageTime.toLocaleTimeString(msgTimeLocale, { hour: '2-digit', minute: '2-digit' });
contentWrapper.appendChild(timeDiv);
// 如果有MCP执行ID或进度ID,添加查看详情区域(统一使用"渗透测试详情"样式)
@@ -1169,7 +1199,7 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
const mcpLabel = document.createElement('div');
mcpLabel.className = 'mcp-call-label';
mcpLabel.textContent = '📋 渗透测试详情';
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
mcpSection.appendChild(mcpLabel);
const buttonsContainer = document.createElement('div');
@@ -1180,7 +1210,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);
// 异步获取工具名称并更新按钮文本
@@ -1192,7 +1222,7 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
if (progressId) {
const progressDetailBtn = document.createElement('button');
progressDetailBtn.className = 'mcp-detail-btn process-detail-btn';
progressDetailBtn.innerHTML = '<span>展开详情</span>';
progressDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
progressDetailBtn.onclick = () => toggleProcessDetails(progressId, messageDiv.id);
buttonsContainer.appendChild(progressDetailBtn);
// 存储进度ID到消息元素
@@ -1236,7 +1266,7 @@ function copyMessageToClipboard(messageDiv, button) {
showCopySuccess(button);
}).catch(err => {
console.error('复制失败:', err);
alert('复制失败,请手动选择内容复制');
alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制');
});
}
return;
@@ -1247,11 +1277,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') : '复制失败,请手动选择内容复制');
}
}
@@ -1259,7 +1289,7 @@ function copyMessageToClipboard(messageDiv, button) {
function showCopySuccess(button) {
if (button) {
const originalText = button.innerHTML;
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>已复制</span>';
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>' + (typeof window.t === 'function' ? window.t('common.copied') : '已复制') + '</span>';
button.style.color = '#10b981';
button.style.background = 'rgba(16, 185, 129, 0.1)';
button.style.borderColor = 'rgba(16, 185, 129, 0.3)';
@@ -1301,11 +1331,11 @@ function renderProcessDetails(messageId, processDetails) {
if (!mcpLabel && !buttonsContainer) {
mcpLabel = document.createElement('div');
mcpLabel.className = 'mcp-call-label';
mcpLabel.textContent = '📋 渗透测试详情';
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
mcpSection.appendChild(mcpLabel);
} else if (mcpLabel && mcpLabel.textContent !== '📋 渗透测试详情') {
} else if (mcpLabel && mcpLabel.textContent !== ('📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情'))) {
// 如果标签存在但不是统一格式,更新它
mcpLabel.textContent = '📋 渗透测试详情';
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
}
// 如果没有按钮容器,创建一个
@@ -1320,7 +1350,7 @@ function renderProcessDetails(messageId, processDetails) {
if (!processDetailBtn) {
processDetailBtn = document.createElement('button');
processDetailBtn.className = 'mcp-detail-btn process-detail-btn';
processDetailBtn.innerHTML = '<span>展开详情</span>';
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
processDetailBtn.onclick = () => toggleProcessDetails(null, messageId);
buttonsContainer.appendChild(processDetailBtn);
}
@@ -1360,7 +1390,7 @@ function renderProcessDetails(messageId, processDetails) {
// 如果没有processDetails或为空,显示空状态
if (!processDetails || processDetails.length === 0) {
// 显示空状态提示
timeline.innerHTML = '<div class="progress-timeline-empty">暂无过程详情(可能执行过快或未触发详细事件)</div>';
timeline.innerHTML = '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>';
// 默认折叠
timeline.classList.remove('expanded');
return;
@@ -1379,32 +1409,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, {
@@ -1425,7 +1456,7 @@ function renderProcessDetails(messageId, processDetails) {
// 更新按钮文本为"展开详情"
const processDetailBtn = messageElement.querySelector('.process-detail-btn');
if (processDetailBtn) {
processDetailBtn.innerHTML = '<span>展开详情</span>';
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
}
}
}
@@ -1480,7 +1511,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}`;
@@ -1499,14 +1530,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)
: '—';
// 请求参数
@@ -1569,22 +1601,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);
}
}
@@ -1679,7 +1711,8 @@ async function startNewConversation() {
currentConversationId = null;
currentConversationGroupId = null; // 新对话不属于任何分组
document.getElementById('chat-messages').innerHTML = '';
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
const readyMsgNew = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
addMessage('assistant', readyMsgNew);
addAttackChainButton(null);
updateActiveConversation();
// 刷新分组列表,清除分组高亮
@@ -1719,12 +1752,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;
}
@@ -1732,6 +1766,7 @@ async function loadConversations(searchQuery = '') {
if (!Array.isArray(conversations) || conversations.length === 0) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -1797,6 +1832,7 @@ async function loadConversations(searchQuery = '') {
if (!rendered) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -1815,8 +1851,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);
}
}
}
@@ -1917,28 +1954,30 @@ 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') : '昨天';
if (messageDate.getTime() === referenceToday.getTime()) {
return dateObj.toLocaleTimeString('zh-CN', {
return dateObj.toLocaleTimeString(fmtLocale, {
hour: '2-digit',
minute: '2-digit'
});
}
if (messageDate.getTime() === referenceYesterday.getTime()) {
return '昨天 ' + dateObj.toLocaleTimeString('zh-CN', {
return yesterdayLabel + ' ' + dateObj.toLocaleTimeString(fmtLocale, {
hour: '2-digit',
minute: '2-digit'
});
}
if (dateObj.getFullYear() === referenceToday.getFullYear()) {
return dateObj.toLocaleString('zh-CN', {
return dateObj.toLocaleString(fmtLocale, {
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
}
return dateObj.toLocaleString('zh-CN', {
return dateObj.toLocaleString(fmtLocale, {
year: 'numeric',
month: 'short',
day: 'numeric',
@@ -2087,7 +2126,8 @@ async function loadConversation(conversationId) {
}
});
} else {
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
const readyMsgEmpty = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
addMessage('assistant', readyMsgEmpty);
}
// 滚动到底部
@@ -2127,7 +2167,8 @@ async function deleteConversation(conversationId, skipConfirm = false) {
if (conversationId === currentConversationId) {
currentConversationId = null;
document.getElementById('chat-messages').innerHTML = '';
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
const readyMsgLoad = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
addMessage('assistant', readyMsgLoad);
addAttackChainButton(null);
}
@@ -2219,7 +2260,7 @@ async function showAttackChain(conversationId) {
// 清空容器
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>';
}
// 隐藏详情面板
@@ -2319,7 +2360,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);
@@ -2345,7 +2386,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;
}
@@ -3962,12 +4003,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;
}
@@ -3975,6 +4017,7 @@ async function loadConversationsWithGroups(searchQuery = '') {
if (!Array.isArray(conversations) || conversations.length === 0) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -4036,6 +4079,7 @@ async function loadConversationsWithGroups(searchQuery = '') {
if (fragment.children.length === 0) {
listContainer.innerHTML = emptyStateHtml;
if (typeof window.applyTranslations === 'function') window.applyTranslations(listContainer);
return;
}
@@ -4054,8 +4098,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);
}
}
}
@@ -4171,13 +4216,13 @@ async function showConversationContextMenu(event) {
attackChainMenuItem.style.opacity = '1';
attackChainMenuItem.style.cursor = 'pointer';
attackChainMenuItem.onclick = showAttackChainFromContext;
attackChainMenuItem.title = '查看当前对话的攻击链';
attackChainMenuItem.title = (typeof window.t === 'function' ? window.t('chat.viewAttackChainCurrentConv') : '查看当前对话的攻击链');
}
} else {
attackChainMenuItem.style.opacity = '0.5';
attackChainMenuItem.style.cursor = 'not-allowed';
attackChainMenuItem.onclick = null;
attackChainMenuItem.title = '请选择一个对话以查看攻击链';
attackChainMenuItem.title = (typeof window.t === 'function' ? window.t('chat.viewAttackChainSelectConv') : '请选择一个对话以查看攻击链');
}
}
@@ -4210,21 +4255,25 @@ async function showConversationContextMenu(event) {
// 更新菜单文本
const pinMenuText = document.getElementById('pin-conversation-menu-text');
if (pinMenuText) {
if (pinMenuText && typeof window.t === 'function') {
pinMenuText.textContent = isPinned ? window.t('contextMenu.unpinConversation') : window.t('contextMenu.pinConversation');
} else if (pinMenuText) {
pinMenuText.textContent = isPinned ? '取消置顶' : '置顶此对话';
}
} catch (error) {
console.error('获取对话置顶状态失败:', error);
// 如果获取失败,使用默认文本
const pinMenuText = document.getElementById('pin-conversation-menu-text');
if (pinMenuText) {
if (pinMenuText && typeof window.t === 'function') {
pinMenuText.textContent = window.t('contextMenu.pinConversation');
} else if (pinMenuText) {
pinMenuText.textContent = '置顶此对话';
}
}
} else {
// 如果没有对话ID,使用默认文本
const pinMenuText = document.getElementById('pin-conversation-menu-text');
if (pinMenuText) {
if (pinMenuText && typeof window.t === 'function') {
pinMenuText.textContent = window.t('contextMenu.pinConversation');
} else if (pinMenuText) {
pinMenuText.textContent = '置顶此对话';
}
}
@@ -4333,14 +4382,17 @@ async function showGroupContextMenu(event, groupId) {
// 更新菜单文本
const pinMenuText = document.getElementById('pin-group-menu-text');
if (pinMenuText) {
if (pinMenuText && typeof window.t === 'function') {
pinMenuText.textContent = isPinned ? window.t('contextMenu.unpinGroup') : window.t('contextMenu.pinGroup');
} else if (pinMenuText) {
pinMenuText.textContent = isPinned ? '取消置顶' : '置顶此分组';
}
} catch (error) {
console.error('获取分组置顶状态失败:', error);
// 如果获取失败,使用默认文本
const pinMenuText = document.getElementById('pin-group-menu-text');
if (pinMenuText) {
if (pinMenuText && typeof window.t === 'function') {
pinMenuText.textContent = window.t('contextMenu.pinGroup');
} else if (pinMenuText) {
pinMenuText.textContent = '置顶此分组';
}
}
@@ -4443,7 +4495,9 @@ async function renameConversation() {
loadConversationsWithGroups();
} catch (error) {
console.error('重命名对话失败:', error);
alert('重命名失败: ' + (error.message || '未知错误'));
const failedLabel = typeof window.t === 'function' ? window.t('chat.renameFailed') : '重命名失败';
const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
alert(failedLabel + ': ' + (error.message || unknownErr));
}
closeContextMenu();
@@ -4636,13 +4690,14 @@ async function showMoveToGroupSubmenu() {
}
// 始终显示"创建分组"选项
const addGroupLabel = typeof window.t === 'function' ? window.t('chat.addNewGroup') : '+ 新增分组';
const addItem = document.createElement('div');
addItem.className = 'context-submenu-item add-group-item';
addItem.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span>+ 新增分组</span>
<span>${addGroupLabel}</span>
`;
addItem.onclick = () => {
showCreateGroupModal(true);
@@ -4917,7 +4972,8 @@ function deleteConversationFromContext() {
const convId = contextMenuConversationId;
if (!convId) return;
if (confirm('确定要删除此对话吗?')) {
const confirmMsg = typeof window.t === 'function' ? window.t('chat.deleteConversationConfirm') : '确定要删除此对话吗?';
if (confirm(confirmMsg)) {
deleteConversation(convId, true); // 跳过内部确认,因为这里已经确认过了
}
closeContextMenu();
@@ -4944,6 +5000,15 @@ function closeContextMenu() {
// 显示批量管理模态框
let allConversationsForBatch = [];
// 更新批量管理模态框标题(含条数),支持 i18n;count 为当前条数
function updateBatchManageTitle(count) {
const titleEl = document.getElementById('batch-manage-title');
if (!titleEl || typeof window.t !== 'function') return;
const template = window.t('batchManageModal.title', { count: '__C__' });
const parts = template.split('__C__');
titleEl.innerHTML = (parts[0] || '') + '<span id="batch-manage-count">' + (count || 0) + '</span>' + (parts[1] || '');
}
async function showBatchManageModal() {
try {
const response = await apiFetch('/api/conversations?limit=1000');
@@ -4957,10 +5022,7 @@ async function showBatchManageModal() {
}
const modal = document.getElementById('batch-manage-modal');
const countEl = document.getElementById('batch-manage-count');
if (countEl) {
countEl.textContent = allConversationsForBatch.length;
}
updateBatchManageTitle(allConversationsForBatch.length);
renderBatchConversations();
if (modal) {
@@ -4971,10 +5033,7 @@ async function showBatchManageModal() {
// 错误时使用空数组,不显示错误提示(更友好的用户体验)
allConversationsForBatch = [];
const modal = document.getElementById('batch-manage-modal');
const countEl = document.getElementById('batch-manage-count');
if (countEl) {
countEl.textContent = 0;
}
updateBatchManageTitle(0);
if (modal) {
renderBatchConversations();
modal.style.display = 'flex';
@@ -5041,7 +5100,7 @@ function renderBatchConversations(filtered = null) {
const name = document.createElement('div');
name.className = 'batch-table-col-name';
const originalTitle = conv.title || '未命名对话';
const originalTitle = conv.title || (typeof window.t === 'function' ? window.t('batchManageModal.unnamedConversation') : '未命名对话');
// 使用安全截断函数,限制最大长度为45个字符(留出空间显示省略号)
const truncatedTitle = safeTruncateText(originalTitle, 45);
name.textContent = truncatedTitle;
@@ -5051,7 +5110,8 @@ function renderBatchConversations(filtered = null) {
const time = document.createElement('div');
time.className = 'batch-table-col-time';
const dateObj = conv.updatedAt ? new Date(conv.updatedAt) : new Date();
time.textContent = dateObj.toLocaleString('zh-CN', {
const locale = (typeof i18next !== 'undefined' && i18next.language) ? i18next.language : 'zh-CN';
time.textContent = dateObj.toLocaleString(locale, {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -5105,11 +5165,12 @@ function toggleSelectAllBatch() {
async function deleteSelectedConversations() {
const checkboxes = document.querySelectorAll('.batch-conversation-checkbox:checked');
if (checkboxes.length === 0) {
alert('请先选择要删除的对话');
alert(typeof window.t === 'function' ? window.t('batchManageModal.confirmDeleteNone') : '请先选择要删除的对话');
return;
}
if (!confirm(`确定要删除选中的 ${checkboxes.length} 条对话吗?`)) {
const confirmMsg = typeof window.t === 'function' ? window.t('batchManageModal.confirmDeleteN', { count: checkboxes.length }) : '确定要删除选中的 ' + checkboxes.length + ' 条对话吗?';
if (!confirm(confirmMsg)) {
return;
}
@@ -5123,7 +5184,9 @@ async function deleteSelectedConversations() {
loadConversationsWithGroups();
} catch (error) {
console.error('删除失败:', error);
alert('删除失败: ' + (error.message || '未知错误'));
const failedMsg = typeof window.t === 'function' ? window.t('batchManageModal.deleteFailed') : '删除失败';
const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
alert(failedMsg + ': ' + (error.message || unknownErr));
}
}
@@ -5140,6 +5203,14 @@ function closeBatchManageModal() {
allConversationsForBatch = [];
}
// 语言切换时刷新批量管理模态框标题(若当前正在显示)
document.addEventListener('languagechange', function () {
const modal = document.getElementById('batch-manage-modal');
if (modal && modal.style.display === 'flex') {
updateBatchManageTitle(allConversationsForBatch.length);
}
});
// 显示创建分组模态框
function showCreateGroupModal(andMoveConversation = false) {
const modal = document.getElementById('create-group-modal');
@@ -5208,6 +5279,15 @@ function selectSuggestion(name) {
}
}
// 按 i18n key 选择建议标签(用于国际化下填充当前语言的文案)
function selectSuggestionByKey(i18nKey) {
const input = document.getElementById('create-group-name-input');
if (input && typeof window.t === 'function') {
input.value = window.t(i18nKey);
input.focus();
}
}
// 切换图标选择器显示状态
function toggleGroupIconPicker() {
const picker = document.getElementById('group-icon-picker');
@@ -5299,7 +5379,7 @@ async function createGroup(event) {
const name = input.value.trim();
if (!name) {
alert('请输入分组名称');
alert(typeof window.t === 'function' ? window.t('createGroupModal.groupNamePlaceholder') : '请输入分组名称');
return;
}
@@ -5320,7 +5400,7 @@ async function createGroup(event) {
const nameExists = groups.some(g => g.name === name);
if (nameExists) {
alert('分组名称已存在,请使用其他名称');
alert(typeof window.t === 'function' ? window.t('createGroupModal.nameExists') : '分组名称已存在,请使用其他名称');
return;
}
} catch (error) {
@@ -5345,11 +5425,13 @@ async function createGroup(event) {
if (!response.ok) {
const error = await response.json();
const nameExistsMsg = typeof window.t === 'function' ? window.t('createGroupModal.nameExists') : '分组名称已存在,请使用其他名称';
if (error.error && error.error.includes('已存在')) {
alert('分组名称已存在,请使用其他名称');
alert(nameExistsMsg);
return;
}
throw new Error(error.error || '创建失败');
const createFailedMsg = typeof window.t === 'function' ? window.t('createGroupModal.createFailed') : '创建失败';
throw new Error(error.error || createFailedMsg);
}
const newGroup = await response.json();
@@ -5375,7 +5457,9 @@ async function createGroup(event) {
}
} catch (error) {
console.error('创建分组失败:', error);
alert('创建失败: ' + (error.message || '未知错误'));
const createFailedMsg = typeof window.t === 'function' ? window.t('createGroupModal.createFailed') : '创建失败';
const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
alert(createFailedMsg + ': ' + (error.message || unknownErr));
}
}
@@ -5465,9 +5549,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参数
@@ -5479,7 +5563,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;
}
@@ -5493,7 +5577,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;
}
@@ -5517,10 +5601,12 @@ async function loadGroupConversations(groupId, searchQuery = '') {
list.innerHTML = '';
if (groupConvs.length === 0) {
const emptyMsg = typeof window.t === 'function' ? window.t('chat.emptyGroupConversations') : '该分组暂无对话';
const noMatchMsg = typeof window.t === 'function' ? window.t('chat.noMatchingConversationsInGroup') : '未找到匹配的对话';
if (searchQuery && searchQuery.trim()) {
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);">' + (noMatchMsg || '未找到匹配的对话') + '</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);">' + (emptyMsg || '该分组暂无对话') + '</div>';
}
return;
}
@@ -5583,7 +5669,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',
@@ -5651,7 +5738,8 @@ async function editGroup() {
const group = await response.json();
if (!group) return;
const newName = prompt('请输入新名称:', group.name);
const renamePrompt = typeof window.t === 'function' ? window.t('chat.renameGroupPrompt') : '请输入新名称';
const newName = prompt(renamePrompt, group.name);
if (newName === null || !newName.trim()) return;
const trimmedName = newName.trim();
@@ -5672,7 +5760,7 @@ async function editGroup() {
const nameExists = groups.some(g => g.name === trimmedName && g.id !== currentGroupId);
if (nameExists) {
alert('分组名称已存在,请使用其他名称');
alert(typeof window.t === 'function' ? window.t('createGroupModal.nameExists') : '分组名称已存在,请使用其他名称');
return;
}
@@ -5712,7 +5800,8 @@ async function editGroup() {
async function deleteGroup() {
if (!currentGroupId) return;
if (!confirm('确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。')) {
const deleteConfirmMsg = typeof window.t === 'function' ? window.t('chat.deleteGroupConfirm') : '确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。';
if (!confirm(deleteConfirmMsg)) {
return;
}
@@ -5758,7 +5847,8 @@ async function renameGroupFromContext() {
const group = await response.json();
if (!group) return;
const newName = prompt('请输入新名称:', group.name);
const renamePrompt = typeof window.t === 'function' ? window.t('chat.renameGroupPrompt') : '请输入新名称';
const newName = prompt(renamePrompt, group.name);
if (newName === null || !newName.trim()) {
closeGroupContextMenu();
return;
@@ -5782,7 +5872,7 @@ async function renameGroupFromContext() {
const nameExists = groups.some(g => g.name === trimmedName && g.id !== groupId);
if (nameExists) {
alert('分组名称已存在,请使用其他名称');
alert(typeof window.t === 'function' ? window.t('createGroupModal.nameExists') : '分组名称已存在,请使用其他名称');
return;
}
@@ -5817,7 +5907,9 @@ async function renameGroupFromContext() {
}
} catch (error) {
console.error('重命名分组失败:', error);
alert('重命名失败: ' + (error.message || '未知错误'));
const failedLabel = typeof window.t === 'function' ? window.t('chat.renameFailed') : '重命名失败';
const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
alert(failedLabel + ': ' + (error.message || unknownErr));
}
closeGroupContextMenu();
@@ -5867,7 +5959,8 @@ async function deleteGroupFromContext() {
const groupId = contextMenuGroupId;
if (!groupId) return;
if (!confirm('确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。')) {
const deleteConfirmMsg = typeof window.t === 'function' ? window.t('chat.deleteGroupConfirm') : '确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。';
if (!confirm(deleteConfirmMsg)) {
closeGroupContextMenu();
return;
}
+11 -11
View File
@@ -17,7 +17,7 @@ async function refreshDashboard() {
setEl('dashboard-kpi-tools-calls', '…');
setEl('dashboard-kpi-success-rate', '…');
var chartPlaceholder = document.getElementById('dashboard-tools-pie-placeholder');
if (chartPlaceholder) { chartPlaceholder.style.removeProperty('display'); chartPlaceholder.textContent = '加载中…'; }
if (chartPlaceholder) { chartPlaceholder.style.removeProperty('display'); chartPlaceholder.textContent = (typeof window.t === 'function' ? window.t('common.loading') : '加载中…'); }
var barChartEl = document.getElementById('dashboard-tools-bar-chart');
if (barChartEl) { barChartEl.style.display = 'none'; barChartEl.innerHTML = ''; }
@@ -77,7 +77,7 @@ async function refreshDashboard() {
setEl('dashboard-batch-pending', String(pending));
setEl('dashboard-batch-running', String(running));
setEl('dashboard-batch-done', String(done));
setEl('dashboard-batch-total', total > 0 ? `${total}` : '暂无任务');
setEl('dashboard-batch-total', total > 0 ? (typeof window.t === 'function' ? window.t('dashboard.totalCount', { count: total }) : `${total}`) : (typeof window.t === 'function' ? window.t('dashboard.noTasks') : '暂无任务'));
// 更新进度条
if (total > 0) {
@@ -138,7 +138,7 @@ async function refreshDashboard() {
if (knowledgeRes && typeof knowledgeRes === 'object') {
if (knowledgeRes.enabled === false) {
// 功能未启用:用状态标签展示,数值保持为 "-"
if (knowledgeStatusEl) knowledgeStatusEl.textContent = '未启用';
if (knowledgeStatusEl) knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.notEnabled') : '未启用');
if (knowledgeItemsEl) knowledgeItemsEl.textContent = '-';
if (knowledgeCategoriesEl) knowledgeCategoriesEl.textContent = '-';
} else {
@@ -149,9 +149,9 @@ async function refreshDashboard() {
// 根据数据量给个轻量状态文案
if (knowledgeStatusEl) {
if (items > 0 || categories > 0) {
knowledgeStatusEl.textContent = '已启用';
knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.enabled') : '已启用');
} else {
knowledgeStatusEl.textContent = '待配置';
knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.toConfigure') : '待配置');
}
}
}
@@ -172,15 +172,15 @@ async function refreshDashboard() {
const statusEl = document.getElementById('dashboard-skills-status');
if (statusEl) {
if (totalCalls === 0) {
statusEl.textContent = '待使用';
statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.toUse') : '待使用');
statusEl.style.background = 'rgba(0, 0, 0, 0.05)';
statusEl.style.color = 'var(--text-secondary)';
} else if (totalCalls < 10) {
statusEl.textContent = '活跃';
statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.active') : '活跃');
statusEl.style.background = 'rgba(16, 185, 129, 0.1)';
statusEl.style.color = '#10b981';
} else {
statusEl.textContent = '高频';
statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.highFreq') : '高频');
statusEl.style.background = 'rgba(59, 130, 246, 0.1)';
statusEl.style.color = '#3b82f6';
}
@@ -200,7 +200,7 @@ async function refreshDashboard() {
setEl('dashboard-kpi-tools-calls', '-');
renderDashboardToolsBar(null);
var ph = document.getElementById('dashboard-tools-pie-placeholder');
if (ph) { ph.style.removeProperty('display'); ph.textContent = '暂无调用数据'; }
if (ph) { ph.style.removeProperty('display'); ph.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据'); }
}
}
@@ -257,7 +257,7 @@ function renderDashboardToolsBar(monitorRes) {
if (!monitorRes || typeof monitorRes !== 'object') {
placeholder.style.removeProperty('display');
placeholder.textContent = '暂无调用数据';
placeholder.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据');
barChartEl.style.display = 'none';
barChartEl.innerHTML = '';
return;
@@ -273,7 +273,7 @@ function renderDashboardToolsBar(monitorRes) {
if (entries.length === 0) {
placeholder.style.removeProperty('display');
placeholder.textContent = '暂无调用数据';
placeholder.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据');
barChartEl.style.display = 'none';
barChartEl.innerHTML = '';
return;
+213
View File
@@ -0,0 +1,213 @@
// 前端国际化初始化(基于 i18next 浏览器版本)
(function () {
const DEFAULT_LANG = 'zh-CN';
const STORAGE_KEY = 'csai_lang';
const RESOURCES_PREFIX = '/static/i18n';
const loadedLangs = {};
function detectInitialLang() {
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
return stored;
}
} catch (e) {
console.warn('无法读取语言设置:', e);
}
const navLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
if (navLang.startsWith('zh')) {
return 'zh-CN';
}
if (navLang.startsWith('en')) {
return 'en-US';
}
return DEFAULT_LANG;
}
async function loadLanguageResources(lang) {
if (loadedLangs[lang]) {
return;
}
try {
const resp = await fetch(RESOURCES_PREFIX + '/' + lang + '.json', {
cache: 'no-cache'
});
if (!resp.ok) {
console.warn('加载语言包失败:', lang, resp.status);
return;
}
const data = await resp.json();
if (typeof i18next !== 'undefined') {
i18next.addResourceBundle(lang, 'translation', data, true, true);
}
loadedLangs[lang] = true;
} catch (e) {
console.error('加载语言包异常:', lang, e);
}
}
function applyTranslations(root) {
if (typeof i18next === 'undefined') return;
const container = root || document;
if (!container) return;
const elements = container.querySelectorAll('[data-i18n]');
elements.forEach(function (el) {
const key = el.getAttribute('data-i18n');
if (!key) return;
const skipText = el.getAttribute('data-i18n-skip-text') === 'true';
const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
const attrList = el.getAttribute('data-i18n-attr');
const text = i18next.t(key);
// 仅当元素无子元素(仅文本或空)时才替换文本,避免覆盖卡片内的数字、子节点等;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;
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);
}
});
}
});
// 对话输入框:若 value 与 placeholder 相同,清空 value 以便正确显示占位提示
try {
const chatInput = document.getElementById('chat-input');
if (chatInput && chatInput.tagName === 'TEXTAREA') {
const ph = (chatInput.getAttribute('placeholder') || '').trim();
if (ph && chatInput.value.trim() === ph) {
chatInput.value = '';
}
}
} catch (e) { /* ignore */ }
// 更新 html lang 属性
try {
if (document && document.documentElement) {
document.documentElement.lang = i18next.language || DEFAULT_LANG;
}
} catch (e) {
// ignore
}
}
function updateLangLabel() {
const label = document.getElementById('current-lang-label');
if (!label || typeof i18next === 'undefined') return;
const lang = (i18next.language || DEFAULT_LANG).toLowerCase();
if (lang.indexOf('zh') === 0) {
label.textContent = i18next.t('lang.zhCN');
} else {
label.textContent = i18next.t('lang.enUS');
}
}
function closeLangDropdown() {
const dropdown = document.getElementById('lang-dropdown');
if (dropdown) {
dropdown.style.display = 'none';
}
}
function handleGlobalClickForLangDropdown(ev) {
const dropdown = document.getElementById('lang-dropdown');
const btn = document.querySelector('.lang-switcher-btn');
if (!dropdown || dropdown.style.display !== 'block') return;
const target = ev.target;
if (btn && btn.contains(target)) {
return;
}
if (!dropdown.contains(target)) {
closeLangDropdown();
}
}
async function changeLanguage(lang) {
if (typeof i18next === 'undefined') return;
const current = i18next.language || DEFAULT_LANG;
if (lang === current) return;
await loadLanguageResources(lang);
await i18next.changeLanguage(lang);
try {
localStorage.setItem(STORAGE_KEY, lang);
} catch (e) {
console.warn('无法保存语言设置:', e);
}
applyTranslations(document);
updateLangLabel();
try {
window.__locale = lang;
} catch (e) { /* ignore */ }
try {
document.dispatchEvent(new CustomEvent('languagechange', { detail: { lang: lang } }));
} catch (e) { /* ignore */ }
}
async function initI18n() {
if (typeof i18next === 'undefined') {
console.warn('i18next 未加载,跳过前端国际化初始化');
return;
}
const initialLang = detectInitialLang();
await i18next.init({
lng: initialLang,
fallbackLng: DEFAULT_LANG,
debug: false,
resources: {}
});
await loadLanguageResources(initialLang);
applyTranslations(document);
updateLangLabel();
try {
window.__locale = i18next.language || initialLang;
} catch (e) { /* ignore */ }
// 导出全局函数供其他脚本调用(支持插值参数,如 _t('key', { count: 2 })
window.t = function (key, opts) {
if (typeof i18next === 'undefined') return key;
return i18next.t(key, opts);
};
window.changeLanguage = changeLanguage;
window.applyTranslations = applyTranslations;
// 语言切换下拉支持
window.toggleLangDropdown = function () {
const dropdown = document.getElementById('lang-dropdown');
if (!dropdown) return;
if (dropdown.style.display === 'block') {
dropdown.style.display = 'none';
} else {
dropdown.style.display = 'block';
}
};
window.onLanguageSelect = function (lang) {
changeLanguage(lang);
closeLangDropdown();
};
document.addEventListener('click', handleGlobalClickForLangDropdown);
}
document.addEventListener('DOMContentLoaded', function () {
// i18n 初始化在 DOM Ready 后执行
initI18n().catch(function (e) {
console.error('初始化国际化失败:', e);
});
});
})();
+74 -61
View File
@@ -1,4 +1,7 @@
// 信息收集页面(FOFA
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
const FOFA_FORM_STORAGE_KEY = 'info-collect-fofa-form';
const FOFA_HIDDEN_FIELDS_STORAGE_KEY = 'info-collect-fofa-hidden-fields';
@@ -197,12 +200,12 @@ async function submitFofaSearch() {
const full = !!els.full?.checked;
if (!query) {
alert('请输入 FOFA 查询语法');
alert(_t('infoCollect.enterFofaQuery'));
return;
}
saveFofaFormToStorage({ query, size, page, fields, full });
setFofaMeta('查询中...');
setFofaMeta(_t('infoCollect.querying'));
setFofaLoading(true);
try {
@@ -219,9 +222,9 @@ async function submitFofaSearch() {
renderFofaResults(result);
} catch (e) {
console.error('FOFA 查询失败:', e);
setFofaMeta('查询失败');
setFofaMeta(_t('infoCollect.queryFailed'));
renderFofaResults({ query, fields: [], results: [], total: 0, page: 1, size: 0 });
alert('FOFA 查询失败: ' + (e && e.message ? e.message : String(e)));
alert(_t('infoCollect.queryFailed') + ': ' + (e && e.message ? e.message : String(e)));
} finally {
setFofaLoading(false);
}
@@ -231,7 +234,7 @@ async function parseFofaNaturalLanguage() {
const els = getFofaFormElements();
const text = (els.nl?.value || '').trim();
if (!text) {
alert('请输入自然语言描述');
alert(_t('infoCollect.enterNaturalLanguage'));
return;
}
@@ -243,16 +246,16 @@ async function parseFofaNaturalLanguage() {
// 先创建 controller,避免极快的重复点击触发并发请求
fofaParseAbortController = new AbortController();
setFofaParseLoading(true, 'AI 解析中...');
setFofaParseLoading(true, _t('infoCollect.parsePending'));
// 持续提示:直到请求完成/取消/失败才消失
fofaParseToastHandle = showInlineToast('AI 解析中...(点击按钮可取消)', { duration: 0, id: 'fofa-parse-pending' });
fofaParseToastHandle = showInlineToast(_t('infoCollect.parsePendingClickCancel'), { duration: 0, id: 'fofa-parse-pending' });
// 如果超过一小段时间还没返回,再强调“仍在进行中”,降低误判为失败的概率
fofaParseSlowTimer = setTimeout(() => {
const status = document.getElementById('fofa-nl-status');
if (status) {
status.textContent = 'AI 解析耗时较长,仍在处理中…';
status.textContent = _t('infoCollect.parseSlow');
status.style.display = 'block';
}
}, 1800);
@@ -269,15 +272,15 @@ async function parseFofaNaturalLanguage() {
throw new Error(result.error || `请求失败: ${resp.status}`);
}
showFofaParseModal(text, result);
showInlineToast('AI 解析完成');
showInlineToast(_t('infoCollect.parseDone'));
} catch (e) {
// AbortController 取消:不视为失败
if (e && (e.name === 'AbortError' || String(e).includes('AbortError'))) {
showInlineToast('已取消 AI 解析');
showInlineToast(_t('infoCollect.parseCancelled'));
return;
}
console.error('FOFA 自然语言解析失败:', e);
showInlineToast('AI 解析失败:' + (e && e.message ? e.message : String(e)), { duration: 2800 });
showInlineToast(_t('infoCollect.parseFailed') + (e && e.message ? e.message : String(e)), { duration: 2800 });
}
finally {
fofaParseAbortController = null;
@@ -298,17 +301,17 @@ function setFofaParseLoading(loading, statusText) {
const status = document.getElementById('fofa-nl-status');
if (btn) {
if (loading) {
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent || 'AI 解析';
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent || _t('infoCollectPage.parseBtn');
btn.classList.add('btn-loading');
btn.textContent = '取消解析';
btn.title = '点击取消 AI 解析';
btn.textContent = _t('infoCollect.cancelParse');
btn.title = _t('infoCollect.clickToCancelParse');
btn.dataset.loading = '1';
btn.setAttribute('aria-busy', 'true');
btn.disabled = false;
} else {
btn.classList.remove('btn-loading');
btn.textContent = btn.dataset.originalText || 'AI 解析';
btn.title = '将自然语言解析为 FOFA 查询语法';
btn.textContent = btn.dataset.originalText || _t('infoCollectPage.parseBtn');
btn.title = _t('infoCollect.parseToFofa');
btn.disabled = false;
delete btn.dataset.loading;
btn.removeAttribute('aria-busy');
@@ -336,7 +339,7 @@ function showFofaParseModal(nlText, parsed) {
const warningsHtml = warnings.length
? `<ul style="margin: 8px 0 0 18px;">${warnings.map(w => `<li>${escapeHtml(w)}</li>`).join('')}</ul>`
: `<div class="muted" style="margin-top: 8px;"></div>`;
: '<div class="muted" style="margin-top: 8px;">' + _t('infoCollect.none') + '</div>';
const modal = document.createElement('div');
modal.id = 'fofa-parse-modal';
@@ -345,23 +348,23 @@ function showFofaParseModal(nlText, parsed) {
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px;">
<div class="modal-header">
<h2>AI 解析结果</h2>
<span class="modal-close" id="fofa-parse-modal-close" title="关闭">&times;</span>
<h2>${_t('infoCollect.parseResultTitle')}</h2>
<span class="modal-close" id="fofa-parse-modal-close" title="${_t('common.close')}">&times;</span>
</div>
<div style="padding: 18px 28px; overflow: auto;">
<div class="form-group">
<label>自然语言</label>
<label>${_t('infoCollect.naturalLanguageLabel')}</label>
<div class="muted" style="margin-top: 6px; white-space: pre-wrap;">${safeNL || '-'}</div>
</div>
<div class="form-group" style="margin-top: 14px;">
<label for="fofa-parse-query">FOFA 查询语法可编辑</label>
<textarea id="fofa-parse-query" class="info-collect-query-input" rows="2" placeholder='例如:app="Apache" && country="CN"'></textarea>
<small class="form-hint">请人工确认语法与范围无误后再执行查询</small>
<label for="fofa-parse-query">${_t('infoCollect.fofaQueryEditable')}</label>
<textarea id="fofa-parse-query" class="info-collect-query-input" rows="2" placeholder="${_t('infoCollect.queryPlaceholder')}"></textarea>
<small class="form-hint">${_t('infoCollect.confirmBeforeQuery')}</small>
</div>
<div class="form-group" style="margin-top: 14px;">
<label>提醒</label>
<label>${_t('infoCollect.reminder')}</label>
<div style="background: #fff8e1; border: 1px solid #ffe8a3; border-radius: 10px; padding: 10px 12px;">
${warningsHtml}
</div>
@@ -369,14 +372,14 @@ function showFofaParseModal(nlText, parsed) {
${explanation ? `
<div class="form-group" style="margin-top: 14px;">
<label>解析说明</label>
<label>${_t('infoCollect.explanation')}</label>
<pre style="margin-top: 8px; white-space: pre-wrap; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 10px; padding: 10px 12px; font-size: 13px;">${escapeHtml(explanation)}</pre>
</div>` : ''}
</div>
<div class="modal-footer" style="padding: 18px 28px;">
<button class="btn-secondary" type="button" id="fofa-parse-cancel">取消</button>
<button class="btn-secondary" type="button" id="fofa-parse-apply">填入查询框</button>
<button class="btn-primary" type="button" id="fofa-parse-apply-run">填入并查询</button>
<button class="btn-secondary" type="button" id="fofa-parse-cancel">${_t('infoCollect.parseModalCancel')}</button>
<button class="btn-secondary" type="button" id="fofa-parse-apply">${_t('infoCollect.parseModalApply')}</button>
<button class="btn-primary" type="button" id="fofa-parse-apply-run">${_t('infoCollect.parseModalApplyRun')}</button>
</div>
</div>
`;
@@ -402,7 +405,7 @@ function showFofaParseModal(nlText, parsed) {
const els = getFofaFormElements();
const q = (queryTextarea?.value || '').trim();
if (!q) {
showInlineToast('解析结果为空:请在弹窗中补充/修改 FOFA 查询语法', { duration: 2600 });
showInlineToast(_t('infoCollect.parseResultEmpty'), { duration: 2600 });
return;
}
if (els.query) {
@@ -444,7 +447,7 @@ function setFofaMeta(text) {
function updateSelectedMeta() {
const els = getFofaFormElements();
if (els.selectedMeta) {
els.selectedMeta.textContent = `已选择 ${infoCollectState.selectedRowIndexes.size}`;
els.selectedMeta.textContent = _t('infoCollectPage.selectedRows', { count: infoCollectState.selectedRowIndexes.size });
}
}
@@ -454,7 +457,7 @@ function setFofaLoading(loading) {
if (loading) {
const fieldsCount = (document.getElementById('fofa-fields')?.value || '').split(',').filter(Boolean).length;
const colspan = Math.max(1, fieldsCount + 1);
els.tbody.innerHTML = `<tr><td class="muted" style="padding: 16px;" colspan="${colspan}">加载中...</td></tr>`;
els.tbody.innerHTML = '<tr><td class="muted" style="padding: 16px;" colspan="' + colspan + '">' + escapeHtml(_t('infoCollect.loading')) + '</td></tr>';
}
}
@@ -490,7 +493,7 @@ function renderFofaResults(payload) {
const size = typeof payload.size === 'number' ? payload.size : 0;
const page = typeof payload.page === 'number' ? payload.page : 1;
setFofaMeta(`${total} 条 · 本页 ${results.length} 条 · page=${page} · size=${size}`);
setFofaMeta(_t('infoCollect.resultsMeta', { total, count: results.length, page, size }));
// 可见字段
const visibleFields = fields.filter(f => !infoCollectState.hiddenFields.has(f));
@@ -500,16 +503,16 @@ function renderFofaResults(payload) {
// 表头(左:勾选列;右:操作列固定)
const headerCells = [
'<th class="info-collect-col-select"><input type="checkbox" id="fofa-select-all" title="全选/全不选"/></th>',
'<th class="info-collect-col-select"><input type="checkbox" id="fofa-select-all" title="' + escapeHtml(_t('infoCollect.selectAll')) + '"/></th>',
...visibleFields.map(f => `<th>${escapeHtml(String(f))}</th>`),
'<th class="info-collect-col-actions">操作</th>'
'<th class="info-collect-col-actions">' + escapeHtml(_t('infoCollect.actions')) + '</th>'
].join('');
els.thead.innerHTML = `<tr>${headerCells}</tr>`;
// 表体
if (results.length === 0) {
const colspan = Math.max(1, visibleFields.length + 2);
els.tbody.innerHTML = `<tr><td class="muted" style="padding: 16px;" colspan="${colspan}">暂无数据</td></tr>`;
els.tbody.innerHTML = '<tr><td class="muted" style="padding: 16px;" colspan="' + colspan + '">' + escapeHtml(_t('common.noData')) + '</td></tr>';
return;
}
@@ -519,7 +522,7 @@ function renderFofaResults(payload) {
const encoded = encodeURIComponent(JSON.stringify(safeRow));
const encodedTarget = encodeURIComponent(target || '');
const selectHtml = `<td class="info-collect-col-select"><input class="fofa-row-select" type="checkbox" data-index="${idx}" title="选择该行"/></td>`;
const selectHtml = '<td class="info-collect-col-select"><input class="fofa-row-select" type="checkbox" data-index="' + idx + '" title="' + escapeHtml(_t('infoCollect.selectRow')) + '"/></td>';
const cellsHtml = visibleFields.map(f => {
const val = safeRow[f];
@@ -537,13 +540,13 @@ function renderFofaResults(payload) {
const actionHtml = `
<div class="info-collect-actions">
<button class="btn-icon" onclick="copyFofaTargetEncoded('${encodedTarget}'); event.stopPropagation();" title="复制目标">
<button class="btn-icon" onclick="copyFofaTargetEncoded('${encodedTarget}'); event.stopPropagation();" title="${escapeHtml(_t('infoCollect.copyTarget'))}">
<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" 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" stroke-linecap="round"/>
</svg>
</button>
<button class="btn-icon" onclick="scanFofaRow('${encoded}', event); event.stopPropagation();" title="发送到对话(可编辑;Ctrl/⌘+点击可直接发送)">
<button class="btn-icon" onclick="scanFofaRow('${encoded}', event); event.stopPropagation();" title="${escapeHtml(_t('infoCollect.sendToChat'))}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.5 13.5l3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
<path d="M8 8H5a4 4 0 1 0 0 8h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -602,14 +605,14 @@ function normalizeHttpLink(raw) {
function copyFofaTarget(target) {
const text = (target || '').trim();
if (!text) {
alert('没有可复制的目标');
alert(_t('infoCollect.noTargetToCopy'));
return;
}
navigator.clipboard.writeText(text).then(() => {
// 简单提示
showInlineToast('已复制目标');
showInlineToast(_t('infoCollect.targetCopied'));
}).catch(() => {
alert('复制失败,请手动复制:' + text);
alert(_t('infoCollect.manualCopyHint') + text);
});
}
@@ -655,7 +658,7 @@ function showInlineToast(text, options) {
function truncateForPreview(value, maxLen) {
const s = value == null ? '' : String(value);
if (maxLen <= 0 || s.length <= maxLen) return s;
return s.slice(0, maxLen) + '...(已截断)';
return s.slice(0, maxLen) + '...(' + _t('infoCollect.truncated') + ')';
}
function formatFofaRowSummary(row, fields) {
@@ -707,7 +710,7 @@ function scanFofaRow(encodedRowJson, clickEvent) {
const fields = (document.getElementById('fofa-fields')?.value || '').split(',').map(s => s.trim()).filter(Boolean);
const target = inferTargetFromRow(row, fields);
if (!target) {
alert('无法从该行推断扫描目标(建议在 fields 中包含 host/ip/port/domain');
alert(_t('infoCollect.cannotInferTarget'));
return;
}
@@ -745,10 +748,10 @@ function scanFofaRow(encodedRowJson, clickEvent) {
if (typeof sendMessage === 'function') {
sendMessage();
} else {
alert('未找到 sendMessage(),请刷新页面后重试');
alert(_t('infoCollect.noSendMessage'));
}
} else {
showInlineToast('已填入对话输入框,可编辑后发送');
showInlineToast(_t('infoCollect.filledToInput'));
}
}, 250);
}
@@ -910,7 +913,7 @@ function hideAllFofaColumns() {
function exportFofaResults(format) {
const p = infoCollectState.currentPayload;
if (!p || !Array.isArray(p.results) || p.results.length === 0) {
alert('暂无可导出的结果');
alert(_t('infoCollect.noExportResult'));
return;
}
@@ -936,7 +939,7 @@ function exportFofaResults(format) {
if (format === 'xlsx') {
// 使用 SheetJS 生成 XLSX(需在页面中引入 xlsx 库)
if (typeof XLSX === 'undefined') {
alert('未加载 XLSX 库,请刷新页面后重试');
alert(_t('infoCollect.xlsxNotLoaded'));
return;
}
const aoa = [visibleFields].concat(p.results.map(row => {
@@ -945,7 +948,7 @@ function exportFofaResults(format) {
}));
const ws = XLSX.utils.aoa_to_sheet(aoa);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, 'FOFA结果');
XLSX.utils.book_append_sheet(wb, ws, _t('infoCollect.batchScanTitle'));
XLSX.writeFile(wb, `fofa_results_${ts}.xlsx`);
return;
}
@@ -982,12 +985,12 @@ function downloadBlob(content, filename, mime) {
async function batchScanSelectedFofaRows() {
const p = infoCollectState.currentPayload;
if (!p || !Array.isArray(p.results) || p.results.length === 0) {
alert('暂无结果');
alert(_t('infoCollect.noResults'));
return;
}
const selected = Array.from(infoCollectState.selectedRowIndexes).sort((a, b) => a - b);
if (selected.length === 0) {
alert('请先勾选需要扫描的行');
alert(_t('infoCollect.selectRowsFirst'));
return;
}
@@ -1009,11 +1012,11 @@ async function batchScanSelectedFofaRows() {
});
if (tasks.length === 0) {
alert('未能从所选行推断任何可扫描目标(建议 fields 中包含 host/ip/port/domain');
alert(_t('infoCollect.noScanTarget'));
return;
}
const title = (p.query ? `FOFA 批量扫描:${p.query}` : 'FOFA 批量扫描').slice(0, 80);
const title = (p.query ? _t('infoCollect.batchScanTitle') + '' + p.query : _t('infoCollect.batchScanTitle')).slice(0, 80);
try {
// 不强制切换到“信息收集”角色:沿用当前已选角色;若为默认则传空字符串交给后端走默认逻辑
let role = '';
@@ -1029,7 +1032,7 @@ async function batchScanSelectedFofaRows() {
});
const result = await resp.json().catch(() => ({}));
if (!resp.ok) {
throw new Error(result.error || `创建批量队列失败: ${resp.status}`);
throw new Error(result.error || _t('infoCollect.createQueueFailed') + ': ' + resp.status);
}
const queueId = result.queueId;
if (!queueId) {
@@ -1045,13 +1048,13 @@ async function batchScanSelectedFofaRows() {
}, 250);
if (skipped.length > 0) {
showInlineToast(`已创建队列(跳过 ${skipped.length} 条无目标行)`);
showInlineToast(_t('infoCollect.queueCreatedSkipped', { n: skipped.length }));
} else {
showInlineToast('已创建批量扫描队列');
showInlineToast(_t('infoCollect.batchQueueCreated'));
}
} catch (e) {
console.error('批量扫描失败:', e);
alert('批量扫描失败: ' + (e && e.message ? e.message : String(e)));
alert(_t('infoCollect.batchScanFailed') + ': ' + (e && e.message ? e.message : String(e)));
}
}
@@ -1065,8 +1068,8 @@ function showCellDetailModal(field, fullText) {
modal.innerHTML = `
<div class="info-collect-cell-modal-content" role="dialog" aria-modal="true">
<div class="info-collect-cell-modal-header">
<div class="info-collect-cell-modal-title">${escapeHtml(field || '字段')}</div>
<button class="btn-icon" type="button" id="info-collect-cell-modal-close" title="关闭">
<div class="info-collect-cell-modal-title">${escapeHtml(field || _t('infoCollect.field'))}</div>
<button class="btn-icon" type="button" id="info-collect-cell-modal-close" title="${_t('common.close')}">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
@@ -1076,8 +1079,8 @@ function showCellDetailModal(field, fullText) {
<pre class="info-collect-cell-modal-pre">${escapeHtml(fullText || '')}</pre>
</div>
<div class="info-collect-cell-modal-footer">
<button class="btn-secondary" type="button" id="info-collect-cell-modal-copy">复制</button>
<button class="btn-primary" type="button" id="info-collect-cell-modal-ok">关闭</button>
<button class="btn-secondary" type="button" id="info-collect-cell-modal-copy">${_t('common.copy')}</button>
<button class="btn-primary" type="button" id="info-collect-cell-modal-ok">${_t('common.close')}</button>
</div>
</div>
`;
@@ -1091,7 +1094,7 @@ function showCellDetailModal(field, fullText) {
document.getElementById('info-collect-cell-modal-close')?.addEventListener('click', close);
document.getElementById('info-collect-cell-modal-ok')?.addEventListener('click', close);
document.getElementById('info-collect-cell-modal-copy')?.addEventListener('click', () => {
navigator.clipboard.writeText(fullText || '').then(() => showInlineToast('已复制')).catch(() => alert('复制失败'));
navigator.clipboard.writeText(fullText || '').then(() => showInlineToast(_t('common.copied'))).catch(() => alert(_t('common.copyFailed')));
});
// Esc 关闭
@@ -1122,3 +1125,13 @@ window.toggleFofaColumn = toggleFofaColumn;
window.exportFofaResults = exportFofaResults;
window.batchScanSelectedFofaRows = batchScanSelectedFofaRows;
document.addEventListener('languagechange', function () {
updateSelectedMeta();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { updateSelectedMeta(); });
} else {
updateSelectedMeta();
}
+108 -102
View File
@@ -1,4 +1,37 @@
// 知识库管理相关功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
// 返回「知识库未启用」提示区块的 HTML(使用 data-i18n 以便语言切换时自动更新)
function getKnowledgeNotEnabledHTML() {
return `
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
<h3 data-i18n="knowledge.notEnabledTitle" style="margin-bottom: 10px; color: #666;"></h3>
<p data-i18n="knowledge.notEnabledHint" style="color: #999; margin-bottom: 20px;"></p>
<button data-i18n="knowledge.goToSettings" onclick="switchToSettings()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
"></button>
</div>
`;
}
// 渲染「知识库未启用」状态到容器,并应用当前语言
function renderKnowledgeNotEnabledState(container) {
if (!container) return;
container.innerHTML = getKnowledgeNotEnabledHTML();
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(container);
}
}
let knowledgeCategories = [];
let knowledgeItems = [];
let currentEditingItemId = null;
@@ -32,26 +65,8 @@ async function loadKnowledgeCategories() {
// 检查知识库功能是否启用
if (data.enabled === false) {
// 功能未启用,显示友好提示
const container = document.getElementById('knowledge-items-list');
if (container) {
container.innerHTML = `
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
<button onclick="switchToSettings()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
">前往设置</button>
</div>
`;
}
// 功能未启用,显示友好提示(使用 data-i18n,切换语言时会自动更新)
renderKnowledgeNotEnabledState(document.getElementById('knowledge-items-list'));
return [];
}
@@ -116,25 +131,10 @@ async function loadKnowledgeItems(category = '', page = 1, pageSize = 10) {
// 检查知识库功能是否启用
if (data.enabled === false) {
// 功能未启用,显示友好提示(如果还没有显示的话)
// 功能未启用,显示友好提示(如果还没有显示的话;使用 data-i18n,切换语言时会自动更新
const container = document.getElementById('knowledge-items-list');
if (container && !container.querySelector('.empty-state')) {
container.innerHTML = `
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
<button onclick="switchToSettings()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
">前往设置</button>
</div>
`;
renderKnowledgeNotEnabledState(container);
}
knowledgeItems = [];
knowledgePagination.total = 0;
@@ -753,25 +753,7 @@ async function searchKnowledgeItems() {
// 检查知识库功能是否启用
if (data.enabled === false) {
const container = document.getElementById('knowledge-items-list');
if (container) {
container.innerHTML = `
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
<button onclick="switchToSettings()" style="
background: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
">前往设置</button>
</div>
`;
}
renderKnowledgeNotEnabledState(document.getElementById('knowledge-items-list'));
return;
}
@@ -1312,7 +1294,7 @@ async function loadRetrievalLogs(conversationId = '', messageId = '') {
renderRetrievalLogs([]);
// 只在非空筛选条件下才显示错误通知(避免在没有数据时显示错误)
if (conversationId || messageId) {
showNotification('加载检索日志失败: ' + error.message, 'error');
showNotification(_t('retrievalLogs.loadError') + ': ' + error.message, 'error');
}
}
}
@@ -1326,7 +1308,7 @@ function renderRetrievalLogs(logs) {
updateRetrievalStats(logs);
if (logs.length === 0) {
container.innerHTML = '<div class="empty-state">暂无检索记录</div>';
container.innerHTML = '<div class="empty-state">' + _t('retrievalLogs.noRecords') + '</div>';
retrievalLogsData = [];
return;
}
@@ -1386,7 +1368,7 @@ function renderRetrievalLogs(logs) {
</div>
<div class="retrieval-log-main-info">
<div class="retrieval-log-query">
${escapeHtml(log.query || '无查询内容')}
${escapeHtml(log.query || _t('retrievalLogs.noQuery'))}
</div>
<div class="retrieval-log-meta">
<span class="retrieval-log-time" title="${formatTime(log.createdAt)}">
@@ -1396,33 +1378,33 @@ function renderRetrievalLogs(logs) {
</div>
</div>
<div class="retrieval-log-result-badge ${hasResults ? 'success' : 'empty'}">
${hasResults ? (itemCount > 0 ? `${itemCount}` : '有结果') : '无结果'}
${hasResults ? (itemCount > 0 ? itemCount + ' ' + _t('retrievalLogs.itemsUnit') : _t('retrievalLogs.hasResults')) : _t('retrievalLogs.noResults')}
</div>
</div>
<div class="retrieval-log-card-body">
<div class="retrieval-log-details-grid">
${log.conversationId ? `
<div class="retrieval-log-detail-item">
<span class="detail-label">对话ID</span>
<code class="detail-value" title="点击复制" onclick="navigator.clipboard.writeText('${escapeHtml(log.conversationId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);" style="cursor: pointer;">${escapeHtml(log.conversationId)}</code>
<span class="detail-label">${_t('retrievalLogs.conversationId')}</span>
<code class="detail-value" title="${_t('retrievalLogs.clickToCopy')}" data-copy-title-copied="${_t('common.copied')}" data-copy-title-click="${_t('retrievalLogs.clickToCopy')}" onclick="var t=this; navigator.clipboard.writeText('${escapeHtml(log.conversationId)}').then(function(){ t.title=t.getAttribute('data-copy-title-copied')||'Copied!'; setTimeout(function(){ t.title=t.getAttribute('data-copy-title-click')||'Click to copy'; }, 2000); });" style="cursor: pointer;">${escapeHtml(log.conversationId)}</code>
</div>
` : ''}
${log.messageId ? `
<div class="retrieval-log-detail-item">
<span class="detail-label">消息ID</span>
<code class="detail-value" title="点击复制" onclick="navigator.clipboard.writeText('${escapeHtml(log.messageId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);" style="cursor: pointer;">${escapeHtml(log.messageId)}</code>
<span class="detail-label">${_t('retrievalLogs.messageId')}</span>
<code class="detail-value" title="${_t('retrievalLogs.clickToCopy')}" data-copy-title-copied="${_t('common.copied')}" data-copy-title-click="${_t('retrievalLogs.clickToCopy')}" onclick="var el=this; navigator.clipboard.writeText('${escapeHtml(log.messageId)}').then(function(){ el.title=el.getAttribute('data-copy-title-copied')||el.title; setTimeout(function(){ el.title=el.getAttribute('data-copy-title-click')||el.title; }, 2000); });" style="cursor: pointer;">${escapeHtml(log.messageId)}</code>
</div>
` : ''}
<div class="retrieval-log-detail-item">
<span class="detail-label">检索结果</span>
<span class="detail-label">${_t('retrievalLogs.retrievalResult')}</span>
<span class="detail-value ${hasResults ? 'text-success' : 'text-muted'}">
${hasResults ? (itemCount > 0 ? `找到 ${itemCount} 个相关知识项` : '找到相关知识项(数量未知)') : '未找到匹配的知识项'}
${hasResults ? (itemCount > 0 ? _t('retrievalLogs.foundCount', { count: itemCount }) : _t('retrievalLogs.foundUnknown')) : _t('retrievalLogs.noMatch')}
</span>
</div>
</div>
${hasResults && log.retrievedItems && log.retrievedItems.length > 0 ? `
<div class="retrieval-log-items-preview">
<div class="retrieval-log-items-label">检索到的知识项:</div>
<div class="retrieval-log-items-label">${_t('retrievalLogs.retrievedItemsLabel')}</div>
<div class="retrieval-log-items-list">
${log.retrievedItems.slice(0, 3).map((itemId, idx) => `
<span class="retrieval-log-item-tag">${idx + 1}</span>
@@ -1437,13 +1419,13 @@ function renderRetrievalLogs(logs) {
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
查看详情
${_t('retrievalLogs.viewDetails')}
</button>
<button class="btn-secondary btn-sm retrieval-log-delete-btn" onclick="deleteRetrievalLog('${escapeHtml(log.id)}', ${index})" style="margin-top: 12px; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: var(--error-color, #dc3545); border-color: var(--error-color, #dc3545);" onmouseover="this.style.backgroundColor='rgba(220, 53, 69, 0.1)'; this.style.color='#dc3545';" onmouseout="this.style.backgroundColor=''; this.style.color='var(--error-color, #dc3545)';" title="删除">
<button class="btn-secondary btn-sm retrieval-log-delete-btn" onclick="deleteRetrievalLog('${escapeHtml(log.id)}', ${index})" style="margin-top: 12px; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: var(--error-color, #dc3545); border-color: var(--error-color, #dc3545);" onmouseover="this.style.backgroundColor='rgba(220, 53, 69, 0.1)'; this.style.color='#dc3545';" onmouseout="this.style.backgroundColor=''; this.style.color='var(--error-color, #dc3545)';" title="${_t('common.delete')}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 6h18M19 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" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
删除
${_t('common.delete')}
</button>
</div>
</div>
@@ -1480,22 +1462,25 @@ function updateRetrievalStats(logs) {
statsContainer.innerHTML = `
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">总检索次数</span>
<span class="retrieval-stat-label" data-i18n="retrievalLogs.totalRetrievals">总检索次数</span>
<span class="retrieval-stat-value">${totalLogs}</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">成功检索</span>
<span class="retrieval-stat-label" data-i18n="retrievalLogs.successRetrievals">成功检索</span>
<span class="retrieval-stat-value text-success">${successfulLogs}</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">成功率</span>
<span class="retrieval-stat-label" data-i18n="retrievalLogs.successRate">成功率</span>
<span class="retrieval-stat-value">${successRate}%</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">检索到知识项</span>
<span class="retrieval-stat-label" data-i18n="retrievalLogs.retrievedItems">检索到知识项</span>
<span class="retrieval-stat-value">${totalItems}</span>
</div>
`;
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(statsContainer);
}
}
// 获取相对时间
@@ -1591,7 +1576,7 @@ function refreshRetrievalLogs() {
// 删除检索日志
async function deleteRetrievalLog(id, index) {
if (!confirm('确定要删除这条检索记录吗?')) {
if (!confirm(_t('retrievalLogs.deleteConfirm'))) {
return;
}
@@ -1677,7 +1662,7 @@ async function deleteRetrievalLog(id, index) {
}
}
showNotification('❌ 删除检索日志失败: ' + error.message, 'error');
showNotification(_t('retrievalLogs.deleteError') + ': ' + error.message, 'error');
}
}
@@ -1699,12 +1684,11 @@ function updateRetrievalStatsAfterDelete() {
const badge = card.querySelector('.retrieval-log-result-badge');
if (badge && badge.classList.contains('success')) {
const text = badge.textContent.trim();
const match = text.match(/(\d+)\s*项/);
const match = text.match(/(\d+)/);
if (match) {
return sum + parseInt(match[1]);
} else if (text === '有结果') {
return sum + 1; // 简化处理,假设为1
return sum + parseInt(match[1], 10);
}
return sum + 1; // 有结果但数量未知(如 "Has results" / "有结果"
}
return sum;
}, 0);
@@ -1713,28 +1697,31 @@ function updateRetrievalStatsAfterDelete() {
statsContainer.innerHTML = `
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">总检索次数</span>
<span class="retrieval-stat-label" data-i18n="retrievalLogs.totalRetrievals">总检索次数</span>
<span class="retrieval-stat-value">${totalLogs}</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">成功检索</span>
<span class="retrieval-stat-label" data-i18n="retrievalLogs.successRetrievals">成功检索</span>
<span class="retrieval-stat-value text-success">${successfulLogs}</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">成功率</span>
<span class="retrieval-stat-label" data-i18n="retrievalLogs.successRate">成功率</span>
<span class="retrieval-stat-value">${successRate}%</span>
</div>
<div class="retrieval-stat-item">
<span class="retrieval-stat-label">检索到知识项</span>
<span class="retrieval-stat-label" data-i18n="retrievalLogs.retrievedItems">检索到知识项</span>
<span class="retrieval-stat-value">${totalItems}</span>
</div>
`;
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(statsContainer);
}
}
// 显示检索日志详情
async function showRetrievalLogDetails(index) {
if (!retrievalLogsData || index < 0 || index >= retrievalLogsData.length) {
showNotification('无法获取检索详情', 'error');
showNotification(_t('retrievalLogs.detailError'), 'error');
return;
}
@@ -1783,16 +1770,19 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; max-height: 90vh; overflow-y: auto;">
<div class="modal-header">
<h2>检索详情</h2>
<h2 data-i18n="retrievalLogs.detailsTitle">检索详情</h2>
<span class="modal-close" onclick="closeRetrievalLogDetailsModal()">&times;</span>
</div>
<div class="modal-body" id="retrieval-log-details-content">
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeRetrievalLogDetailsModal()">关闭</button>
<button class="btn-secondary" onclick="closeRetrievalLogDetailsModal()" data-i18n="common.close">关闭</button>
</div>
</div>
`;
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(modal);
}
document.body.appendChild(modal);
}
@@ -1816,57 +1806,57 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
return `
<div class="retrieval-detail-item-card" style="margin-bottom: 16px; padding: 16px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-secondary);">
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
<h4 style="margin: 0; color: var(--text-primary);">${idx + 1}. ${escapeHtml(item.title || '未命名')}</h4>
<span style="font-size: 0.875rem; color: var(--text-secondary);">${escapeHtml(item.category || '未分类')}</span>
<h4 style="margin: 0; color: var(--text-primary);">${idx + 1}. ${escapeHtml(item.title || _t('retrievalLogs.untitled'))}</h4>
<span style="font-size: 0.875rem; color: var(--text-secondary);">${escapeHtml(item.category || _t('retrievalLogs.uncategorized'))}</span>
</div>
${item.filePath ? `<div style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 8px;">📁 ${escapeHtml(item.filePath)}</div>` : ''}
<div style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.6;">
${escapeHtml(previewText || '无内容预览')}
${escapeHtml(previewText || _t('retrievalLogs.noContentPreview'))}
</div>
</div>
`;
}).join('');
} else {
itemsHtml = '<div style="padding: 16px; text-align: center; color: var(--text-muted);">未找到知识项详情</div>';
itemsHtml = '<div style="padding: 16px; text-align: center; color: var(--text-muted);">' + _t('retrievalLogs.noItemDetails') + '</div>';
}
content.innerHTML = `
<div style="display: flex; flex-direction: column; gap: 20px;">
<div class="retrieval-detail-section">
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">查询信息</h3>
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">${_t('retrievalLogs.queryInfo')}</h3>
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px; border-left: 3px solid var(--accent-color);">
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">查询内容:</div>
<div style="color: var(--text-primary); line-height: 1.6; word-break: break-word;">${escapeHtml(log.query || '无查询内容')}</div>
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${_t('retrievalLogs.queryContent')}</div>
<div style="color: var(--text-primary); line-height: 1.6; word-break: break-word;">${escapeHtml(log.query || _t('retrievalLogs.noQuery'))}</div>
</div>
</div>
<div class="retrieval-detail-section">
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">检索信息</h3>
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">${_t('retrievalLogs.retrievalInfo')}</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
${log.riskType ? `
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">风险类型</div>
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.riskType')}</div>
<div style="font-weight: 500; color: var(--text-primary);">${escapeHtml(log.riskType)}</div>
</div>
` : ''}
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">检索时间</div>
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.retrievalTime')}</div>
<div style="font-weight: 500; color: var(--text-primary);" title="${fullTime}">${timeAgo}</div>
</div>
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">检索结果</div>
<div style="font-weight: 500; color: var(--text-primary);">${retrievedItems.length} 个知识项</div>
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.retrievalResult')}</div>
<div style="font-weight: 500; color: var(--text-primary);">${_t('retrievalLogs.itemsCount', { count: retrievedItems.length })}</div>
</div>
</div>
</div>
${log.conversationId || log.messageId ? `
<div class="retrieval-detail-section">
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">关联信息</h3>
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">${_t('retrievalLogs.relatedInfo')}</h3>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
${log.conversationId ? `
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">对话ID</div>
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.conversationId')}</div>
<code style="font-size: 0.8125rem; color: var(--text-primary); word-break: break-all; cursor: pointer;"
onclick="navigator.clipboard.writeText('${escapeHtml(log.conversationId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
title="点击复制">${escapeHtml(log.conversationId)}</code>
@@ -1874,7 +1864,7 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
` : ''}
${log.messageId ? `
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">消息ID</div>
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.messageId')}</div>
<code style="font-size: 0.8125rem; color: var(--text-primary); word-break: break-all; cursor: pointer;"
onclick="navigator.clipboard.writeText('${escapeHtml(log.messageId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
title="点击复制">${escapeHtml(log.messageId)}</code>
@@ -1910,6 +1900,22 @@ window.addEventListener('click', function(event) {
}
});
// 语言切换时重新渲染检索历史列表与统计,使动态内容随语言更新;知识管理页的「未启用」区块已使用 data-i18n,会由 applyTranslations(document) 自动更新
document.addEventListener('languagechange', function () {
var cur = typeof window.currentPage === 'function' ? window.currentPage() : (window.currentPage || '');
if (cur === 'knowledge-retrieval-logs') {
if (retrievalLogsData && retrievalLogsData.length >= 0) {
renderRetrievalLogs(retrievalLogsData);
}
} else if (cur === 'knowledge-management') {
// 仅对「知识库未启用」状态:已有 data-i18napplyTranslations 已处理;此处可选地重新应用一次以兼容旧 DOM
var listEl = document.getElementById('knowledge-items-list');
if (listEl && typeof window.applyTranslations === 'function') {
window.applyTranslations(listEl);
}
}
});
// 页面切换时加载数据
if (typeof switchPage === 'function') {
const originalSwitchPage = switchPage;
+227 -148
View File
@@ -3,6 +3,30 @@ let activeTaskInterval = null;
const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次
const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']);
// 将后端下发的进度文案转为当前语言的翻译(已知中文 key 映射)
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'
};
if (map[trim]) return window.t(map[trim]);
const callingToolPrefix = '正在调用工具: ';
if (trim.indexOf(callingToolPrefix) === 0) {
const name = trim.slice(callingToolPrefix.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 +81,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 +104,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 +122,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 +154,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 +174,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 +189,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 +201,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 +277,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 +289,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 +309,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 +350,7 @@ async function cancelProgressTask(progressId) {
stopBtn.disabled = false;
}, 1500);
}
alert('任务信息尚未同步,请稍后再试。');
alert(typeof window.t === 'function' ? window.t('tasks.taskInfoNotSynced') : '任务信息尚未同步,请稍后再试。');
return;
}
@@ -330,7 +361,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 +369,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 +422,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);
@@ -466,41 +499,37 @@ function handleStreamEvent(event, progressElement, progressId,
case 'iteration':
// 添加迭代标记
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
});
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 +548,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 +567,30 @@ function handleStreamEvent(event, progressElement, progressId,
break;
case 'progress':
// 更新进度状态
const progressTitle = document.querySelector(`#${progressId} .progress-title`);
if (progressTitle) {
progressTitle.textContent = '🔍 ' + 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 +689,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 +697,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 +708,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 +762,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 +772,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 +826,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>';
}
// 更新标题(保留原有文本,追加状态)
@@ -854,7 +875,8 @@ 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 = (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' });
let content = `
<div class="timeline-item-header">
@@ -869,11 +891,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 +905,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 +949,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 +957,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 +987,33 @@ 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 timeText = startedTime && !isNaN(startedTime.getTime())
? startedTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' })
? startedTime.toLocaleTimeString(taskTimeLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit' })
: '';
// 根据任务状态显示不同的文本
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 +1024,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 +1037,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;
}
@@ -1138,10 +1168,10 @@ async function refreshMonitorPanel(page = null) {
} catch (error) {
console.error('刷新监控面板失败:', error);
if (statsContainer) {
statsContainer.innerHTML = `<div class="monitor-error">无法加载统计信息${escapeHtml(error.message)}</div>`;
statsContainer.innerHTML = `<div class="monitor-error">${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}${escapeHtml(error.message)}</div>`;
}
if (execContainer) {
execContainer.innerHTML = `<div class="monitor-error">无法加载执行记录${escapeHtml(error.message)}</div>`;
execContainer.innerHTML = `<div class="monitor-error">${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadExecutionsError') : '无法加载执行记录')}${escapeHtml(error.message)}</div>`;
}
}
}
@@ -1215,10 +1245,10 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter =
} catch (error) {
console.error('刷新监控面板失败:', error);
if (statsContainer) {
statsContainer.innerHTML = `<div class="monitor-error">无法加载统计信息${escapeHtml(error.message)}</div>`;
statsContainer.innerHTML = `<div class="monitor-error">${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}${escapeHtml(error.message)}</div>`;
}
if (execContainer) {
execContainer.innerHTML = `<div class="monitor-error">无法加载执行记录${escapeHtml(error.message)}</div>`;
execContainer.innerHTML = `<div class="monitor-error">${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadExecutionsError') : '无法加载执行记录')}${escapeHtml(error.message)}</div>`;
}
}
}
@@ -1232,7 +1262,8 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
const entries = Object.values(statsMap);
if (entries.length === 0) {
container.innerHTML = '<div class="monitor-empty">暂无统计数据</div>';
const noStats = typeof window.t === 'function' ? window.t('mcpMonitor.noStatsData') : '暂无统计数据';
container.innerHTML = '<div class="monitor-empty">' + escapeHtml(noStats) + '</div>';
return;
}
@@ -1252,24 +1283,32 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
);
const successRate = totals.total > 0 ? ((totals.success / totals.total) * 100).toFixed(1) : '0.0';
const lastUpdatedText = lastFetchedAt ? lastFetchedAt.toLocaleString('zh-CN') : 'N/A';
const lastCallText = totals.lastCallTime ? totals.lastCallTime.toLocaleString('zh-CN') : '暂无调用';
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined;
const lastUpdatedText = lastFetchedAt ? (lastFetchedAt.toLocaleString ? lastFetchedAt.toLocaleString(locale || 'en-US') : String(lastFetchedAt)) : 'N/A';
const noCallsYet = typeof window.t === 'function' ? window.t('mcpMonitor.noCallsYet') : '暂无调用';
const lastCallText = totals.lastCallTime ? (totals.lastCallTime.toLocaleString ? totals.lastCallTime.toLocaleString(locale || 'en-US') : String(totals.lastCallTime)) : noCallsYet;
const totalCallsLabel = typeof window.t === 'function' ? window.t('mcpMonitor.totalCalls') : '总调用次数';
const successFailedLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successFailed', { success: totals.success, failed: totals.failed }) : `成功 ${totals.success} / 失败 ${totals.failed}`;
const successRateLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successRate') : '成功率';
const statsFromAll = typeof window.t === 'function' ? window.t('mcpMonitor.statsFromAllTools') : '统计自全部工具调用';
const lastCallLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastCall') : '最近一次调用';
const lastRefreshLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastRefreshTime') : '最后刷新时间';
let html = `
<div class="monitor-stat-card">
<h4>总调用次数</h4>
<h4>${escapeHtml(totalCallsLabel)}</h4>
<div class="monitor-stat-value">${totals.total}</div>
<div class="monitor-stat-meta">成功 ${totals.success} / 失败 ${totals.failed}</div>
<div class="monitor-stat-meta">${escapeHtml(successFailedLabel)}</div>
</div>
<div class="monitor-stat-card">
<h4>成功率</h4>
<h4>${escapeHtml(successRateLabel)}</h4>
<div class="monitor-stat-value">${successRate}%</div>
<div class="monitor-stat-meta">统计自全部工具调用</div>
<div class="monitor-stat-meta">${escapeHtml(statsFromAll)}</div>
</div>
<div class="monitor-stat-card">
<h4>最近一次调用</h4>
<div class="monitor-stat-value" style="font-size:1rem;">${lastCallText}</div>
<div class="monitor-stat-meta">最后刷新时间${lastUpdatedText}</div>
<h4>${escapeHtml(lastCallLabel)}</h4>
<div class="monitor-stat-value" style="font-size:1rem;">${escapeHtml(lastCallText)}</div>
<div class="monitor-stat-meta">${escapeHtml(lastRefreshLabel)}${escapeHtml(lastUpdatedText)}</div>
</div>
`;
@@ -1280,14 +1319,16 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
.sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0))
.slice(0, 4);
const unknownToolLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknownTool') : '未知工具';
topTools.forEach(tool => {
const toolSuccessRate = tool.totalCalls > 0 ? ((tool.successCalls || 0) / tool.totalCalls * 100).toFixed(1) : '0.0';
const toolMeta = typeof window.t === 'function' ? window.t('mcpMonitor.successFailedRate', { success: tool.successCalls || 0, failed: tool.failedCalls || 0, rate: toolSuccessRate }) : `成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%`;
html += `
<div class="monitor-stat-card">
<h4>${escapeHtml(tool.toolName || '未知工具')}</h4>
<h4>${escapeHtml(tool.toolName || unknownToolLabel)}</h4>
<div class="monitor-stat-value">${tool.totalCalls || 0}</div>
<div class="monitor-stat-meta">
成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%
${escapeHtml(toolMeta)}
</div>
</div>
`;
@@ -1307,10 +1348,12 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
const toolFilter = document.getElementById('monitor-tool-filter');
const currentToolFilter = toolFilter ? toolFilter.value : 'all';
const hasFilter = (statusFilter && statusFilter !== 'all') || (currentToolFilter && currentToolFilter !== 'all');
const noRecordsFilter = typeof window.t === 'function' ? window.t('mcpMonitor.noRecordsWithFilter') : '当前筛选条件下暂无记录';
const noExecutions = typeof window.t === 'function' ? window.t('mcpMonitor.noExecutions') : '暂无执行记录';
if (hasFilter) {
container.innerHTML = '<div class="monitor-empty">当前筛选条件下暂无记录</div>';
container.innerHTML = '<div class="monitor-empty">' + escapeHtml(noRecordsFilter) + '</div>';
} else {
container.innerHTML = '<div class="monitor-empty">暂无执行记录</div>';
container.innerHTML = '<div class="monitor-empty">' + escapeHtml(noExecutions) + '</div>';
}
// 隐藏批量操作栏
const batchActions = document.getElementById('monitor-batch-actions');
@@ -1322,14 +1365,22 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
// 由于筛选已经在后端完成,这里直接使用所有传入的执行记录
// 不再需要前端再次筛选,因为后端已经返回了筛选后的数据
const unknownLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknown') : '未知';
const unknownToolLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknownTool') : '未知工具';
const viewDetailLabel = typeof window.t === 'function' ? window.t('mcpMonitor.viewDetail') : '查看详情';
const deleteLabel = typeof window.t === 'function' ? window.t('mcpMonitor.delete') : '删除';
const deleteExecTitle = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecTitle') : '删除此执行记录';
const statusKeyMap = { pending: 'statusPending', running: 'statusRunning', completed: 'statusCompleted', failed: 'statusFailed' };
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined;
const rows = executions
.map(exec => {
const status = (exec.status || 'unknown').toLowerCase();
const statusClass = `monitor-status-chip ${status}`;
const statusLabel = getStatusText(status);
const startTime = exec.startTime ? new Date(exec.startTime).toLocaleString('zh-CN') : '未知';
const statusKey = statusKeyMap[status];
const statusLabel = (typeof window.t === 'function' && statusKey) ? window.t('mcpMonitor.' + statusKey) : getStatusText(status);
const startTime = exec.startTime ? (new Date(exec.startTime).toLocaleString ? new Date(exec.startTime).toLocaleString(locale || 'en-US') : String(exec.startTime)) : unknownLabel;
const duration = formatExecutionDuration(exec.startTime, exec.endTime);
const toolName = escapeHtml(exec.toolName || '未知工具');
const toolName = escapeHtml(exec.toolName || unknownToolLabel);
const executionId = escapeHtml(exec.id || '');
return `
<tr>
@@ -1337,13 +1388,13 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
<input type="checkbox" class="monitor-execution-checkbox" value="${executionId}" onchange="updateBatchActionsState()" />
</td>
<td>${toolName}</td>
<td><span class="${statusClass}">${statusLabel}</span></td>
<td>${startTime}</td>
<td>${duration}</td>
<td><span class="${statusClass}">${escapeHtml(statusLabel)}</span></td>
<td>${escapeHtml(startTime)}</td>
<td>${escapeHtml(duration)}</td>
<td>
<div class="monitor-execution-actions">
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">查看详情</button>
<button class="btn-secondary btn-delete" onclick="deleteExecution('${executionId}')" title="删除此执行记录">删除</button>
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">${escapeHtml(viewDetailLabel)}</button>
<button class="btn-secondary btn-delete" onclick="deleteExecution('${executionId}')" title="${escapeHtml(deleteExecTitle)}">${escapeHtml(deleteLabel)}</button>
</div>
</td>
</tr>
@@ -1365,6 +1416,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
// 创建表格容器
const tableContainer = document.createElement('div');
tableContainer.className = 'monitor-table-container';
const colTool = typeof window.t === 'function' ? window.t('mcpMonitor.columnTool') : '工具';
const colStatus = typeof window.t === 'function' ? window.t('mcpMonitor.columnStatus') : '状态';
const colStartTime = typeof window.t === 'function' ? window.t('mcpMonitor.columnStartTime') : '开始时间';
const colDuration = typeof window.t === 'function' ? window.t('mcpMonitor.columnDuration') : '耗时';
const colActions = typeof window.t === 'function' ? window.t('mcpMonitor.columnActions') : '操作';
tableContainer.innerHTML = `
<table class="monitor-table">
<thead>
@@ -1372,11 +1428,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
<th style="width: 40px;">
<input type="checkbox" id="monitor-select-all" onchange="toggleSelectAll(this)" />
</th>
<th>工具</th>
<th>状态</th>
<th>开始时间</th>
<th>耗时</th>
<th>操作</th>
<th>${escapeHtml(colTool)}</th>
<th>${escapeHtml(colStatus)}</th>
<th>${escapeHtml(colStartTime)}</th>
<th>${escapeHtml(colDuration)}</th>
<th>${escapeHtml(colActions)}</th>
</tr>
</thead>
<tbody>${rows}</tbody>
@@ -1415,12 +1471,18 @@ function renderMonitorPagination() {
// 处理没有数据的情况
const startItem = total === 0 ? 0 : (page - 1) * pageSize + 1;
const endItem = total === 0 ? 0 : Math.min(page * pageSize, total);
const paginationInfoText = typeof window.t === 'function' ? window.t('mcpMonitor.paginationInfo', { start: startItem, end: endItem, total: total }) : `显示 ${startItem}-${endItem} / 共 ${total} 条记录`;
const perPageLabel = typeof window.t === 'function' ? window.t('mcpMonitor.perPageLabel') : '每页显示';
const firstPageLabel = typeof window.t === 'function' ? window.t('mcp.firstPage') : '首页';
const prevPageLabel = typeof window.t === 'function' ? window.t('mcp.prevPage') : '上一页';
const pageInfoText = typeof window.t === 'function' ? window.t('mcp.pageInfo', { page: page, total: totalPages || 1 }) : `${page} / ${totalPages || 1}`;
const nextPageLabel = typeof window.t === 'function' ? window.t('mcp.nextPage') : '下一页';
const lastPageLabel = typeof window.t === 'function' ? window.t('mcp.lastPage') : '末页';
pagination.innerHTML = `
<div class="pagination-info">
<span>显示 ${startItem}-${endItem} / ${total} 条记录</span>
<span>${escapeHtml(paginationInfoText)}</span>
<label class="pagination-page-size">
每页显示
${escapeHtml(perPageLabel)}
<select id="monitor-page-size" onchange="changeMonitorPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
@@ -1430,11 +1492,11 @@ function renderMonitorPagination() {
</label>
</div>
<div class="pagination-controls">
<button class="btn-secondary" onclick="refreshMonitorPanel(1)" ${page === 1 || total === 0 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="refreshMonitorPanel(${page - 1})" ${page === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page"> ${page} / ${totalPages || 1} </span>
<button class="btn-secondary" onclick="refreshMonitorPanel(${page + 1})" ${page >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="refreshMonitorPanel(${totalPages || 1})" ${page >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
<button class="btn-secondary" onclick="refreshMonitorPanel(1)" ${page === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(firstPageLabel)}</button>
<button class="btn-secondary" onclick="refreshMonitorPanel(${page - 1})" ${page === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(prevPageLabel)}</button>
<span class="pagination-page">${escapeHtml(pageInfoText)}</span>
<button class="btn-secondary" onclick="refreshMonitorPanel(${page + 1})" ${page >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(nextPageLabel)}</button>
<button class="btn-secondary" onclick="refreshMonitorPanel(${totalPages || 1})" ${page >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(lastPageLabel)}</button>
</div>
`;
@@ -1450,8 +1512,8 @@ async function deleteExecution(executionId) {
return;
}
// 确认删除
if (!confirm('确定要删除此执行记录吗?此操作不可恢复。')) {
const deleteConfirmMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecConfirmSingle') : '确定要删除此执行记录吗?此操作不可恢复。';
if (!confirm(deleteConfirmMsg)) {
return;
}
@@ -1462,17 +1524,20 @@ async function deleteExecution(executionId) {
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error || '删除执行记录失败');
const deleteFailedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecFailed') : '删除执行记录失败';
throw new Error(error.error || deleteFailedMsg);
}
// 删除成功后刷新当前页面
const currentPage = monitorState.pagination.page;
await refreshMonitorPanel(currentPage);
alert('执行记录已删除');
const execDeletedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.execDeleted') : '执行记录已删除';
alert(execDeletedMsg);
} catch (error) {
console.error('删除执行记录失败:', error);
alert('删除执行记录失败: ' + error.message);
const deleteFailedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecFailed') : '删除执行记录失败';
alert(deleteFailedMsg + ': ' + error.message);
}
}
@@ -1487,14 +1552,14 @@ function updateBatchActionsState() {
if (batchActions) {
batchActions.style.display = 'flex';
}
if (selectedCountSpan) {
selectedCountSpan.textContent = `已选择 ${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');
@@ -1547,15 +1612,15 @@ function deselectAllExecutions() {
async function batchDeleteExecutions() {
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked');
if (checkboxes.length === 0) {
alert('请先选择要删除的执行记录');
const selectFirstMsg = typeof window.t === 'function' ? window.t('mcpMonitor.selectExecFirst') : '请先选择要删除的执行记录';
alert(selectFirstMsg);
return;
}
const ids = Array.from(checkboxes).map(cb => cb.value);
const count = ids.length;
// 确认删除
if (!confirm(`确定要删除选中的 ${count} 条执行记录吗?此操作不可恢复。`)) {
const batchConfirmMsg = typeof window.t === 'function' ? window.t('mcpMonitor.batchDeleteConfirm', { count: count }) : `确定要删除选中的 ${count} 条执行记录吗?此操作不可恢复。`;
if (!confirm(batchConfirmMsg)) {
return;
}
@@ -1570,7 +1635,8 @@ async function batchDeleteExecutions() {
if (!response.ok) {
const error = await response.json().catch(() => ({}));
throw new Error(error.error || '批量删除执行记录失败');
const batchFailedMsg = typeof window.t === 'function' ? window.t('mcp.batchDeleteFailed') : '批量删除执行记录失败';
throw new Error(error.error || batchFailedMsg);
}
const result = await response.json().catch(() => ({}));
@@ -1580,33 +1646,46 @@ async function batchDeleteExecutions() {
const currentPage = monitorState.pagination.page;
await refreshMonitorPanel(currentPage);
alert(`成功删除 ${deletedCount} 条执行记录`);
const batchSuccessMsg = typeof window.t === 'function' ? window.t('mcpMonitor.batchDeleteSuccess', { count: deletedCount }) : `成功删除 ${deletedCount} 条执行记录`;
alert(batchSuccessMsg);
} catch (error) {
console.error('批量删除执行记录失败:', error);
alert('批量删除执行记录失败: ' + error.message);
const batchFailedMsg = typeof window.t === 'function' ? window.t('mcp.batchDeleteFailed') : '批量删除执行记录失败';
alert(batchFailedMsg + ': ' + error.message);
}
}
function formatExecutionDuration(start, end) {
const unknownLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknown') : '未知';
if (!start) {
return '未知';
return unknownLabel;
}
const startTime = new Date(start);
const endTime = end ? new Date(end) : new Date();
if (Number.isNaN(startTime.getTime()) || Number.isNaN(endTime.getTime())) {
return '未知';
return unknownLabel;
}
const diffMs = Math.max(0, endTime - startTime);
const seconds = Math.floor(diffMs / 1000);
if (seconds < 60) {
return `${seconds}`;
return typeof window.t === 'function' ? window.t('mcpMonitor.durationSeconds', { n: seconds }) : seconds + ' 秒';
}
const minutes = Math.floor(seconds / 60);
if (minutes < 60) {
const remain = seconds % 60;
return remain > 0 ? `${minutes}${remain}` : `${minutes}`;
if (remain > 0) {
return typeof window.t === 'function' ? window.t('mcpMonitor.durationMinutes', { minutes: minutes, seconds: remain }) : minutes + ' 分 ' + remain + ' 秒';
}
return typeof window.t === 'function' ? window.t('mcpMonitor.durationMinutesOnly', { minutes: minutes }) : minutes + ' 分';
}
const hours = Math.floor(minutes / 60);
const remainMinutes = minutes % 60;
return remainMinutes > 0 ? `${hours} 小时 ${remainMinutes}` : `${hours} 小时`;
if (remainMinutes > 0) {
return typeof window.t === 'function' ? window.t('mcpMonitor.durationHours', { hours: hours, minutes: remainMinutes }) : hours + ' 小时 ' + remainMinutes + ' 分';
}
return typeof window.t === 'function' ? window.t('mcpMonitor.durationHoursOnly', { hours: hours }) : hours + ' 小时';
}
document.addEventListener('languagechange', function () {
updateBatchActionsState();
});
+48 -38
View File
@@ -1,4 +1,7 @@
// 角色管理相关功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
let currentRole = localStorage.getItem('currentRole') || '';
let roles = [];
let rolesSearchKeyword = ''; // 角色搜索关键词
@@ -54,7 +57,7 @@ async function loadRoles() {
return roles;
} catch (error) {
console.error('加载角色失败:', error);
showNotification('加载角色失败: ' + error.message, 'error');
showNotification(_t('roles.loadFailed') + ': ' + error.message, 'error');
return [];
}
}
@@ -108,11 +111,13 @@ function updateRoleSelectorDisplay() {
}
}
roleSelectorIcon.textContent = icon;
roleSelectorText.textContent = selectedRole.name || '默认';
const displayName = (selectedRole.name === '默认' || !selectedRole.name) && typeof window.t === 'function'
? window.t('chat.defaultRole') : (selectedRole.name || (typeof window.t === 'function' ? window.t('chat.defaultRole') : '默认'));
roleSelectorText.textContent = displayName;
} else {
// 默认角色
roleSelectorIcon.textContent = '🔵';
roleSelectorText.textContent = '默认';
roleSelectorText.textContent = typeof window.t === 'function' ? window.t('chat.defaultRole') : '默认';
}
}
@@ -165,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 = `
@@ -280,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;
}
@@ -310,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个工具名称
@@ -322,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 `
@@ -339,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>
`;
@@ -501,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>`;
}
}
}
@@ -519,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;
}
@@ -592,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>
`;
@@ -725,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;
}
@@ -777,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>
`;
}
@@ -836,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 = '';
@@ -916,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 || '';
@@ -1184,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;
}
@@ -1225,7 +1230,7 @@ async function saveRole() {
// 如果是首次添加角色且没有选择工具,默认使用全部工具
if (isFirstUserRole && allSelectedTools.length === 0) {
roleUsesAllTools = true;
showNotification('检测到这是首次添加角色且未选择工具,将默认使用全部工具', 'info');
showNotification(_t('roleModal.firstRoleNoToolsHint'), 'info');
} else if (roleUsesAllTools) {
// 如果当前使用所有工具,需要检查用户是否取消了一些工具
// 检查状态映射中是否有未选中的已启用工具
@@ -1356,7 +1361,7 @@ async function saveRole() {
// 删除角色
async function deleteRole(roleName) {
if (roleName === '默认') {
showNotification('不能删除默认角色', 'error');
showNotification(_t('roleModal.cannotDeleteDefaultRole'), 'error');
return;
}
@@ -1428,6 +1433,11 @@ document.addEventListener('DOMContentLoaded', () => {
updateRoleSelectorDisplay();
});
// 语言切换后刷新角色选择器显示(默认/自定义角色名)
document.addEventListener('languagechange', () => {
updateRoleSelectorDisplay();
});
// 获取当前选中的角色(供chat.js使用)
function getCurrentRole() {
return currentRole || '';
@@ -1467,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>';
}
}
}
@@ -1488,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;
@@ -1594,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转义函数
+105 -73
View File
@@ -255,7 +255,10 @@ async function loadConfig(loadTools = true) {
}
} catch (error) {
console.error('加载配置失败:', error);
alert('加载配置失败: ' + error.message);
const baseMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('settings.apply.loadFailed')
: '加载配置失败';
alert(baseMsg + ': ' + error.message);
}
}
@@ -269,7 +272,7 @@ async function loadToolsList(page = 1, searchKeyword = '') {
// 显示加载状态
if (toolsList) {
// 清空整个容器,包括可能存在的分页控件
toolsList.innerHTML = '<div class="tools-list-items"><div class="loading" style="padding: 20px; text-align: center; color: var(--text-muted);">⏳ 正在加载工具列表...</div></div>';
toolsList.innerHTML = '<div class="tools-list-items"><div class="loading" style="padding: 20px; text-align: center; color: var(--text-muted);">⏳ ' + (typeof window.t === 'function' ? window.t('mcp.loadingTools') : '正在加载工具列表...') + '</div></div>';
}
try {
@@ -324,8 +327,8 @@ async function loadToolsList(page = 1, searchKeyword = '') {
if (toolsList) {
const isTimeout = error.name === 'AbortError' || error.message.includes('timeout');
const errorMsg = isTimeout
? '加载工具列表超时,可能是外部MCP连接较慢。请点击"刷新"按钮重试,或检查外部MCP连接状态。'
: `加载工具列表失败: ${escapeHtml(error.message)}`;
? (typeof window.t === 'function' ? window.t('mcp.loadToolsTimeout') : '加载工具列表超时,可能是外部MCP连接较慢。请点击"刷新"按钮重试,或检查外部MCP连接状态。')
: (typeof window.t === 'function' ? window.t('mcp.loadToolsFailed') : '加载工具列表失败') + ': ' + escapeHtml(error.message);
toolsList.innerHTML = `<div class="error" style="padding: 20px; text-align: center;">${errorMsg}</div>`;
}
}
@@ -399,7 +402,7 @@ function renderToolsList() {
listContainer.innerHTML = '';
if (allTools.length === 0) {
listContainer.innerHTML = '<div class="empty">暂无工具</div>';
listContainer.innerHTML = '<div class="empty">' + (typeof window.t === 'function' ? window.t('mcp.noTools') : '暂无工具') + '</div>';
if (!toolsList.contains(listContainer)) {
toolsList.appendChild(listContainer);
}
@@ -428,8 +431,8 @@ function renderToolsList() {
let externalBadge = '';
if (toolState.is_external || tool.is_external) {
const externalMcpName = toolState.external_mcp || tool.external_mcp || '';
const badgeText = externalMcpName ? `外部 (${escapeHtml(externalMcpName)})` : '外部';
const badgeTitle = externalMcpName ? `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}` : '外部MCP工具';
const badgeText = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalFrom', { name: escapeHtml(externalMcpName) }) : `外部 (${escapeHtml(externalMcpName)})`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部');
const badgeTitle = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalToolFrom', { name: escapeHtml(externalMcpName) }) : `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部MCP工具');
externalBadge = `<span class="external-tool-badge" title="${badgeTitle}">${badgeText}</span>`;
}
@@ -443,7 +446,7 @@ function renderToolsList() {
${escapeHtml(tool.name)}
${externalBadge}
</div>
<div class="tool-item-desc">${escapeHtml(tool.description || '无描述')}</div>
<div class="tool-item-desc">${escapeHtml(tool.description || (typeof window.t === 'function' ? window.t('mcp.noDescription') : '无描述'))}</div>
</div>
`;
listContainer.appendChild(toolItem);
@@ -481,12 +484,19 @@ function renderToolsPagination() {
const endItem = Math.min(page * toolsPagination.pageSize, total);
const savedPageSize = getToolsPageSize();
const t = typeof window.t === 'function' ? window.t : (k) => k;
const paginationT = (key, opts) => {
if (typeof window.t === 'function') return window.t(key, opts);
if (key === 'mcp.paginationInfo' && opts) return `显示 ${opts.start}-${opts.end} / 共 ${opts.total} 个工具`;
if (key === 'mcp.pageInfo' && opts) return `${opts.page} / ${opts.total}`;
return key;
};
pagination.innerHTML = `
<div class="pagination-info">
显示 ${startItem}-${endItem} / ${total} 个工具${toolsSearchKeyword ? ` (搜索: "${escapeHtml(toolsSearchKeyword)}")` : ''}
${paginationT('mcp.paginationInfo', { start: startItem, end: endItem, total: total })}${toolsSearchKeyword ? ` (${t('common.search')}: "${escapeHtml(toolsSearchKeyword)}")` : ''}
</div>
<div class="pagination-page-size">
<label for="tools-page-size-pagination">每页:</label>
<label for="tools-page-size-pagination">${t('mcp.perPage')}</label>
<select id="tools-page-size-pagination" onchange="changeToolsPageSize()">
<option value="10" ${savedPageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${savedPageSize === 20 ? 'selected' : ''}>20</option>
@@ -495,11 +505,11 @@ function renderToolsPagination() {
</select>
</div>
<div class="pagination-controls">
<button class="btn-secondary" onclick="loadToolsList(1, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="loadToolsList(${page - 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page"> ${page} / ${totalPages} </span>
<button class="btn-secondary" onclick="loadToolsList(${page + 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="loadToolsList(${totalPages}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>末页</button>
<button class="btn-secondary" onclick="loadToolsList(1, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${t('mcp.firstPage')}</button>
<button class="btn-secondary" onclick="loadToolsList(${page - 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${t('mcp.prevPage')}</button>
<span class="pagination-page">${paginationT('mcp.pageInfo', { page: page, total: totalPages })}</span>
<button class="btn-secondary" onclick="loadToolsList(${page + 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${t('mcp.nextPage')}</button>
<button class="btn-secondary" onclick="loadToolsList(${totalPages}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${t('mcp.lastPage')}</button>
</div>
`;
@@ -693,9 +703,10 @@ async function updateToolsStats() {
totalEnabled = currentPageEnabled;
}
const tStats = typeof window.t === 'function' ? window.t : (k) => k;
statsEl.innerHTML = `
<span title="当前页启用的工具数"> 当前页已启用: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
<span title="所有工具中启用的工具总数">📊 总计已启用: <strong>${totalEnabled}</strong> / ${totalTools}</span>
<span title="${tStats('mcp.currentPageEnabled')}"> ${tStats('mcp.currentPageEnabled')}: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
<span title="${tStats('mcp.totalEnabled')}">📊 ${tStats('mcp.totalEnabled')}: <strong>${totalEnabled}</strong> / ${totalTools}</span>
`;
}
@@ -737,7 +748,10 @@ async function applySettings() {
}
if (hasError) {
alert('请填写所有必填字段(标记为 * 的字段)');
const msg = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('settings.apply.fillRequired')
: '请填写所有必填字段(标记为 * 的字段)';
alert(msg);
return;
}
@@ -896,7 +910,10 @@ async function applySettings() {
if (!updateResponse.ok) {
const error = await updateResponse.json();
throw new Error(error.error || '更新配置失败');
const fallback = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('settings.apply.applyFailed')
: '应用配置失败';
throw new Error(error.error || fallback);
}
// 应用配置
@@ -906,14 +923,23 @@ async function applySettings() {
if (!applyResponse.ok) {
const error = await applyResponse.json();
throw new Error(error.error || '应用配置失败');
const fallback = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('settings.apply.applyFailed')
: '应用配置失败';
throw new Error(error.error || fallback);
}
alert('配置已成功应用!');
const successMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('settings.apply.applySuccess')
: '配置已成功应用!';
alert(successMsg);
closeSettings();
} catch (error) {
console.error('应用配置失败:', error);
alert('应用配置失败: ' + error.message);
const baseMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('settings.apply.applyFailed')
: '应用配置失败';
alert(baseMsg + ': ' + error.message);
}
}
@@ -1024,7 +1050,7 @@ async function saveToolsConfig() {
throw new Error(error.error || '应用配置失败');
}
alert('工具配置已成功保存!');
alert(typeof window.t === 'function' ? window.t('mcp.toolsConfigSaved') : '工具配置已成功保存!');
// 重新加载工具列表以反映最新状态
if (typeof loadToolsList === 'function') {
@@ -1032,7 +1058,7 @@ async function saveToolsConfig() {
}
} catch (error) {
console.error('保存工具配置失败:', error);
alert('保存工具配置失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('mcp.saveToolsConfigFailed') : '保存工具配置失败') + ': ' + error.message);
}
}
@@ -1079,7 +1105,7 @@ async function changePassword() {
}
if (hasError) {
alert('请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。');
alert(typeof window.t === 'function' ? window.t('settings.security.fillPasswordHint') : '请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。');
return;
}
@@ -1104,13 +1130,14 @@ async function changePassword() {
throw new Error(result.error || '修改密码失败');
}
alert('密码已更新,请使用新密码重新登录。');
const pwdMsg = typeof window.t === 'function' ? window.t('settings.security.passwordUpdated') : '密码已更新,请使用新密码重新登录。';
alert(pwdMsg);
resetPasswordForm();
handleUnauthorized({ message: '密码已更新,请使用新密码重新登录。', silent: false });
handleUnauthorized({ message: pwdMsg, silent: false });
closeSettings();
} catch (error) {
console.error('修改密码失败:', error);
alert('修改密码失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('settings.security.changePasswordFailed') : '修改密码失败') + ': ' + error.message);
} finally {
if (submitBtn) {
submitBtn.disabled = false;
@@ -1173,7 +1200,8 @@ function renderExternalMCPList(servers) {
if (!list) return;
if (Object.keys(servers).length === 0) {
list.innerHTML = '<div class="empty">📋 暂无外部MCP配置<br><span style="font-size: 0.875rem; margin-top: 8px; display: block;">点击"添加外部MCP"按钮开始配置</span></div>';
const emptyT = typeof window.t === 'function' ? window.t : (k) => k;
list.innerHTML = '<div class="empty">📋 ' + emptyT('mcp.noExternalMCP') + '<br><span style="font-size: 0.875rem; margin-top: 8px; display: block;">' + emptyT('mcp.clickToAddExternal') + '</span></div>';
return;
}
@@ -1184,10 +1212,11 @@ function renderExternalMCPList(servers) {
status === 'connecting' ? 'status-connecting' :
status === 'error' ? 'status-error' :
status === 'disabled' ? 'status-disabled' : 'status-disconnected';
const statusText = status === 'connected' ? '已连接' :
status === 'connecting' ? '连接中...' :
status === 'error' ? '连接失败' :
status === 'disabled' ? '已禁用' : '未连接';
const statusT = typeof window.t === 'function' ? window.t : (k) => k;
const statusText = status === 'connected' ? statusT('mcp.connected') :
status === 'connecting' ? statusT('mcp.connecting') :
status === 'error' ? statusT('mcp.connectionFailed') :
status === 'disabled' ? statusT('mcp.disabled') : statusT('mcp.disconnected');
const transport = server.config.transport || (server.config.command ? 'stdio' : 'http');
const transportIcon = transport === 'stdio' ? '⚙️' : '🌐';
@@ -1200,15 +1229,15 @@ function renderExternalMCPList(servers) {
</div>
<div class="external-mcp-item-actions">
${status === 'connected' || status === 'disconnected' || status === 'error' ?
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" onclick="toggleExternalMCP('${escapeHtml(name)}', '${status}')" title="${status === 'connected' ? '停止连接' : '启动连接'}">
${status === 'connected' ? '⏸ 停止' : '▶ 启动'}
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" onclick="toggleExternalMCP('${escapeHtml(name)}', '${status}')" title="${status === 'connected' ? statusT('mcp.stopConnection') : statusT('mcp.startConnection')}">
${status === 'connected' ? '⏸ ' + statusT('mcp.stop') : '▶ ' + statusT('mcp.start')}
</button>` :
status === 'connecting' ?
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" disabled style="opacity: 0.6; cursor: not-allowed;">
连接中...
</button>` : ''}
<button class="btn-small" onclick="editExternalMCP('${escapeHtml(name)}')" title="编辑配置" ${status === 'connecting' ? 'disabled' : ''}> 编辑</button>
<button class="btn-small btn-danger" onclick="deleteExternalMCP('${escapeHtml(name)}')" title="删除配置" ${status === 'connecting' ? 'disabled' : ''}>🗑 删除</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>
</div>
</div>
${status === 'error' && server.error ? `
@@ -1217,31 +1246,31 @@ function renderExternalMCPList(servers) {
</div>` : ''}
<div class="external-mcp-item-details">
<div>
<strong>传输模式</strong>
<strong>${statusT('mcp.transportMode')}</strong>
<span>${transportIcon} ${escapeHtml(transport.toUpperCase())}</span>
</div>
${server.tool_count !== undefined && server.tool_count > 0 ? `
<div>
<strong>工具数量</strong>
<strong>${statusT('mcp.toolCount')}</strong>
<span style="font-weight: 600; color: var(--accent-color);">🔧 ${server.tool_count} 个工具</span>
</div>` : server.tool_count === 0 && status === 'connected' ? `
<div>
<strong>工具数量</strong>
<span style="color: var(--text-muted);">暂无工具</span>
<strong>${statusT('mcp.toolCount')}</strong>
<span style="color: var(--text-muted);">${statusT('mcp.noTools')}</span>
</div>` : ''}
${server.config.description ? `
<div>
<strong>描述</strong>
<strong>${statusT('mcp.description')}</strong>
<span>${escapeHtml(server.config.description)}</span>
</div>` : ''}
${server.config.timeout ? `
<div>
<strong>超时时间</strong>
<strong>${statusT('mcp.timeout')}</strong>
<span>${server.config.timeout} </span>
</div>` : ''}
${transport === 'stdio' && server.config.command ? `
<div>
<strong>命令</strong>
<strong>${statusT('mcp.command')}</strong>
<span style="font-family: monospace; font-size: 0.8125rem;">${escapeHtml(server.config.command)}</span>
</div>` : ''}
${transport === 'http' && server.config.url ? `
@@ -1267,18 +1296,19 @@ function renderExternalMCPStats(stats) {
const disabled = stats.disabled || 0;
const connected = stats.connected || 0;
const statsT = typeof window.t === 'function' ? window.t : (k) => k;
statsEl.innerHTML = `
<span title="总配置数">📊 总数: <strong>${total}</strong></span>
<span title="已启用的配置数"> 已启用: <strong>${enabled}</strong></span>
<span title="已停用的配置数"> 已停用: <strong>${disabled}</strong></span>
<span title="当前已连接的配置数">🔗 已连接: <strong>${connected}</strong></span>
<span title="${statsT('mcp.totalCount')}">📊 ${statsT('mcp.totalCount')}: <strong>${total}</strong></span>
<span title="${statsT('mcp.enabledCount')}"> ${statsT('mcp.enabledCount')}: <strong>${enabled}</strong></span>
<span title="${statsT('mcp.disabledCount')}"> ${statsT('mcp.disabledCount')}: <strong>${disabled}</strong></span>
<span title="${statsT('mcp.connectedCount')}">🔗 ${statsT('mcp.connectedCount')}: <strong>${connected}</strong></span>
`;
}
// 显示添加外部MCP模态框
function showAddExternalMCPModal() {
currentEditingMCPName = null;
document.getElementById('external-mcp-modal-title').textContent = '添加外部MCP';
document.getElementById('external-mcp-modal-title').textContent = (typeof window.t === 'function' ? window.t('mcp.addExternalMCP') : '添加外部MCP');
document.getElementById('external-mcp-json').value = '';
document.getElementById('external-mcp-json-error').style.display = 'none';
document.getElementById('external-mcp-json-error').textContent = '';
@@ -1303,7 +1333,7 @@ async function editExternalMCP(name) {
const server = await response.json();
currentEditingMCPName = name;
document.getElementById('external-mcp-modal-title').textContent = '编辑外部MCP';
document.getElementById('external-mcp-modal-title').textContent = (typeof window.t === 'function' ? window.t('mcp.editExternalMCP') : '编辑外部MCP');
// 将配置转换为对象格式(key为名称)
const config = { ...server.config };
@@ -1325,7 +1355,7 @@ async function editExternalMCP(name) {
document.getElementById('external-mcp-modal').style.display = 'block';
} catch (error) {
console.error('编辑外部MCP失败:', error);
alert('编辑失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('mcp.operationFailed') : '编辑失败') + ': ' + error.message);
}
}
@@ -1337,7 +1367,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;
@@ -1349,7 +1379,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');
}
@@ -1357,6 +1387,7 @@ function formatExternalMCPJSON() {
// 加载示例
function loadExternalMCPExample() {
const desc = (typeof window.t === 'function' ? window.t('externalMcpModal.exampleDescription') : '示例描述');
const example = {
"hexstrike-ai": {
command: "python3",
@@ -1365,7 +1396,7 @@ function loadExternalMCPExample() {
"--server",
"http://example.com"
],
description: "示例描述",
description: desc,
timeout: 300
},
"cyberstrike-ai-http": {
@@ -1390,7 +1421,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();
@@ -1401,16 +1432,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;
@@ -1419,7 +1451,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;
@@ -1428,7 +1460,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;
@@ -1436,7 +1468,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;
@@ -1448,28 +1480,28 @@ async function saveExternalMCP() {
// 验证配置内容
const transport = config.transport || (config.command ? 'stdio' : config.url ? 'http' : '');
if (!transport) {
errorDiv.textContent = `配置错误: "${name}" 需要指定commandstdio模式)或urlhttp/sse模式)`;
errorDiv.textContent = t('mcp.configNeedCommand', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
}
if (transport === 'stdio' && !config.command) {
errorDiv.textContent = `配置错误: "${name}" stdio模式需要command字段`;
errorDiv.textContent = t('mcp.configStdioNeedCommand', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
}
if (transport === 'http' && !config.url) {
errorDiv.textContent = `配置错误: "${name}" http模式需要url字段`;
errorDiv.textContent = t('mcp.configHttpNeedUrl', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
}
if (transport === 'sse' && !config.url) {
errorDiv.textContent = `配置错误: "${name}" sse模式需要url字段`;
errorDiv.textContent = t('mcp.configSseNeedUrl', { name: name });
errorDiv.style.display = 'block';
jsonTextarea.classList.add('error');
return;
@@ -1484,7 +1516,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;
@@ -1528,10 +1560,10 @@ async function saveExternalMCP() {
}
// 轮询几次以拉取后端异步更新的工具数量(无固定延迟,拿到即停)
pollExternalMCPToolCount(null, 5);
alert('保存成功');
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');
}
@@ -1539,7 +1571,7 @@ async function saveExternalMCP() {
// 删除外部MCP
async function deleteExternalMCP(name) {
if (!confirm(`确定要删除外部MCP "${name}" 吗?`)) {
if (!confirm((typeof window.t === 'function' ? window.t('mcp.deleteExternalConfirm', { name: name }) : `确定要删除外部MCP "${name}" 吗?`))) {
return;
}
@@ -1558,10 +1590,10 @@ async function deleteExternalMCP(name) {
if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') {
window.refreshMentionTools();
}
alert('删除成功');
alert(typeof window.t === 'function' ? window.t('mcp.deleteSuccess') : '删除成功');
} catch (error) {
console.error('删除外部MCP失败:', error);
alert('删除失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('mcp.operationFailed') : '删除失败') + ': ' + error.message);
}
}
@@ -1626,7 +1658,7 @@ async function toggleExternalMCP(name, currentStatus) {
}
} catch (error) {
console.error('切换外部MCP状态失败:', error);
alert('操作失败: ' + error.message);
alert((typeof window.t === 'function' ? window.t('mcp.operationFailed') : '操作失败') + ': ' + error.message);
// 恢复按钮状态
if (button) {
@@ -1679,7 +1711,7 @@ async function pollExternalMCPStatus(name, maxAttempts = 30) {
window.refreshMentionTools();
}
if (status === 'error') {
alert('连接失败,请检查配置和网络连接');
alert(typeof window.t === 'function' ? window.t('mcp.connectionFailedCheck') : '连接失败,请检查配置和网络连接');
}
return;
} else if (status === 'connecting') {
@@ -1701,7 +1733,7 @@ async function pollExternalMCPStatus(name, maxAttempts = 30) {
if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') {
window.refreshMentionTools();
}
alert('连接超时,请检查配置和网络连接');
alert(typeof window.t === 'function' ? window.t('mcp.connectionTimeout') : '连接超时,请检查配置和网络连接');
}
// 在打开设置时加载外部MCP列表
+94 -66
View File
@@ -1,4 +1,7 @@
// Skills管理相关功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
let skillsList = [];
let currentEditingSkillName = null;
let isSavingSkill = false; // 防止重复提交
@@ -65,7 +68,7 @@ async function loadSkills(page = 1, pageSize = null) {
const response = await apiFetch(url);
if (!response.ok) {
throw new Error('获取skills列表失败');
throw new Error(_t('skills.loadListFailed'));
}
const data = await response.json();
skillsList = data.skills || [];
@@ -76,10 +79,10 @@ async function loadSkills(page = 1, pageSize = null) {
updateSkillsManagementStats();
} catch (error) {
console.error('加载skills列表失败:', error);
showNotification('加载skills列表失败: ' + error.message, 'error');
showNotification(_t('skills.loadListFailed') + ': ' + error.message, 'error');
const skillsListEl = document.getElementById('skills-list');
if (skillsListEl) {
skillsListEl.innerHTML = '<div class="empty-state">加载失败: ' + error.message + '</div>';
skillsListEl.innerHTML = '<div class="empty-state">' + _t('skills.loadFailedShort') + ': ' + escapeHtml(error.message) + '</div>';
}
}
}
@@ -94,7 +97,7 @@ function renderSkillsList() {
if (filteredSkills.length === 0) {
skillsListEl.innerHTML = '<div class="empty-state">' +
(skillsSearchKeyword ? '没有找到匹配的skills' : '暂无skills,点击"创建Skill"创建第一个skill') +
(skillsSearchKeyword ? _t('skills.noMatch') : _t('skills.noSkills')) +
'</div>';
// 搜索时隐藏分页
const paginationContainer = document.getElementById('skills-pagination');
@@ -109,12 +112,12 @@ function renderSkillsList() {
<div class="skill-card">
<div class="skill-card-header">
<h3 class="skill-card-title">${escapeHtml(skill.name || '')}</h3>
<div class="skill-card-description">${escapeHtml(skill.description || '无描述')}</div>
<div class="skill-card-description">${escapeHtml(skill.description || _t('skills.noDescription'))}</div>
</div>
<div class="skill-card-actions">
<button class="btn-secondary btn-small" onclick="viewSkill('${escapeHtml(skill.name)}')">查看</button>
<button class="btn-secondary btn-small" onclick="editSkill('${escapeHtml(skill.name)}')">编辑</button>
<button class="btn-secondary btn-small btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')">删除</button>
<button class="btn-secondary btn-small" onclick="viewSkill('${escapeHtml(skill.name)}')">${_t('common.view')}</button>
<button class="btn-secondary btn-small" onclick="editSkill('${escapeHtml(skill.name)}')">${_t('common.edit')}</button>
<button class="btn-secondary btn-small btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')">${_t('common.delete')}</button>
</div>
</div>
`;
@@ -154,12 +157,19 @@ function renderSkillsPagination() {
let paginationHTML = '<div class="pagination">';
const paginationShowText = _t('skillsPage.paginationShow', { start, end, total });
const perPageLabelText = _t('skillsPage.perPageLabel');
const firstPageText = _t('skillsPage.firstPage');
const prevPageText = _t('skillsPage.prevPage');
const pageOfText = _t('skillsPage.pageOf', { current: currentPage, total: totalPages || 1 });
const nextPageText = _t('skillsPage.nextPage');
const lastPageText = _t('skillsPage.lastPage');
// 左侧:显示范围信息和每页数量选择器(参考MCP样式)
paginationHTML += `
<div class="pagination-info">
<span>显示 ${start}-${end} / ${total} </span>
<span>${escapeHtml(paginationShowText)}</span>
<label class="pagination-page-size">
每页显示
${escapeHtml(perPageLabelText)}
<select id="skills-page-size-pagination" onchange="changeSkillsPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
@@ -173,11 +183,11 @@ function renderSkillsPagination() {
// 右侧:分页按钮(参考MCP样式:首页、上一页、第X/Y页、下一页、末页)
paginationHTML += `
<div class="pagination-controls">
<button class="btn-secondary" onclick="loadSkills(1, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="loadSkills(${currentPage - 1}, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page"> ${currentPage} / ${totalPages || 1} </span>
<button class="btn-secondary" onclick="loadSkills(${currentPage + 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="loadSkills(${totalPages || 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
<button class="btn-secondary" onclick="loadSkills(1, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(firstPageText)}</button>
<button class="btn-secondary" onclick="loadSkills(${currentPage - 1}, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(prevPageText)}</button>
<span class="pagination-page">${escapeHtml(pageOfText)}</span>
<button class="btn-secondary" onclick="loadSkills(${currentPage + 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(nextPageText)}</button>
<button class="btn-secondary" onclick="loadSkills(${totalPages || 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(lastPageText)}</button>
</div>
`;
@@ -291,7 +301,7 @@ async function searchSkills() {
try {
const response = await apiFetch(`/api/skills?search=${encodeURIComponent(skillsSearchKeyword)}&limit=10000&offset=0`);
if (!response.ok) {
throw new Error('获取skills列表失败');
throw new Error(_t('skills.loadListFailed'));
}
const data = await response.json();
skillsList = data.skills || [];
@@ -306,7 +316,7 @@ async function searchSkills() {
updateSkillsManagementStats();
} catch (error) {
console.error('搜索skills失败:', error);
showNotification('搜索失败: ' + error.message, 'error');
showNotification(_t('skills.searchFailed') + ': ' + error.message, 'error');
}
} else {
// 没有搜索关键词时,恢复分页加载
@@ -332,7 +342,7 @@ function clearSkillsSearch() {
// 刷新skills
async function refreshSkills() {
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
showNotification('已刷新', 'success');
showNotification(_t('skills.refreshed'), 'success');
}
// 显示添加skill模态框
@@ -340,7 +350,7 @@ function showAddSkillModal() {
const modal = document.getElementById('skill-modal');
if (!modal) return;
document.getElementById('skill-modal-title').textContent = '添加Skill';
document.getElementById('skill-modal-title').textContent = _t('skills.addSkill');
document.getElementById('skill-name').value = '';
document.getElementById('skill-name').disabled = false;
document.getElementById('skill-description').value = '';
@@ -354,7 +364,7 @@ async function editSkill(skillName) {
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) {
throw new Error('获取skill详情失败');
throw new Error(_t('skills.loadDetailFailed'));
}
const data = await response.json();
const skill = data.skill;
@@ -362,7 +372,7 @@ async function editSkill(skillName) {
const modal = document.getElementById('skill-modal');
if (!modal) return;
document.getElementById('skill-modal-title').textContent = '编辑Skill';
document.getElementById('skill-modal-title').textContent = _t('skills.editSkill');
document.getElementById('skill-name').value = skill.name;
document.getElementById('skill-name').disabled = true; // 编辑时不允许修改名称
document.getElementById('skill-description').value = skill.description || '';
@@ -372,7 +382,7 @@ async function editSkill(skillName) {
modal.style.display = 'flex';
} catch (error) {
console.error('加载skill详情失败:', error);
showNotification('加载skill详情失败: ' + error.message, 'error');
showNotification(_t('skills.loadDetailFailed') + ': ' + error.message, 'error');
}
}
@@ -381,7 +391,7 @@ async function viewSkill(skillName) {
try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) {
throw new Error('获取skill详情失败');
throw new Error(_t('skills.loadDetailFailed'));
}
const data = await response.json();
const skill = data.skill;
@@ -390,22 +400,29 @@ async function viewSkill(skillName) {
const modal = document.createElement('div');
modal.className = 'modal';
modal.id = 'skill-view-modal';
const viewTitle = _t('skills.viewSkillTitle', { name: skill.name });
const descLabel = _t('skills.descriptionLabel');
const pathLabel = _t('skills.pathLabel');
const modTimeLabel = _t('skills.modTimeLabel');
const contentLabel = _t('skills.contentLabel');
const closeBtn = _t('common.close');
const editBtn = _t('common.edit');
modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; max-height: 90vh;">
<div class="modal-header">
<h2>查看Skill: ${escapeHtml(skill.name)}</h2>
<h2>${escapeHtml(viewTitle)}</h2>
<span class="modal-close" onclick="closeSkillViewModal()">&times;</span>
</div>
<div class="modal-body" style="overflow-y: auto; max-height: calc(90vh - 120px);">
${skill.description ? `<div style="margin-bottom: 16px;"><strong>描述:</strong> ${escapeHtml(skill.description)}</div>` : ''}
<div style="margin-bottom: 8px;"><strong>路径:</strong> ${escapeHtml(skill.path || '')}</div>
<div style="margin-bottom: 16px;"><strong>修改时间:</strong> ${escapeHtml(skill.mod_time || '')}</div>
<div style="margin-bottom: 8px;"><strong>内容:</strong></div>
${skill.description ? `<div style="margin-bottom: 16px;"><strong>${escapeHtml(descLabel)}</strong> ${escapeHtml(skill.description)}</div>` : ''}
<div style="margin-bottom: 8px;"><strong>${escapeHtml(pathLabel)}</strong> ${escapeHtml(skill.path || '')}</div>
<div style="margin-bottom: 16px;"><strong>${escapeHtml(modTimeLabel)}</strong> ${escapeHtml(skill.mod_time || '')}</div>
<div style="margin-bottom: 8px;"><strong>${escapeHtml(contentLabel)}</strong></div>
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(skill.content || '')}</pre>
</div>
<div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillViewModal()">关闭</button>
<button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">编辑</button>
<button class="btn-secondary" onclick="closeSkillViewModal()">${escapeHtml(closeBtn)}</button>
<button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">${escapeHtml(editBtn)}</button>
</div>
</div>
`;
@@ -413,7 +430,7 @@ async function viewSkill(skillName) {
modal.style.display = 'flex';
} catch (error) {
console.error('查看skill失败:', error);
showNotification('查看skill失败: ' + error.message, 'error');
showNotification(_t('skills.viewFailed') + ': ' + error.message, 'error');
}
}
@@ -443,18 +460,18 @@ async function saveSkill() {
const content = document.getElementById('skill-content').value.trim();
if (!name) {
showNotification('skill名称不能为空', 'error');
showNotification(_t('skills.nameRequired'), 'error');
return;
}
if (!content) {
showNotification('skill内容不能为空', 'error');
showNotification(_t('skills.contentRequired'), 'error');
return;
}
// 验证skill名称
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
showNotification('skill名称只能包含字母、数字、连字符和下划线', 'error');
showNotification(_t('skills.nameInvalid'), 'error');
return;
}
@@ -462,7 +479,7 @@ async function saveSkill() {
const saveBtn = document.querySelector('#skill-modal .btn-primary');
if (saveBtn) {
saveBtn.disabled = true;
saveBtn.textContent = '保存中...';
saveBtn.textContent = _t('skills.saving');
}
try {
@@ -484,20 +501,20 @@ async function saveSkill() {
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '保存skill失败');
throw new Error(error.error || _t('skills.saveFailed'));
}
showNotification(isEdit ? 'skill已更新' : 'skill已创建', 'success');
showNotification(isEdit ? _t('skills.saveSuccess') : _t('skills.createdSuccess'), 'success');
closeSkillModal();
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
} catch (error) {
console.error('保存skill失败:', error);
showNotification('保存skill失败: ' + error.message, 'error');
showNotification(_t('skills.saveFailed') + ': ' + error.message, 'error');
} finally {
isSavingSkill = false;
if (saveBtn) {
saveBtn.disabled = false;
saveBtn.textContent = '保存';
saveBtn.textContent = _t('common.save');
}
}
}
@@ -518,10 +535,10 @@ async function deleteSkill(skillName) {
}
// 构建确认消息
let confirmMessage = `确定要删除skill "${skillName}" 吗?此操作不可恢复。`;
let confirmMessage = _t('skills.deleteConfirm', { name: skillName });
if (boundRoles.length > 0) {
const rolesList = boundRoles.join('、');
confirmMessage = `确定要删除skill "${skillName}" 吗?\n\n⚠️ 该skill当前已被以下 ${boundRoles.length} 个角色绑定:\n${rolesList}\n\n删除后,系统将自动从这些角色中移除该skill的绑定。\n\n此操作不可恢复,是否继续?`;
confirmMessage = _t('skills.deleteConfirmWithRoles', { name: skillName, count: boundRoles.length, roles: rolesList });
}
if (!confirm(confirmMessage)) {
@@ -535,14 +552,14 @@ async function deleteSkill(skillName) {
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '删除skill失败');
throw new Error(error.error || _t('skills.deleteFailed'));
}
const data = await response.json();
let successMessage = 'skill已删除';
let successMessage = _t('skills.deleteSuccess');
if (data.affected_roles && data.affected_roles.length > 0) {
const rolesList = data.affected_roles.join('、');
successMessage = `skill已删除,已自动从 ${data.affected_roles.length} 个角色中移除绑定:${rolesList}`;
successMessage = _t('skills.deleteSuccessWithRoles', { count: data.affected_roles.length, roles: rolesList });
}
showNotification(successMessage, 'success');
@@ -554,7 +571,7 @@ async function deleteSkill(skillName) {
await loadSkills(pageToLoad, skillsPagination.pageSize);
} catch (error) {
console.error('删除skill失败:', error);
showNotification('删除skill失败: ' + error.message, 'error');
showNotification(_t('skills.deleteFailed') + ': ' + error.message, 'error');
}
}
@@ -565,7 +582,7 @@ async function loadSkillsMonitor() {
try {
const response = await apiFetch('/api/skills/stats');
if (!response.ok) {
throw new Error('获取skills统计信息失败');
throw new Error(_t('skills.loadStatsFailed'));
}
const data = await response.json();
@@ -581,14 +598,14 @@ async function loadSkillsMonitor() {
renderSkillsMonitor();
} catch (error) {
console.error('加载skills监控数据失败:', error);
showNotification('加载skills监控数据失败: ' + error.message, 'error');
showNotification(_t('skills.loadStatsFailed') + ': ' + error.message, 'error');
const statsEl = document.getElementById('skills-stats');
if (statsEl) {
statsEl.innerHTML = '<div class="monitor-error">无法加载统计信息:' + escapeHtml(error.message) + '</div>';
statsEl.innerHTML = '<div class="monitor-error">' + _t('skills.loadStatsErrorShort') + ': ' + escapeHtml(error.message) + '</div>';
}
const monitorListEl = document.getElementById('skills-monitor-list');
if (monitorListEl) {
monitorListEl.innerHTML = '<div class="monitor-error">无法加载调用统计:' + escapeHtml(error.message) + '</div>';
monitorListEl.innerHTML = '<div class="monitor-error">' + _t('skills.loadCallStatsError') + ': ' + escapeHtml(error.message) + '</div>';
}
}
}
@@ -604,23 +621,23 @@ function renderSkillsMonitor() {
statsEl.innerHTML = `
<div class="monitor-stat-card">
<div class="monitor-stat-label">总Skills数</div>
<div class="monitor-stat-label">${_t('skills.totalSkillsCount')}</div>
<div class="monitor-stat-value">${skillsStats.total}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">总调用次数</div>
<div class="monitor-stat-label">${_t('skills.totalCallsCount')}</div>
<div class="monitor-stat-value">${skillsStats.totalCalls}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">成功调用</div>
<div class="monitor-stat-label">${_t('skills.successfulCalls')}</div>
<div class="monitor-stat-value" style="color: #28a745;">${skillsStats.totalSuccess}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">失败调用</div>
<div class="monitor-stat-label">${_t('skills.failedCalls')}</div>
<div class="monitor-stat-value" style="color: #dc3545;">${skillsStats.totalFailed}</div>
</div>
<div class="monitor-stat-card">
<div class="monitor-stat-label">成功率</div>
<div class="monitor-stat-label">${_t('skills.successRate')}</div>
<div class="monitor-stat-value">${successRate}%</div>
</div>
`;
@@ -634,7 +651,7 @@ function renderSkillsMonitor() {
// 如果没有统计数据,显示空状态
if (stats.length === 0) {
monitorListEl.innerHTML = '<div class="monitor-empty">暂无Skills调用记录</div>';
monitorListEl.innerHTML = '<div class="monitor-empty">' + _t('skills.noCallRecords') + '</div>';
return;
}
@@ -652,12 +669,12 @@ function renderSkillsMonitor() {
<table class="monitor-table">
<thead>
<tr>
<th style="text-align: left !important;">Skill名称</th>
<th style="text-align: center;">总调用</th>
<th style="text-align: center;">成功</th>
<th style="text-align: center;">失败</th>
<th style="text-align: center;">成功率</th>
<th style="text-align: left;">最后调用时间</th>
<th style="text-align: left !important;">${_t('skills.skillName')}</th>
<th style="text-align: center;">${_t('skills.totalCalls')}</th>
<th style="text-align: center;">${_t('skills.success')}</th>
<th style="text-align: center;">${_t('skills.failure')}</th>
<th style="text-align: center;">${_t('skills.successRate')}</th>
<th style="text-align: left;">${_t('skills.lastCallTime')}</th>
</tr>
</thead>
<tbody>
@@ -687,12 +704,12 @@ function renderSkillsMonitor() {
// 刷新skills监控
async function refreshSkillsMonitor() {
await loadSkillsMonitor();
showNotification('已刷新', 'success');
showNotification(_t('skills.refreshed'), 'success');
}
// 清空skills统计数据
async function clearSkillsStats() {
if (!confirm('确定要清空所有Skills统计数据吗?此操作不可恢复。')) {
if (!confirm(_t('skills.clearStatsConfirm'))) {
return;
}
@@ -703,15 +720,15 @@ async function clearSkillsStats() {
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '清空统计数据失败');
throw new Error(error.error || _t('skills.clearStatsFailed'));
}
showNotification('已清空所有Skills统计数据', 'success');
showNotification(_t('skills.statsCleared'), 'success');
// 重新加载统计数据
await loadSkillsMonitor();
} catch (error) {
console.error('清空统计数据失败:', error);
showNotification('清空统计数据失败: ' + error.message, 'error');
showNotification(_t('skills.clearStatsFailed') + ': ' + error.message, 'error');
}
}
@@ -722,3 +739,14 @@ function escapeHtml(text) {
div.textContent = text;
return div.innerHTML;
}
// 语言切换时重新渲染当前页(技能列表与分页使用 _t,需随语言更新)
document.addEventListener('languagechange', function () {
const page = document.getElementById('page-skills-management');
if (page && page.classList.contains('active')) {
renderSkillsList();
if (!skillsSearchKeyword) {
renderSkillsPagination();
}
}
});
+134 -131
View File
@@ -1,4 +1,7 @@
// 任务管理页面功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
// HTML转义函数(如果未定义)
if (typeof escapeHtml === 'undefined') {
@@ -106,7 +109,7 @@ async function loadTasks() {
const listContainer = document.getElementById('tasks-list');
if (!listContainer) return;
listContainer.innerHTML = '<div class="loading-spinner">加载中...</div>';
listContainer.innerHTML = '<div class="loading-spinner">' + _t('tasks.loadingTasks') + '</div>';
try {
// 并行加载运行中的任务和已完成的任务历史
@@ -117,7 +120,7 @@ async function loadTasks() {
// 处理运行中的任务
if (activeResponse.status === 'rejected' || !activeResponse.value || !activeResponse.value.ok) {
throw new Error('获取任务列表失败');
throw new Error(_t('tasks.loadTaskListFailed'));
}
const activeResult = await activeResponse.value.json();
@@ -177,8 +180,8 @@ async function loadTasks() {
console.error('加载任务失败:', error);
listContainer.innerHTML = `
<div class="tasks-empty">
<p>加载失败: ${escapeHtml(error.message)}</p>
<button class="btn-secondary" onclick="loadTasks()">重试</button>
<p>${_t('tasks.loadFailedRetry')}: ${escapeHtml(error.message)}</p>
<button class="btn-secondary" onclick="loadTasks()">${_t('tasks.retry')}</button>
</div>
`;
}
@@ -296,21 +299,21 @@ function toggleShowHistory(show) {
// 计算执行时长
function calculateDuration(startedAt) {
if (!startedAt) return '未知';
if (!startedAt) return _t('tasks.unknown');
const start = new Date(startedAt);
const now = new Date();
const diff = Math.floor((now - start) / 1000); // 秒
const diff = Math.floor((now - start) / 1000);
if (diff < 60) {
return `${diff}`;
return diff + _t('tasks.durationSeconds');
} else if (diff < 3600) {
const minutes = Math.floor(diff / 60);
const seconds = diff % 60;
return `${minutes}${seconds}`;
return minutes + _t('tasks.durationMinutes') + ' ' + seconds + _t('tasks.durationSeconds');
} else {
const hours = Math.floor(diff / 3600);
const minutes = Math.floor((diff % 3600) / 60);
return `${hours}小时${minutes}`;
return hours + _t('tasks.durationHours') + ' ' + minutes + _t('tasks.durationMinutes');
}
}
@@ -349,9 +352,9 @@ function renderTasks(tasks) {
if (tasks.length === 0) {
listContainer.innerHTML = `
<div class="tasks-empty">
<p>当前没有符合条件的任务</p>
<p>${_t('tasks.noMatchingTasks')}</p>
${tasksState.allTasks.length === 0 && tasksState.completedTasksHistory.length > 0 ?
'<p style="margin-top: 8px; color: var(--text-muted); font-size: 0.875rem;">提示:有已完成的任务历史,请勾选"显示历史记录"查看</p>' : ''}
'<p style="margin-top: 8px; color: var(--text-muted); font-size: 0.875rem;">' + _t('tasks.historyHint') + '</p>' : ''}
</div>
`;
return;
@@ -359,12 +362,12 @@ function renderTasks(tasks) {
// 状态映射
const statusMap = {
'running': { text: '执行中', class: 'task-status-running' },
'cancelling': { text: '取消中', class: 'task-status-cancelling' },
'failed': { text: '执行失败', class: 'task-status-failed' },
'timeout': { text: '执行超时', class: 'task-status-timeout' },
'cancelled': { text: '已取消', class: 'task-status-cancelled' },
'completed': { text: '已完成', class: 'task-status-completed' }
'running': { text: _t('tasks.statusRunning'), class: 'task-status-running' },
'cancelling': { text: _t('tasks.statusCancelling'), class: 'task-status-cancelling' },
'failed': { text: _t('tasks.statusFailed'), class: 'task-status-failed' },
'timeout': { text: _t('tasks.statusTimeout'), class: 'task-status-timeout' },
'cancelled': { text: _t('tasks.statusCancelled'), class: 'task-status-cancelled' },
'completed': { text: _t('tasks.statusCompleted'), class: 'task-status-completed' }
};
// 分离当前任务和历史任务
@@ -382,8 +385,8 @@ function renderTasks(tasks) {
if (historyTasks.length > 0) {
html += `<div class="tasks-history-section">
<div class="tasks-history-header">
<span class="tasks-history-title">📜 最近完成的任务最近24小时</span>
<button class="btn-secondary btn-small" onclick="clearTasksHistory()">清空历史</button>
<span class="tasks-history-title">📜 ` + _t('tasks.recentCompletedTasks') + `</span>
<button class="btn-secondary btn-small" onclick="clearTasksHistory()">` + _t('tasks.clearHistory') + `</button>
</div>
${historyTasks.map(task => renderTaskItem(task, statusMap, true)).join('')}
</div>`;
@@ -406,7 +409,7 @@ function renderTaskItem(task, statusMap, isHistory = false) {
minute: '2-digit',
second: '2-digit'
})
: '未知时间';
: _t('tasks.unknownTime');
const completedText = completedTime && !isNaN(completedTime.getTime())
? completedTime.toLocaleString('zh-CN', {
@@ -438,22 +441,22 @@ function renderTaskItem(task, statusMap, isHistory = false) {
</label>
` : '<div class="task-checkbox-placeholder"></div>'}
<span class="task-status ${status.class}">${status.text}</span>
${isHistory ? '<span class="task-history-badge" title="历史记录">📜</span>' : ''}
<span class="task-message" title="${escapeHtml(task.message || '未命名任务')}">${escapeHtml(task.message || '未命名任务')}</span>
${isHistory ? '<span class="task-history-badge" title="' + _t('tasks.historyBadge') + '">📜</span>' : ''}
<span class="task-message" title="${escapeHtml(task.message || _t('tasks.unnamedTask'))}">${escapeHtml(task.message || _t('tasks.unnamedTask'))}</span>
</div>
<div class="task-actions">
${duration ? `<span class="task-duration" title="执行时长">⏱ ${duration}</span>` : ''}
<span class="task-time" title="${isHistory && completedText ? '完成时间' : '开始时间'}">
${duration ? `<span class="task-duration" title="${_t('tasks.duration')}">⏱ ${duration}</span>` : ''}
<span class="task-time" title="${isHistory && completedText ? _t('tasks.completedAt') : _t('tasks.startedAt')}">
${isHistory && completedText ? completedText : timeText}
</span>
${canCancel ? `<button class="btn-secondary btn-small" onclick="cancelTask('${task.conversationId}', this)">取消任务</button>` : ''}
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation('${task.conversationId}')">查看对话</button>` : ''}
${canCancel ? `<button class="btn-secondary btn-small" onclick="cancelTask('${task.conversationId}', this)">` + _t('tasks.cancelTask') + `</button>` : ''}
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation('${task.conversationId}')">` + _t('tasks.viewConversation') + `</button>` : ''}
</div>
</div>
${task.conversationId ? `
<div class="task-details">
<span class="task-id-label">对话ID:</span>
<span class="task-id-value" title="点击复制" onclick="copyTaskId('${task.conversationId}')">${escapeHtml(task.conversationId)}</span>
<span class="task-id-label">` + _t('tasks.conversationIdLabel') + `:</span>
<span class="task-id-value" title="` + _t('tasks.clickToCopy') + `" onclick="copyTaskId('${task.conversationId}')">${escapeHtml(task.conversationId)}</span>
</div>
` : ''}
</div>
@@ -462,7 +465,7 @@ function renderTaskItem(task, statusMap, isHistory = false) {
// 清空任务历史
function clearTasksHistory() {
if (!confirm('确定要清空所有任务历史记录吗?')) {
if (!confirm(_t('tasks.clearHistoryConfirm'))) {
return;
}
tasksState.completedTasksHistory = [];
@@ -490,7 +493,7 @@ function updateBatchActions() {
const count = tasksState.selectedTasks.size;
if (count > 0) {
batchActions.style.display = 'flex';
selectedCount.textContent = `已选择 ${count}`;
selectedCount.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: count }) : `已选择 ${count}`;
} else {
batchActions.style.display = 'none';
}
@@ -509,7 +512,7 @@ async function batchCancelTasks() {
const selected = Array.from(tasksState.selectedTasks);
if (selected.length === 0) return;
if (!confirm(`确定要取消 ${selected.length} 个任务吗?`)) {
if (!confirm(_t('tasks.confirmCancelTasks', { n: selected.length }))) {
return;
}
@@ -545,9 +548,9 @@ async function batchCancelTasks() {
// 显示结果
if (failCount > 0) {
alert(`批量取消完成:成功 ${successCount} 个,失败 ${failCount}`);
alert(_t('tasks.batchCancelResultPartial', { success: successCount, fail: failCount }));
} else {
alert(`成功取消 ${successCount} 个任务`);
alert(_t('tasks.batchCancelResultSuccess', { n: successCount }));
}
}
@@ -556,7 +559,7 @@ function copyTaskId(conversationId) {
navigator.clipboard.writeText(conversationId).then(() => {
// 显示复制成功提示
const tooltip = document.createElement('div');
tooltip.textContent = '已复制!';
tooltip.textContent = _t('tasks.copiedToast');
tooltip.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: white; padding: 8px 16px; border-radius: 4px; z-index: 10000;';
document.body.appendChild(tooltip);
setTimeout(() => tooltip.remove(), 1000);
@@ -571,7 +574,7 @@ async function cancelTask(conversationId, button) {
const originalText = button.textContent;
button.disabled = true;
button.textContent = '取消中...';
button.textContent = _t('tasks.cancelling');
try {
const response = await apiFetch('/api/agent-loop/cancel', {
@@ -584,7 +587,7 @@ async function cancelTask(conversationId, button) {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '取消任务失败');
throw new Error(result.error || _t('tasks.cancelTaskFailed'));
}
// 从选择中移除
@@ -595,7 +598,7 @@ async function cancelTask(conversationId, button) {
await loadTasks();
} catch (error) {
console.error('取消任务失败:', error);
alert('取消任务失败: ' + error.message);
alert(_t('tasks.cancelTaskFailed') + ': ' + error.message);
button.disabled = false;
button.textContent = originalText;
}
@@ -738,7 +741,7 @@ async function showBatchImportModal() {
try {
const loadedRoles = await loadRoles();
// 清空现有选项(除了默认选项)
roleSelect.innerHTML = '<option value="">默认</option>';
roleSelect.innerHTML = '<option value="">' + _t('batchImportModal.defaultRole') + '</option>';
// 添加已启用的角色
const sortedRoles = loadedRoles.sort((a, b) => {
@@ -782,7 +785,7 @@ function updateBatchImportStats(text) {
const count = lines.length;
if (count > 0) {
statsEl.innerHTML = `<div class="batch-import-stat">${count} 个任务</div>`;
statsEl.innerHTML = '<div class="batch-import-stat">' + _t('tasks.taskCount', { count: count }) + '</div>';
statsEl.style.display = 'block';
} else {
statsEl.style.display = 'none';
@@ -808,14 +811,14 @@ async function createBatchQueue() {
const text = input.value.trim();
if (!text) {
alert('请输入至少一个任务');
alert(_t('tasks.enterTaskPrompt'));
return;
}
// 按行分割任务
const tasks = text.split('\n').map(line => line.trim()).filter(line => line !== '');
if (tasks.length === 0) {
alert('没有有效的任务');
alert(_t('tasks.noValidTask'));
return;
}
@@ -836,7 +839,7 @@ async function createBatchQueue() {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '创建批量任务队列失败');
throw new Error(result.error || _t('tasks.createBatchQueueFailed'));
}
const result = await response.json();
@@ -849,7 +852,7 @@ async function createBatchQueue() {
refreshBatchQueues();
} catch (error) {
console.error('创建批量任务队列失败:', error);
alert('创建批量任务队列失败: ' + error.message);
alert(_t('tasks.createBatchQueueFailed') + ': ' + error.message);
}
}
@@ -916,7 +919,7 @@ async function loadBatchQueues(page) {
try {
const response = await apiFetch(`/api/batch-tasks?${params.toString()}`);
if (!response.ok) {
throw new Error('获取批量任务队列失败');
throw new Error(_t('tasks.loadFailedRetry'));
}
const result = await response.json();
@@ -929,7 +932,7 @@ async function loadBatchQueues(page) {
section.style.display = 'block';
const list = document.getElementById('batch-queues-list');
if (list) {
list.innerHTML = '<div class="tasks-empty"><p>加载失败: ' + escapeHtml(error.message) + '</p><button class="btn-secondary" onclick="refreshBatchQueues()">重试</button></div>';
list.innerHTML = '<div class="tasks-empty"><p>' + _t('tasks.loadFailedRetry') + ': ' + escapeHtml(error.message) + '</p><button class="btn-secondary" onclick="refreshBatchQueues()">' + _t('tasks.retry') + '</button></div>';
}
}
}
@@ -964,7 +967,7 @@ function renderBatchQueues() {
const queues = batchQueuesState.queues;
if (queues.length === 0) {
list.innerHTML = '<div class="tasks-empty"><p>当前没有批量任务队列</p></div>';
list.innerHTML = '<div class="tasks-empty"><p>' + _t('tasks.noBatchQueues') + '</p></div>';
if (pagination) pagination.style.display = 'none';
return;
}
@@ -976,11 +979,11 @@ function renderBatchQueues() {
list.innerHTML = queues.map(queue => {
const statusMap = {
'pending': { text: '待执行', class: 'batch-queue-status-pending' },
'running': { text: '执行中', class: 'batch-queue-status-running' },
'paused': { text: '已暂停', class: 'batch-queue-status-paused' },
'completed': { text: '已完成', class: 'batch-queue-status-completed' },
'cancelled': { text: '已取消', class: 'batch-queue-status-cancelled' }
'pending': { text: _t('tasks.statusPending'), class: 'batch-queue-status-pending' },
'running': { text: _t('tasks.statusRunning'), class: 'batch-queue-status-running' },
'paused': { text: _t('tasks.statusPaused'), class: 'batch-queue-status-paused' },
'completed': { text: _t('tasks.statusCompleted'), class: 'batch-queue-status-completed' },
'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-queue-status-cancelled' }
};
const status = statusMap[queue.status] || { text: queue.status, class: 'batch-queue-status-unknown' };
@@ -1012,8 +1015,8 @@ function renderBatchQueues() {
// 显示角色信息(使用正确的角色图标)
const loadedRoles = batchQueuesState.loadedRoles || [];
const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles);
const roleName = queue.role && queue.role !== '' ? queue.role : '默认';
const roleDisplay = `<span class="batch-queue-role" style="margin-right: 8px;" title="角色: ${escapeHtml(roleName)}">${roleIcon} ${escapeHtml(roleName)}</span>`;
const roleName = queue.role && queue.role !== '' ? queue.role : _t('batchQueueDetailModal.defaultRole');
const roleDisplay = `<span class="batch-queue-role" style="margin-right: 8px;" title="${_t('batchQueueDetailModal.role')}: ${escapeHtml(roleName)}">${roleIcon} ${escapeHtml(roleName)}</span>`;
return `
<div class="batch-queue-item" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
@@ -1022,8 +1025,8 @@ function renderBatchQueues() {
${titleDisplay}
${roleDisplay}
<span class="batch-queue-status ${status.class}">${status.text}</span>
<span class="batch-queue-id">队列ID: ${escapeHtml(queue.id)}</span>
<span class="batch-queue-time">创建时间: ${new Date(queue.createdAt).toLocaleString('zh-CN')}</span>
<span class="batch-queue-id">${_t('tasks.queueIdLabel')}: ${escapeHtml(queue.id)}</span>
<span class="batch-queue-time">${_t('tasks.createdTimeLabel')}: ${new Date(queue.createdAt).toLocaleString()}</span>
</div>
<div class="batch-queue-progress">
<div class="batch-queue-progress-bar">
@@ -1032,16 +1035,16 @@ function renderBatchQueues() {
<span class="batch-queue-progress-text">${progress}% (${stats.completed + stats.failed + stats.cancelled}/${stats.total})</span>
</div>
<div class="batch-queue-actions" style="display: flex; align-items: center; gap: 8px; margin-left: 12px;" onclick="event.stopPropagation();">
${canDelete ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteBatchQueueFromList('${queue.id}')" title="删除队列">删除</button>` : ''}
${canDelete ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteBatchQueueFromList('${queue.id}')" title="${_t('tasks.deleteQueue')}">${_t('common.delete')}</button>` : ''}
</div>
</div>
<div class="batch-queue-stats">
<span>总计: ${stats.total}</span>
<span>待执行: ${stats.pending}</span>
<span>执行中: ${stats.running}</span>
<span style="color: var(--success-color);">已完成: ${stats.completed}</span>
<span style="color: var(--error-color);">失败: ${stats.failed}</span>
${stats.cancelled > 0 ? `<span style="color: var(--text-secondary);">已取消: ${stats.cancelled}</span>` : ''}
<span>${_t('tasks.totalLabel')}: ${stats.total}</span>
<span>${_t('tasks.pendingLabel')}: ${stats.pending}</span>
<span>${_t('tasks.runningLabel')}: ${stats.running}</span>
<span style="color: var(--success-color);">${_t('tasks.completedLabel')}: ${stats.completed}</span>
<span style="color: var(--error-color);">${_t('tasks.failedLabel')}: ${stats.failed}</span>
${stats.cancelled > 0 ? `<span style="color: var(--text-secondary);">${_t('tasks.cancelledLabel')}: ${stats.cancelled}</span>` : ''}
</div>
</div>
`;
@@ -1073,9 +1076,9 @@ function renderBatchQueuesPagination() {
// 左侧:显示范围信息和每页数量选择器(参考Skills样式)
paginationHTML += `
<div class="pagination-info">
<span>显示 ${start}-${end} / ${total} </span>
<span>` + _t('tasks.paginationShow', { start: start, end: end, total: total }) + `</span>
<label class="pagination-page-size">
每页显示
` + _t('tasks.paginationPerPage') + `
<select id="batch-queues-page-size-pagination" onchange="changeBatchQueuesPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
@@ -1089,11 +1092,11 @@ function renderBatchQueuesPagination() {
// 右侧:分页按钮(参考Skills样式:首页、上一页、第X/Y页、下一页、末页)
paginationHTML += `
<div class="pagination-controls">
<button class="btn-secondary" onclick="goBatchQueuesPage(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
<span class="pagination-page"> ${currentPage} / ${totalPages || 1} </span>
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
<button class="btn-secondary" onclick="goBatchQueuesPage(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
<button class="btn-secondary" onclick="goBatchQueuesPage(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationFirst') + `</button>
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationPrev') + `</button>
<span class="pagination-page">` + _t('tasks.paginationPage', { current: currentPage, total: totalPages || 1 }) + `</span>
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationNext') + `</button>
<button class="btn-secondary" onclick="goBatchQueuesPage(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationLast') + `</button>
</div>
`;
@@ -1189,7 +1192,7 @@ async function showBatchQueueDetail(queueId) {
const response = await apiFetch(`/api/batch-tasks/${queueId}`);
if (!response.ok) {
throw new Error('获取队列详情失败');
throw new Error(_t('tasks.getQueueDetailFailed'));
}
const result = await response.json();
@@ -1198,7 +1201,7 @@ async function showBatchQueueDetail(queueId) {
if (title) {
// textContent 本身会做转义;这里不要再 escapeHtml,否则会把 && 显示成 &amp;...(看起来像“变形/乱码”)
title.textContent = queue.title ? `批量任务队列 - ${String(queue.title)}` : '批量任务队列';
title.textContent = queue.title ? _t('tasks.batchQueueTitle') + ' - ' + String(queue.title) : _t('tasks.batchQueueTitle');
}
// 更新按钮显示
@@ -1210,9 +1213,9 @@ async function showBatchQueueDetail(queueId) {
// pending状态显示"开始执行"paused状态显示"继续执行"
startBtn.style.display = (queue.status === 'pending' || queue.status === 'paused') ? 'inline-block' : 'none';
if (startBtn && queue.status === 'paused') {
startBtn.textContent = '继续执行';
startBtn.textContent = _t('tasks.resumeExecute');
} else if (startBtn && queue.status === 'pending') {
startBtn.textContent = '开始执行';
startBtn.textContent = _t('batchQueueDetailModal.startExecute');
}
}
if (pauseBtn) {
@@ -1226,20 +1229,20 @@ async function showBatchQueueDetail(queueId) {
// 队列状态映射
const queueStatusMap = {
'pending': { text: '待执行', class: 'batch-queue-status-pending' },
'running': { text: '执行中', class: 'batch-queue-status-running' },
'paused': { text: '已暂停', class: 'batch-queue-status-paused' },
'completed': { text: '已完成', class: 'batch-queue-status-completed' },
'cancelled': { text: '已取消', class: 'batch-queue-status-cancelled' }
'pending': { text: _t('tasks.statusPending'), class: 'batch-queue-status-pending' },
'running': { text: _t('tasks.statusRunning'), class: 'batch-queue-status-running' },
'paused': { text: _t('tasks.statusPaused'), class: 'batch-queue-status-paused' },
'completed': { text: _t('tasks.statusCompleted'), class: 'batch-queue-status-completed' },
'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-queue-status-cancelled' }
};
// 任务状态映射
const taskStatusMap = {
'pending': { text: '待执行', class: 'batch-task-status-pending' },
'running': { text: '执行中', class: 'batch-task-status-running' },
'completed': { text: '已完成', class: 'batch-task-status-completed' },
'failed': { text: '失败', class: 'batch-task-status-failed' },
'cancelled': { text: '已取消', class: 'batch-task-status-cancelled' }
'pending': { text: _t('tasks.statusPending'), class: 'batch-task-status-pending' },
'running': { text: _t('tasks.statusRunning'), class: 'batch-task-status-running' },
'completed': { text: _t('tasks.statusCompleted'), class: 'batch-task-status-completed' },
'failed': { text: _t('tasks.failedLabel'), class: 'batch-task-status-failed' },
'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-task-status-cancelled' }
};
// 获取角色信息(如果队列有角色配置)
@@ -1266,51 +1269,51 @@ async function showBatchQueueDetail(queueId) {
}
}
roleDisplay = `<div class="detail-item">
<span class="detail-label">角色</span>
<span class="detail-label">` + _t('batchQueueDetailModal.role') + `</span>
<span class="detail-value">${roleIcon} ${escapeHtml(roleName)}</span>
</div>`;
} else {
// 默认角色
roleDisplay = `<div class="detail-item">
<span class="detail-label">角色</span>
<span class="detail-value">🔵 默认</span>
<span class="detail-label">` + _t('batchQueueDetailModal.role') + `</span>
<span class="detail-value">🔵 ` + _t('batchQueueDetailModal.defaultRole') + `</span>
</div>`;
}
content.innerHTML = `
<div class="batch-queue-detail-info">
${queue.title ? `<div class="detail-item">
<span class="detail-label">任务标题</span>
<span class="detail-label">` + _t('batchQueueDetailModal.queueTitle') + `</span>
<span class="detail-value">${escapeHtml(queue.title)}</span>
</div>` : ''}
${roleDisplay}
<div class="detail-item">
<span class="detail-label">队列ID</span>
<span class="detail-label">` + _t('batchQueueDetailModal.queueId') + `</span>
<span class="detail-value"><code>${escapeHtml(queue.id)}</code></span>
</div>
<div class="detail-item">
<span class="detail-label">状态</span>
<span class="detail-label">` + _t('batchQueueDetailModal.status') + `</span>
<span class="detail-value"><span class="batch-queue-status ${queueStatusMap[queue.status]?.class || ''}">${queueStatusMap[queue.status]?.text || queue.status}</span></span>
</div>
<div class="detail-item">
<span class="detail-label">创建时间</span>
<span class="detail-value">${new Date(queue.createdAt).toLocaleString('zh-CN')}</span>
<span class="detail-label">` + _t('batchQueueDetailModal.createdAt') + `</span>
<span class="detail-value">${new Date(queue.createdAt).toLocaleString()}</span>
</div>
${queue.startedAt ? `<div class="detail-item">
<span class="detail-label">开始时间</span>
<span class="detail-value">${new Date(queue.startedAt).toLocaleString('zh-CN')}</span>
<span class="detail-label">` + _t('batchQueueDetailModal.startedAt') + `</span>
<span class="detail-value">${new Date(queue.startedAt).toLocaleString()}</span>
</div>` : ''}
${queue.completedAt ? `<div class="detail-item">
<span class="detail-label">完成时间</span>
<span class="detail-value">${new Date(queue.completedAt).toLocaleString('zh-CN')}</span>
<span class="detail-label">` + _t('batchQueueDetailModal.completedAt') + `</span>
<span class="detail-value">${new Date(queue.completedAt).toLocaleString()}</span>
</div>` : ''}
<div class="detail-item">
<span class="detail-label">任务总数</span>
<span class="detail-label">` + _t('batchQueueDetailModal.taskTotal') + `</span>
<span class="detail-value">${queue.tasks.length}</span>
</div>
</div>
<div class="batch-queue-tasks-list">
<h4>任务列表</h4>
<h4>` + _t('batchQueueDetailModal.taskList') + `</h4>
${queue.tasks.map((task, index) => {
const taskStatus = taskStatusMap[task.status] || { text: task.status, class: 'batch-task-status-unknown' };
const canEdit = queue.status === 'pending' && task.status === 'pending';
@@ -1321,14 +1324,14 @@ async function showBatchQueueDetail(queueId) {
<span class="batch-task-index">#${index + 1}</span>
<span class="batch-task-status ${taskStatus.class}">${taskStatus.text}</span>
<span class="batch-task-message" title="${escapeHtml(task.message)}">${escapeHtml(task.message)}</span>
${canEdit ? `<button class="btn-secondary btn-small batch-task-edit-btn" onclick="editBatchTaskFromElement(this); event.stopPropagation();">编辑</button>` : ''}
${canEdit ? `<button class="btn-secondary btn-small btn-danger batch-task-delete-btn" onclick="deleteBatchTaskFromElement(this); event.stopPropagation();">删除</button>` : ''}
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation('${task.conversationId}'); event.stopPropagation();">查看对话</button>` : ''}
${canEdit ? `<button class="btn-secondary btn-small batch-task-edit-btn" onclick="editBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.edit') + `</button>` : ''}
${canEdit ? `<button class="btn-secondary btn-small btn-danger batch-task-delete-btn" onclick="deleteBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.delete') + `</button>` : ''}
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation('${task.conversationId}'); event.stopPropagation();">` + _t('tasks.viewConversation') + `</button>` : ''}
</div>
${task.startedAt ? `<div class="batch-task-time">开始: ${new Date(task.startedAt).toLocaleString('zh-CN')}</div>` : ''}
${task.completedAt ? `<div class="batch-task-time">完成: ${new Date(task.completedAt).toLocaleString('zh-CN')}</div>` : ''}
${task.error ? `<div class="batch-task-error">错误: ${escapeHtml(task.error)}</div>` : ''}
${task.result ? `<div class="batch-task-result">结果: ${escapeHtml(task.result.substring(0, 200))}${task.result.length > 200 ? '...' : ''}</div>` : ''}
${task.startedAt ? `<div class="batch-task-time">` + _t('batchQueueDetailModal.startLabel') + `: ${new Date(task.startedAt).toLocaleString()}</div>` : ''}
${task.completedAt ? `<div class="batch-task-time">` + _t('batchQueueDetailModal.completeLabel') + `: ${new Date(task.completedAt).toLocaleString()}</div>` : ''}
${task.error ? `<div class="batch-task-error">` + _t('batchQueueDetailModal.errorLabel') + `: ${escapeHtml(task.error)}</div>` : ''}
${task.result ? `<div class="batch-task-result">` + _t('batchQueueDetailModal.resultLabel') + `: ${escapeHtml(task.result.substring(0, 200))}${task.result.length > 200 ? '...' : ''}</div>` : ''}
</div>
`;
}).join('')}
@@ -1343,7 +1346,7 @@ async function showBatchQueueDetail(queueId) {
}
} catch (error) {
console.error('获取队列详情失败:', error);
alert('获取队列详情失败: ' + error.message);
alert(_t('tasks.getQueueDetailFailed') + ': ' + error.message);
}
}
@@ -1359,7 +1362,7 @@ async function startBatchQueue() {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '启动批量任务失败');
throw new Error(result.error || _t('tasks.startBatchQueueFailed'));
}
// 刷新详情
@@ -1367,7 +1370,7 @@ async function startBatchQueue() {
refreshBatchQueues();
} catch (error) {
console.error('启动批量任务失败:', error);
alert('启动批量任务失败: ' + error.message);
alert(_t('tasks.startBatchQueueFailed') + ': ' + error.message);
}
}
@@ -1376,7 +1379,7 @@ async function pauseBatchQueue() {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) return;
if (!confirm('确定要暂停这个批量任务队列吗?当前正在执行的任务将被停止,后续任务将保留待执行状态。')) {
if (!confirm(_t('tasks.pauseQueueConfirm'))) {
return;
}
@@ -1387,7 +1390,7 @@ async function pauseBatchQueue() {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '暂停批量任务失败');
throw new Error(result.error || _t('tasks.pauseQueueFailed'));
}
// 刷新详情
@@ -1395,7 +1398,7 @@ async function pauseBatchQueue() {
refreshBatchQueues();
} catch (error) {
console.error('暂停批量任务失败:', error);
alert('暂停批量任务失败: ' + error.message);
alert(_t('tasks.pauseQueueFailed') + ': ' + error.message);
}
}
@@ -1404,7 +1407,7 @@ async function deleteBatchQueue() {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) return;
if (!confirm('确定要删除这个批量任务队列吗?此操作不可恢复。')) {
if (!confirm(_t('tasks.deleteQueueConfirm'))) {
return;
}
@@ -1415,14 +1418,14 @@ async function deleteBatchQueue() {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '删除批量任务队列失败');
throw new Error(result.error || _t('tasks.deleteQueueFailed'));
}
closeBatchQueueDetailModal();
refreshBatchQueues();
} catch (error) {
console.error('删除批量任务队列失败:', error);
alert('删除批量任务队列失败: ' + error.message);
alert(_t('tasks.deleteQueueFailed') + ': ' + error.message);
}
}
@@ -1430,7 +1433,7 @@ async function deleteBatchQueue() {
async function deleteBatchQueueFromList(queueId) {
if (!queueId) return;
if (!confirm('确定要删除这个批量任务队列吗?此操作不可恢复。')) {
if (!confirm(_t('tasks.deleteQueueConfirm'))) {
return;
}
@@ -1441,7 +1444,7 @@ async function deleteBatchQueueFromList(queueId) {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '删除批量任务队列失败');
throw new Error(result.error || _t('tasks.deleteQueueFailed'));
}
// 如果当前正在查看这个队列的详情,关闭详情模态框
@@ -1453,7 +1456,7 @@ async function deleteBatchQueueFromList(queueId) {
refreshBatchQueues();
} catch (error) {
console.error('删除批量任务队列失败:', error);
alert('删除批量任务队列失败: ' + error.message);
alert(_t('tasks.deleteQueueFailed') + ': ' + error.message);
}
}
@@ -1599,18 +1602,18 @@ async function saveBatchTask() {
const messageInput = document.getElementById('edit-task-message');
if (!queueId || !taskId) {
alert('任务信息不完整');
alert(_t('tasks.taskIncomplete'));
return;
}
if (!messageInput) {
alert('无法获取任务消息输入框');
alert(_t('tasks.cannotGetTaskMessageInput'));
return;
}
const message = messageInput.value.trim();
if (!message) {
alert('任务消息不能为空');
alert(_t('tasks.taskMessageRequired'));
return;
}
@@ -1625,7 +1628,7 @@ async function saveBatchTask() {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '更新任务失败');
throw new Error(result.error || _t('tasks.updateTaskFailed'));
}
// 关闭编辑模态框
@@ -1640,7 +1643,7 @@ async function saveBatchTask() {
refreshBatchQueues();
} catch (error) {
console.error('保存任务失败:', error);
alert('保存任务失败: ' + error.message);
alert(_t('tasks.saveTaskFailed') + ': ' + error.message);
}
}
@@ -1648,7 +1651,7 @@ async function saveBatchTask() {
function showAddBatchTaskModal() {
const queueId = batchQueuesState.currentQueueId;
if (!queueId) {
alert('队列信息不存在');
alert(_t('tasks.queueInfoMissing'));
return;
}
@@ -1706,18 +1709,18 @@ async function saveAddBatchTask() {
const messageInput = document.getElementById('add-task-message');
if (!queueId) {
alert('队列信息不存在');
alert(_t('tasks.queueInfoMissing'));
return;
}
if (!messageInput) {
alert('无法获取任务消息输入框');
alert(_t('tasks.cannotGetTaskMessageInput'));
return;
}
const message = messageInput.value.trim();
if (!message) {
alert('任务消息不能为空');
alert(_t('tasks.taskMessageRequired'));
return;
}
@@ -1732,7 +1735,7 @@ async function saveAddBatchTask() {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '添加任务失败');
throw new Error(result.error || _t('tasks.addTaskFailed'));
}
// 关闭添加任务模态框
@@ -1747,7 +1750,7 @@ async function saveAddBatchTask() {
refreshBatchQueues();
} catch (error) {
console.error('添加任务失败:', error);
alert('添加任务失败: ' + error.message);
alert(_t('tasks.addTaskFailed') + ': ' + error.message);
}
}
@@ -1779,7 +1782,7 @@ function deleteBatchTaskFromElement(button) {
? decodedMessage.substring(0, 50) + '...'
: decodedMessage;
if (!confirm(`确定要删除这个任务吗?\n\n任务内容: ${displayMessage}\n\n此操作不可恢复。`)) {
if (!confirm(_t('tasks.confirmDeleteTask', { message: displayMessage }))) {
return;
}
@@ -1789,7 +1792,7 @@ function deleteBatchTaskFromElement(button) {
// 删除批量任务
async function deleteBatchTask(queueId, taskId) {
if (!queueId || !taskId) {
alert('任务信息不完整');
alert(_t('tasks.taskIncomplete'));
return;
}
@@ -1800,7 +1803,7 @@ async function deleteBatchTask(queueId, taskId) {
if (!response.ok) {
const result = await response.json().catch(() => ({}));
throw new Error(result.error || '删除任务失败');
throw new Error(result.error || _t('tasks.deleteTaskFailed'));
}
// 刷新队列详情
@@ -1812,7 +1815,7 @@ async function deleteBatchTask(queueId, taskId) {
refreshBatchQueues();
} catch (error) {
console.error('删除任务失败:', error);
alert('删除任务失败: ' + error.message);
alert(_t('tasks.deleteTaskFailed') + ': ' + error.message);
}
}
+11 -5
View File
@@ -156,14 +156,20 @@ async function loadVulnerabilities(page = null) {
function renderVulnerabilities(vulnerabilities) {
const listContainer = document.getElementById('vulnerabilities-list');
// 处理空值情况
// 处理空值情况(使用 data-i18n 以便语言切换时自动更新)
if (!vulnerabilities || !Array.isArray(vulnerabilities)) {
listContainer.innerHTML = '<div class="empty-state">暂无漏洞记录</div>';
listContainer.innerHTML = '<div class="empty-state" data-i18n="vulnerabilityPage.noRecords">暂无漏洞记录</div>';
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(listContainer);
}
return;
}
if (vulnerabilities.length === 0) {
listContainer.innerHTML = '<div class="empty-state">暂无漏洞记录</div>';
listContainer.innerHTML = '<div class="empty-state" data-i18n="vulnerabilityPage.noRecords">暂无漏洞记录</div>';
if (typeof window.applyTranslations === 'function') {
window.applyTranslations(listContainer);
}
// 清空分页信息
const paginationContainer = document.getElementById('vulnerability-pagination');
if (paginationContainer) {
@@ -328,7 +334,7 @@ async function changeVulnerabilityPageSize() {
// 显示添加漏洞模态框
function showAddVulnerabilityModal() {
currentVulnerabilityId = null;
document.getElementById('vulnerability-modal-title').textContent = '添加漏洞';
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.addVuln') : '添加漏洞');
// 清空表单
document.getElementById('vulnerability-conversation-id').value = '';
@@ -353,7 +359,7 @@ async function editVulnerability(id) {
const vuln = await response.json();
currentVulnerabilityId = id;
document.getElementById('vulnerability-modal-title').textContent = '编辑漏洞';
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.editVuln') : '编辑漏洞');
// 填充表单
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
+30 -15
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API 文档 - CyberStrikeAI</title>
<title data-i18n="apiDocs.pageTitle">API 文档 - CyberStrikeAI</title>
<link rel="icon" type="image/png" href="/static/logo.png">
<link rel="stylesheet" href="/static/css/style.css">
<style>
@@ -22,6 +22,7 @@
}
.api-docs-header {
position: relative;
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 2px solid var(--border-color);
@@ -833,9 +834,21 @@
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
API 文档
<span data-i18n="apiDocs.title">API 文档</span>
</h1>
<p>CyberStrikeAI 平台 API 接口文档,支持在线测试</p>
<p data-i18n="apiDocs.subtitle">CyberStrikeAI 平台 API 接口文档,支持在线测试</p>
<div class="api-docs-lang-switcher" style="position: absolute; top: 24px; right: 24px;">
<div class="lang-switcher">
<button type="button" class="btn-secondary lang-switcher-btn" onclick="typeof toggleLangDropdown === 'function' && toggleLangDropdown()" title="界面语言">
<span class="lang-switcher-icon">🌐</span>
<span id="current-lang-label">中文</span>
</button>
<div id="lang-dropdown" class="lang-dropdown" style="display: none;">
<div class="lang-option" data-lang="zh-CN" onclick="typeof onLanguageSelect === 'function' && onLanguageSelect('zh-CN')">中文</div>
<div class="lang-option" data-lang="en-US" onclick="typeof onLanguageSelect === 'function' && onLanguageSelect('en-US')">English</div>
</div>
</div>
</div>
</div>
<div id="auth-info-section" class="auth-info-section" style="display: none;">
@@ -846,17 +859,17 @@
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
</svg>
<h3 style="margin: 0; font-size: 1rem; font-weight: 600; color: var(--text-primary);">API 认证说明</h3>
<h3 style="margin: 0; font-size: 1rem; font-weight: 600; color: var(--text-primary);" data-i18n="apiDocs.authTitle">API 认证说明</h3>
</div>
<svg id="auth-info-arrow" class="auth-info-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="transition: transform 0.2s ease;">
<polyline points="6 9 12 15 18 9"/>
</svg>
</div>
<div id="auth-info-body" class="auth-info-body" style="display: none; color: var(--text-secondary); font-size: 0.875rem; line-height: 1.6; margin-top: 16px;">
<p style="margin: 0 0 12px 0;"><strong>所有 API 接口都需要 Token 认证。</strong></p>
<p style="margin: 0 0 12px 0;"><strong data-i18n="apiDocs.authAllNeedToken">所有 API 接口都需要 Token 认证。</strong></p>
<div style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin-bottom: 12px;">
<p style="margin: 0 0 8px 0; font-weight: 500;">1. 获取 Token</p>
<p style="margin: 0 0 8px 0;">在前端页面登录后,Token 会自动保存。您也可以通过以下方式获取:</p>
<p style="margin: 0 0 8px 0; font-weight: 500;" data-i18n="apiDocs.authGetToken">1. 获取 Token</p>
<p style="margin: 0 0 8px 0;" data-i18n="apiDocs.authGetTokenDesc">在前端页面登录后,Token 会自动保存。您也可以通过以下方式获取:</p>
<pre style="background: var(--bg-primary); padding: 8px; border-radius: 4px; margin: 8px 0; overflow-x: auto; font-size: 0.8125rem;"><code>POST /api/auth/login
Content-Type: application/json
@@ -871,13 +884,13 @@ Content-Type: application/json
}</code></pre>
</div>
<div style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin-bottom: 12px;">
<p style="margin: 0 0 8px 0; font-weight: 500;">2. 使用 Token</p>
<p style="margin: 0 0 8px 0;">在请求头中添加 Authorization 字段:</p>
<p style="margin: 0 0 8px 0; font-weight: 500;" data-i18n="apiDocs.authUseToken">2. 使用 Token</p>
<p style="margin: 0 0 8px 0;" data-i18n="apiDocs.authUseTokenDesc">在请求头中添加 Authorization 字段:</p>
<pre style="background: var(--bg-primary); padding: 8px; border-radius: 4px; margin: 8px 0; overflow-x: auto; font-size: 0.8125rem;"><code>Authorization: Bearer your_token_here</code></pre>
<p style="margin: 8px 0 0 0; font-size: 0.8125rem; color: var(--text-muted);">💡 提示:本页面会自动使用您已登录的 Token,无需手动填写。</p>
<p style="margin: 8px 0 0 0; font-size: 0.8125rem; color: var(--text-muted);" data-i18n="apiDocs.authTip">💡 提示:本页面会自动使用您已登录的 Token,无需手动填写。</p>
</div>
<div id="token-status" style="display: none; background: rgba(0, 102, 255, 0.1); padding: 8px 12px; border-radius: 6px; border-left: 3px solid var(--accent-color);">
<p style="margin: 0; font-size: 0.8125rem; color: var(--accent-color);">
<p style="margin: 0; font-size: 0.8125rem; color: var(--accent-color);" data-i18n="apiDocs.tokenDetected">
<strong>✓ 已检测到 Token</strong> - 您可以直接测试 API 接口
</p>
</div>
@@ -899,10 +912,10 @@ Content-Type: application/json
<div class="api-docs-content">
<div class="api-docs-sidebar">
<h3>API 分组</h3>
<h3 data-i18n="apiDocs.sidebarGroupTitle">API 分组</h3>
<ul class="api-group-list" id="api-group-list">
<li class="api-group-item">
<a href="#" class="api-group-link active" data-group="all">全部接口</a>
<a href="#" class="api-group-link active" data-group="all" data-i18n="apiDocs.allApis">全部接口</a>
</li>
</ul>
</div>
@@ -914,13 +927,15 @@ Content-Type: application/json
<line x1="12" y1="8" x2="12" y2="12"/>
<line x1="12" y1="16" x2="12.01" y2="16"/>
</svg>
<h3>加载中...</h3>
<p>正在加载 API 文档</p>
<h3 data-i18n="apiDocs.loading">加载中...</h3>
<p data-i18n="apiDocs.loadingDesc">正在加载 API 文档</p>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/i18next.min.js"></script>
<script src="/static/js/i18n.js"></script>
<script src="/static/js/api-docs.js"></script>
</body>
</html>
+579 -568
View File
File diff suppressed because it is too large Load Diff