diff --git a/docs/frontend-i18n.md b/docs/frontend-i18n.md new file mode 100644 index 00000000..7a9f832f --- /dev/null +++ b/docs/frontend-i18n.md @@ -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 +仪表盘 +``` + +- 默认行为:脚本会替换元素的 `textContent`。 +- 同时翻译属性时,额外使用 `data-i18n-attr`,逗号分隔多个属性名: + +```html + +``` + +### 4.2 默认文本的作用 + +- HTML 内的中文默认值作为「**无 JS / 初始化前**」的占位内容: + - 页面在 JS 尚未加载完成时不会出现空白或 key。 + - JS 初始化后会用当前语言覆盖这些文本。 + +--- + +## 五、JavaScript 中的文案规范 + +### 5.1 全局翻译函数 `t()` + +由 `i18n.js` 暴露以下全局函数: + +- `window.t(key: string): string` + - 返回当前语言下的翻译文本,若缺失则回退到默认语言,再不行则返回 key 本身。 +- `window.changeLanguage(lang: string): Promise` + - 切换语言并刷新页面文案(不会刷新整页)。 + +示例(以 `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. 更新: + - `` 属性; + - 所有带 `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 +
+ + +
+``` + +对应 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` 返回对应语言,前端只负责展示。 + diff --git a/web/static/css/style.css b/web/static/css/style.css index ad0aa81d..1921d298 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -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 { diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json new file mode 100644 index 00000000..2f807062 --- /dev/null +++ b/web/static/i18n/en-US.json @@ -0,0 +1,925 @@ +{ + "common": { + "ok": "OK", + "cancel": "Cancel", + "refresh": "Refresh", + "close": "Close", + "edit": "Edit", + "delete": "Delete", + "save": "Save", + "loading": "Loading…", + "search": "Search", + "clearSearch": "Clear search", + "noData": "No data", + "confirm": "Confirm", + "copy": "Copy", + "copied": "Copied", + "copyFailed": "Copy failed" + }, + "header": { + "title": "CyberStrikeAI", + "apiDocs": "API Docs", + "logout": "Sign out", + "language": "Interface language", + "backToDashboard": "Back to dashboard", + "userMenu": "User menu", + "version": "Current version", + "toggleSidebar": "Collapse/expand sidebar" + }, + "login": { + "title": "Sign in to CyberStrikeAI", + "subtitle": "Enter the access password from config", + "passwordLabel": "Password", + "passwordPlaceholder": "Enter password", + "submit": "Sign in" + }, + "nav": { + "dashboard": "Dashboard", + "chat": "Chat", + "infoCollect": "Recon", + "tasks": "Tasks", + "vulnerabilities": "Vulnerabilities", + "mcp": "MCP", + "mcpMonitor": "MCP Monitor", + "mcpManagement": "MCP Management", + "knowledge": "Knowledge", + "knowledgeRetrievalLogs": "Retrieval history", + "knowledgeManagement": "Knowledge management", + "skills": "Skills", + "skillsMonitor": "Skills monitor", + "skillsManagement": "Skills management", + "roles": "Roles", + "rolesManagement": "Roles management", + "settings": "System settings" + }, + "dashboard": { + "title": "Dashboard", + "refresh": "Refresh", + "refreshData": "Refresh data", + "runningTasks": "Running tasks", + "vulnTotal": "Total vulnerabilities", + "toolCalls": "Tool invocations", + "successRate": "Tool success rate", + "clickToViewTasks": "Click to view tasks", + "clickToViewVuln": "Click to view vulnerabilities", + "clickToViewMCP": "Click to view MCP monitor", + "severityDistribution": "Vulnerability severity distribution", + "severityCritical": "Critical", + "severityHigh": "High", + "severityMedium": "Medium", + "severityLow": "Low", + "severityInfo": "Info", + "runOverview": "Run overview", + "batchQueues": "Batch task queues", + "pending": "Pending", + "executing": "Running", + "completed": "Completed", + "toolInvocations": "Tool invocations", + "callsUnit": "calls", + "toolsUnit": "tools", + "knowledgeLabel": "Knowledge", + "knowledgeItems": "items", + "categoriesUnit": "categories", + "skillsLabel": "Skills", + "skillUnit": "Skills", + "quickLinks": "Quick links", + "toolsExecCount": "Tool execution count", + "ctaTitle": "Start your security journey", + "ctaSub": "Describe your target in chat, AI will assist with scanning and vulnerability analysis", + "goToChat": "Go to chat", + "noTasks": "No tasks", + "totalCount": "{{count}} total", + "notEnabled": "Disabled", + "enabled": "Enabled", + "toConfigure": "To configure", + "toUse": "To use", + "active": "Active", + "highFreq": "High frequency", + "noCallData": "No call data" + }, + "chat": { + "newChat": "New chat", + "searchHistory": "Search history...", + "conversationGroups": "Conversation groups", + "addGroup": "New group", + "recentConversations": "Recent conversations", + "batchManage": "Batch manage", + "attackChain": "Attack chain", + "viewAttackChain": "View attack chain", + "selectRole": "Select role", + "defaultRole": "Default", + "inputPlaceholder": "Enter target or command... (type @ to select tools | Shift+Enter newline, Enter send)", + "selectFile": "Select file", + "uploadFile": "Upload file (multi-select or drag & drop)", + "send": "Send", + "searchInGroup": "Search in group...", + "loadingTools": "Loading tools...", + "noMatchTools": "No matching tools", + "penetrationTestDetail": "Penetration test details", + "expandDetail": "Expand details", + "noProcessDetail": "No process details (execution may be too fast or no detailed events)", + "copyMessageTitle": "Copy message", + "emptyGroupConversations": "This group has no conversations yet.", + "noMatchingConversationsInGroup": "No matching conversations found.", + "renameGroupPrompt": "Please enter new name:", + "deleteGroupConfirm": "Are you sure you want to delete this group? Conversations in the group will not be deleted, but will be removed from the group.", + "deleteConversationConfirm": "Are you sure you want to delete this conversation?", + "renameFailed": "Rename failed", + "viewAttackChainSelectConv": "Please select a conversation to view attack chain", + "viewAttackChainCurrentConv": "View attack chain of current conversation", + "executeFailed": "Execution failed", + "callOpenAIFailed": "Call OpenAI failed", + "systemReadyMessage": "System is ready. Please enter your test requirements, and the system will automatically perform the corresponding security tests.", + "addNewGroup": "+ New group" + }, + "tasks": { + "title": "Task management", + "newTask": "New task", + "autoRefresh": "Auto refresh", + "historyHint": "Tip: Completed task history available. Check \"Show history\" to view.", + "statusRunning": "Running", + "statusCancelling": "Cancelling", + "statusFailed": "Failed", + "statusTimeout": "Timeout", + "statusCancelled": "Cancelled", + "statusCompleted": "Completed", + "historyBadge": "History", + "duration": "Duration", + "completedAt": "Completed at", + "startedAt": "Started at", + "clickToCopy": "Click to copy", + "unnamedTask": "Unnamed task", + "unknown": "Unknown", + "unknownTime": "Unknown time", + "clearHistoryConfirm": "Clear all task history?", + "cancelTaskFailed": "Cancel task failed", + "copiedToast": "Copied!", + "cancelling": "Cancelling...", + "enterTaskPrompt": "Enter at least one task", + "noValidTask": "No valid tasks", + "createBatchQueueFailed": "Failed to create batch task queue", + "noBatchQueues": "Currently there are no batch task queues", + "recentCompletedTasks": "Recently completed tasks (last 24 hours)", + "clearHistory": "Clear history", + "cancelTask": "Cancel task", + "viewConversation": "View conversation", + "conversationIdLabel": "Conversation ID", + "statusPending": "Pending", + "statusPaused": "Paused", + "confirmCancelTasks": "Cancel {{n}} selected task(s)?", + "batchCancelResultPartial": "Batch cancel: {{success}} succeeded, {{fail}} failed", + "batchCancelResultSuccess": "Successfully cancelled {{n}} task(s)", + "taskCount": "{{count}} task(s)", + "queueIdLabel": "Queue ID", + "createdTimeLabel": "Created at", + "totalLabel": "Total", + "pendingLabel": "Pending", + "runningLabel": "Running", + "completedLabel": "Completed", + "failedLabel": "Failed", + "cancelledLabel": "Cancelled", + "loadingTasks": "Loading...", + "loadFailedRetry": "Load failed", + "loadTaskListFailed": "Failed to load task list", + "getQueueDetailFailed": "Failed to load queue details", + "startBatchQueueFailed": "Failed to start batch queue", + "pauseQueueFailed": "Failed to pause queue", + "pauseQueueConfirm": "Pause this batch queue? The current task will be stopped; remaining tasks will stay pending.", + "deleteQueueConfirm": "Delete this batch queue? This cannot be undone.", + "deleteQueueFailed": "Failed to delete batch queue", + "batchQueueTitle": "Batch task queue", + "resumeExecute": "Resume", + "taskIncomplete": "Task information incomplete", + "cannotGetTaskMessageInput": "Cannot get task message input", + "taskMessageRequired": "Task message is required", + "saveTaskFailed": "Failed to save task", + "queueInfoMissing": "Queue information not found", + "addTaskFailed": "Failed to add task", + "confirmDeleteTask": "Delete this task?\n\nTask: {{message}}\n\nThis cannot be undone.", + "deleteTaskFailed": "Failed to delete task", + "paginationShow": "{{start}}-{{end}} of {{total}}", + "paginationPerPage": "Per page", + "paginationFirst": "First", + "paginationPrev": "Previous", + "paginationNext": "Next", + "paginationLast": "Last", + "paginationPage": "Page {{current}} / {{total}}", + "deleteQueue": "Delete queue", + "retry": "Retry", + "noMatchingTasks": "No matching tasks", + "updateTaskFailed": "Failed to update task", + "durationSeconds": "s", + "durationMinutes": "m", + "durationHours": "h" + }, + "infoCollect": { + "enterFofaQuery": "Enter FOFA query syntax", + "querying": "Querying...", + "queryFailed": "Query failed", + "enterNaturalLanguage": "Enter natural language description", + "cancelParse": "Cancel parse", + "clickToCancelParse": "Click to cancel AI parse", + "parseToFofa": "Parse natural language to FOFA query", + "parseResultEmpty": "Parse result empty: Please add/modify FOFA query in popup", + "queryPlaceholder": "e.g. app=\"Apache\" && country=\"CN\"", + "selectAll": "Select all/none", + "selectRow": "Select row", + "copyTarget": "Copy target", + "sendToChat": "Send to chat (editable; Ctrl/Cmd+click to send directly)", + "noTargetToCopy": "No target to copy", + "targetCopied": "Target copied", + "manualCopyHint": "Copy failed, please copy manually: ", + "cannotInferTarget": "Cannot infer scan target from row (include host/ip/port/domain in fields)", + "noSendMessage": "sendMessage() not found, please refresh and retry", + "filledToInput": "Filled to chat input, edit and send", + "noExportResult": "No results to export", + "xlsxNotLoaded": "XLSX library not loaded, please refresh and retry", + "noResults": "No results", + "selectRowsFirst": "Select rows to scan first", + "noScanTarget": "No scan targets inferred from selection (include host/ip/port/domain in fields)", + "batchScanFailed": "Batch scan failed", + "batchQueueCreated": "Batch scan queue created", + "field": "Field", + "parsePending": "AI parsing...", + "parsePendingClickCancel": "AI parsing... (click button to cancel)", + "parseSlow": "AI parse is taking a while, still processing…", + "parseDone": "AI parse complete", + "parseCancelled": "AI parse cancelled", + "parseFailed": "AI parse failed: ", + "parseResultTitle": "AI parse result", + "naturalLanguageLabel": "Natural language", + "fofaQueryEditable": "FOFA query (editable)", + "confirmBeforeQuery": "Confirm syntax and scope before running the query.", + "reminder": "Reminder", + "explanation": "Explanation", + "actions": "Actions", + "batchScanTitle": "FOFA batch scan", + "queueCreatedSkipped": "Queue created ({{n}} rows skipped, no target)", + "createQueueFailed": "Failed to create batch queue", + "loading": "Loading...", + "none": "None", + "truncated": "truncated", + "resultsMeta": "Total {{total}} · This page {{count}} · page={{page}} · size={{size}}", + "parseModalCancel": "Cancel", + "parseModalApply": "Fill into query", + "parseModalApplyRun": "Fill and query" + }, + "vulnerability": { + "title": "Vulnerability management", + "addVuln": "Add vulnerability", + "editVuln": "Edit vulnerability", + "loadFailed": "Failed to load vulnerabilities", + "deleteConfirm": "Delete this vulnerability?" + }, + "mcp": { + "monitorTitle": "MCP Status Monitor", + "execStats": "Execution stats", + "latestExecutions": "Latest executions", + "toolSearch": "Tool search", + "toolSearchPlaceholder": "Enter tool name...", + "statusFilter": "Status filter", + "filterAll": "All", + "selectedCount": "{{count}} selected", + "selectAll": "Select all", + "deselectAll": "Deselect all", + "deleteSelected": "Batch delete", + "deleteExecConfirm": "Delete this execution record?", + "batchDeleteFailed": "Batch delete failed", + "managementTitle": "MCP Management", + "addExternal": "Add external MCP", + "toolConfig": "MCP tool config", + "saveToolConfig": "Save tool config", + "externalConfig": "External MCP config", + "loadingTools": "Loading tools...", + "loadToolsTimeout": "Tools load timeout. External MCP may be slow. Click Refresh to retry or check connection.", + "loadToolsFailed": "Failed to load tools", + "noTools": "No tools", + "externalBadge": "External", + "externalFrom": "External ({{name}})", + "externalToolFrom": "External MCP - Source: {{name}}", + "noDescription": "No description", + "paginationInfo": "{{start}}-{{end}} of {{total}} tools", + "perPage": "Per page:", + "firstPage": "First", + "prevPage": "Previous", + "nextPage": "Next", + "lastPage": "Last", + "pageInfo": "Page {{page}} of {{total}}", + "currentPageEnabled": "Enabled on current page", + "totalEnabled": "Total enabled", + "toolsConfigSaved": "Tool configuration saved!", + "saveToolsConfigFailed": "Failed to save tool config", + "getConfigFailed": "Failed to get config", + "noExternalMCP": "No external MCP configured", + "clickToAddExternal": "Click \"Add external MCP\" to configure", + "connected": "Connected", + "connecting": "Connecting...", + "connectionFailed": "Connection failed", + "disabled": "Disabled", + "disconnected": "Disconnected", + "stopConnection": "Stop connection", + "startConnection": "Start connection", + "stop": "Stop", + "start": "Start", + "editConfig": "Edit config", + "deleteConfig": "Delete config", + "transportMode": "Transport", + "toolCount": "Tool count", + "description": "Description", + "timeout": "Timeout", + "command": "Command", + "addExternalMCP": "Add external MCP", + "editExternalMCP": "Edit external MCP", + "jsonEmpty": "JSON cannot be empty", + "jsonError": "JSON format error", + "configMustBeObject": "Config error: Must be JSON object with name as key", + "configNeedOne": "Config error: At least one config item required", + "configNameEmpty": "Config error: Name cannot be empty", + "configMustBeObj": "Config error: \"{{name}}\" must be object", + "configNeedCommand": "Config error: \"{{name}}\" needs command (stdio) or url (http/sse)", + "configStdioNeedCommand": "Config error: \"{{name}}\" stdio mode needs command", + "configHttpNeedUrl": "Config error: \"{{name}}\" http mode needs url", + "configSseNeedUrl": "Config error: \"{{name}}\" sse mode needs url", + "saveSuccess": "Saved", + "deleteSuccess": "Deleted", + "deleteExternalConfirm": "Delete external MCP \"{{name}}\"?", + "operationFailed": "Operation failed", + "connectionFailedCheck": "Connection failed. Check config and network.", + "connectionTimeout": "Connection timeout. Check config and network.", + "totalCount": "Total", + "enabledCount": "Enabled", + "disabledCount": "Disabled", + "connectedCount": "Connected" + }, + "settings": { + "title": "System settings", + "nav": { + "basic": "Basic", + "robots": "Bots", + "terminal": "Terminal", + "security": "Security" + }, + "robots": { + "title": "Bot settings", + "description": "Configure WeCom, DingTalk and Lark bots so you can chat with CyberStrikeAI on your phone without opening the web UI.", + "wecom": { + "title": "WeCom", + "enabled": "Enable WeCom bot" + }, + "dingtalk": { + "title": "DingTalk", + "enabled": "Enable DingTalk bot" + }, + "lark": { + "title": "Lark", + "enabled": "Enable Lark bot" + } + }, + "apply": { + "button": "Apply configuration", + "loadFailed": "Failed to load configuration", + "fillRequired": "Please fill in all required fields (marked with *)", + "applyFailed": "Failed to apply configuration", + "applySuccess": "Configuration applied successfully!" + }, + "security": { + "changePassword": "Change password", + "fillPasswordHint": "Fill current and new password correctly. New password at least 8 characters, must match twice.", + "changePasswordFailed": "Failed to change password", + "passwordUpdated": "Password updated. Please sign in again with new password." + } + }, + "auth": { + "sessionExpired": "Session expired, please sign in again", + "unauthorized": "Unauthorized", + "enterPassword": "Please enter password", + "loginFailedCheck": "Sign-in failed, please check the password", + "loginFailedRetry": "Sign-in failed, please try again later", + "loggedOut": "Signed out" + }, + "knowledge": { + "title": "Knowledge management", + "retrievalLogs": "Retrieval history", + "totalItems": "Total items", + "categories": "Categories", + "addKnowledge": "Add knowledge", + "rebuildIndex": "Rebuild index", + "rebuildIndexConfirm": "Rebuild index?", + "deleteItemConfirm": "Delete this knowledge item?", + "notEnabledTitle": "Knowledge base function not enabled", + "notEnabledHint": "Please go to system settings to enable knowledge retrieval.", + "goToSettings": "Go to settings" + }, + "roles": { + "title": "Role management", + "createRole": "Create role", + "searchPlaceholder": "Search roles...", + "deleteConfirm": "Delete this role?" + }, + "skills": { + "title": "Skills management", + "monitorTitle": "Skills monitor", + "createSkill": "Create Skill", + "callStats": "Call stats", + "addSkill": "Add Skill", + "editSkill": "Edit Skill", + "loadListFailed": "Failed to load skills list", + "noSkills": "No skills. Click \"Create Skill\" to add first.", + "noMatch": "No matching skills", + "searchFailed": "Search failed", + "refreshed": "Refreshed", + "loadDetailFailed": "Failed to load skill details", + "viewFailed": "Failed to view skill", + "saving": "Saving...", + "saveFailed": "Failed to save skill", + "deleteFailed": "Failed to delete skill", + "loadStatsFailed": "Failed to load skills monitor data", + "clearStatsConfirm": "Clear all Skills statistics? This cannot be undone.", + "statsCleared": "Skills statistics cleared", + "clearStatsFailed": "Failed to clear statistics" + }, + "apiDocs": { + "curlCopied": "curl command copied to clipboard!" + }, + "chatGroup": { + "search": "Search", + "edit": "Edit", + "delete": "Delete", + "clearSearch": "Clear search", + "searchInGroupPlaceholder": "Search in group...", + "attackChain": "Attack chain", + "viewAttackChain": "View attack chain", + "selectRole": "Select role", + "close": "Close", + "selectFile": "Select file", + "uploadFile": "Upload file (multi-select or drag & drop)", + "send": "Send", + "rolePanelTitle": "Select role", + "copyMessage": "Copy message", + "remove": "Remove" + }, + "mcpMonitor": { + "deselectAll": "Deselect all", + "statusPending": "Pending", + "statusCompleted": "Completed", + "statusRunning": "Running", + "statusFailed": "Failed", + "loading": "Loading...", + "noStatsData": "No statistical data", + "noExecutions": "No execution records", + "noRecordsWithFilter": "No records with current filter", + "paginationInfo": "Show {{start}}-{{end}} of {{total}} records", + "perPageLabel": "Per page", + "loadStatsError": "Failed to load statistics", + "loadExecutionsError": "Failed to load execution records", + "totalCalls": "Total calls", + "successFailed": "Success {{success}} / Failed {{failed}}", + "successRate": "Success rate", + "statsFromAllTools": "From all tool calls", + "lastCall": "Last call", + "lastRefreshTime": "Last refresh", + "noCallsYet": "No calls yet", + "unknownTool": "Unknown tool", + "successFailedRate": "Success {{success}} / Failed {{failed}} · {{rate}}% success rate", + "columnTool": "Tool", + "columnStatus": "Status", + "columnStartTime": "Start time", + "columnDuration": "Duration", + "columnActions": "Actions", + "viewDetail": "View details", + "delete": "Delete", + "deleteExecTitle": "Delete this execution record", + "deleteExecConfirmSingle": "Are you sure you want to delete this execution record? This cannot be undone.", + "deleteExecFailed": "Failed to delete execution record", + "execDeleted": "Execution record deleted", + "selectExecFirst": "Please select execution record(s) to delete first", + "batchDeleteConfirm": "Are you sure you want to delete the selected {{count}} execution record(s)? This cannot be undone.", + "batchDeleteSuccess": "Successfully deleted {{count}} execution record(s)", + "unknown": "Unknown", + "durationSeconds": "{{n}} sec", + "durationMinutes": "{{minutes}} min {{seconds}} sec", + "durationMinutesOnly": "{{minutes}} min", + "durationHours": "{{hours}} hr {{minutes}} min", + "durationHoursOnly": "{{hours}} hr" + }, + "knowledgePage": { + "totalContent": "Total content", + "categoryFilter": "Category filter", + "all": "All", + "searchPlaceholder": "Search knowledge...", + "loading": "Loading..." + }, + "retrievalLogs": { + "totalRetrievals": "Total retrievals", + "successRetrievals": "Success", + "successRate": "Success rate", + "retrievedItems": "Items retrieved", + "conversationId": "Conversation ID", + "messageId": "Message ID", + "filter": "Filter", + "optionalConversation": "Optional: filter by conversation", + "optionalMessage": "Optional: filter by message", + "loading": "Loading...", + "noRecords": "No retrieval records yet", + "noQuery": "No query content", + "itemsUnit": "items", + "hasResults": "Has results", + "noResults": "No results", + "clickToCopy": "Click to copy", + "retrievalResult": "Retrieval result", + "foundCount": "Found {{count}} related knowledge item(s)", + "foundUnknown": "Found related knowledge (count unknown)", + "noMatch": "No matching knowledge items", + "retrievedItemsLabel": "Retrieved knowledge items:", + "viewDetails": "View details", + "loadError": "Failed to load retrieval logs", + "detailError": "Unable to get retrieval details", + "deleteError": "Failed to delete retrieval log", + "detailsTitle": "Retrieval details", + "queryInfo": "Query info", + "queryContent": "Query content:", + "retrievalInfo": "Retrieval info", + "riskType": "Risk type", + "retrievalTime": "Retrieval time", + "noItemDetails": "No knowledge item details found", + "noContentPreview": "No content preview", + "untitled": "Untitled", + "uncategorized": "Uncategorized", + "relatedInfo": "Related info", + "itemsCount": "{{count}} knowledge item(s)", + "deleteConfirm": "Delete this retrieval record?" + }, + "infoCollectPage": { + "title": "Recon", + "reset": "Reset", + "confirm": "OK", + "fofaQuerySyntax": "FOFA query syntax", + "naturalLanguage": "Natural language (AI parses to FOFA)", + "returnCount": "Return count", + "pageNum": "Page", + "returnFields": "Return fields (comma-separated)", + "queryResults": "Query results", + "selectedRows": "{{count}} selected", + "selectedRowsZero": "0 selected", + "columns": "Columns", + "exportCsv": "Export CSV", + "exportJson": "Export JSON", + "exportXlsx": "Export XLSX", + "batchScan": "Batch scan", + "showColumns": "Show columns", + "columnsPanelAll": "Select all", + "columnsPanelNone": "Deselect all", + "columnsPanelClose": "Close", + "formHint": "See FOFA docs for query syntax; supports && / || / ().", + "parseBtn": "AI parse", + "parseHint": "Result will open in a popup for editing before running the query.", + "minFields": "Min fields", + "webCommon": "Web common", + "intelEnhanced": "Intel enhanced", + "presetApache": "Apache + China", + "presetLogin": "Login page + China", + "presetDomain": "By domain", + "presetIp": "By IP", + "nlPlaceholder": "e.g. Apache sites in Missouri, US, title contains Home", + "showHideColumns": "Show/hide columns", + "exportCsvTitle": "Export results as CSV (UTF-8)", + "exportJsonTitle": "Export results as JSON", + "exportXlsxTitle": "Export results as Excel", + "batchScanTitle": "Create batch task queue from selected rows" + }, + "vulnerabilityPage": { + "statTotal": "Total", + "filter": "Filter", + "clear": "Clear", + "vulnId": "Vuln ID", + "conversationId": "Conversation ID", + "severity": "Severity", + "status": "Status", + "statusOpen": "Open", + "statusConfirmed": "Confirmed", + "statusFixed": "Fixed", + "statusFalsePositive": "False positive", + "searchVulnId": "Search vuln ID", + "filterConversation": "Filter by conversation", + "loading": "Loading...", + "noRecords": "No vulnerability records" + }, + "tasksPage": { + "statusFilter": "Status filter", + "statusPending": "Pending", + "statusPaused": "Paused", + "statusCancelled": "Cancelled", + "searchQueuePlaceholder": "Search queue ID, title or created time", + "searchKeywordPlaceholder": "Enter keyword..." + }, + "skillsPage": { + "clearStats": "Clear stats", + "clearStatsTitle": "Clear all statistics", + "skillsCallStats": "Skills call stats", + "searchPlaceholder": "Search Skills...", + "loading": "Loading..." + }, + "settingsBasic": { + "basicTitle": "Basic settings", + "openaiConfig": "OpenAI config", + "fofaConfig": "FOFA config", + "agentConfig": "Agent config", + "knowledgeConfig": "Knowledge base config", + "baseUrl": "Base URL", + "apiKey": "API Key", + "model": "Model", + "openaiBaseUrlPlaceholder": "https://api.openai.com/v1", + "openaiApiKeyPlaceholder": "Enter OpenAI API Key", + "modelPlaceholder": "gpt-4", + "fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all (optional)", + "fofaBaseUrlHint": "Leave empty for default.", + "email": "Email", + "fofaEmailPlaceholder": "Enter FOFA email", + "fofaApiKeyPlaceholder": "Enter FOFA API Key", + "fofaApiKeyHint": "Stored in server config (config.yaml) only.", + "maxIterations": "Max iterations", + "iterationsPlaceholder": "30", + "enableKnowledge": "Enable knowledge retrieval", + "knowledgeBasePath": "Knowledge base path", + "knowledgeBasePathPlaceholder": "knowledge_base", + "knowledgeBasePathHint": "Relative to config file directory", + "embeddingConfig": "Embedding config", + "provider": "Provider", + "embeddingBaseUrlPlaceholder": "Leave empty to use OpenAI base_url", + "embeddingApiKeyPlaceholder": "Leave empty to use OpenAI api_key", + "modelName": "Model name", + "embeddingModelPlaceholder": "text-embedding-v4", + "retrievalConfig": "Retrieval config", + "topK": "Top-K results", + "topKPlaceholder": "5", + "topKHint": "Number of top-K results to return", + "similarityThreshold": "Similarity threshold", + "similarityPlaceholder": "0.7", + "similarityHint": "Results below this value are filtered (0-1)", + "hybridWeight": "Hybrid weight", + "hybridPlaceholder": "0.7", + "hybridHint": "Vector weight (0-1); 1.0 = vector only, 0.0 = keyword only", + "indexConfig": "Index config", + "chunkSize": "Chunk size", + "chunkSizePlaceholder": "512", + "chunkSizeHint": "Max tokens per chunk (default 512)", + "chunkOverlap": "Chunk overlap", + "chunkOverlapPlaceholder": "50", + "chunkOverlapHint": "Overlap tokens between chunks (default 50)", + "maxChunksPerItem": "Max chunks per item", + "maxChunksPlaceholder": "0", + "maxChunksHint": "Max chunks per knowledge item (0 = no limit)", + "maxRpm": "Max RPM", + "maxRpmPlaceholder": "0", + "maxRpmHint": "Max requests per minute (0 = no limit)", + "rateLimitDelay": "Rate limit delay (ms)", + "rateLimitPlaceholder": "300", + "rateLimitHint": "Delay between requests (ms); 0 = no limit", + "maxRetries": "Max retries", + "maxRetriesPlaceholder": "3", + "maxRetriesHint": "Retries on rate limit or server error", + "retryDelay": "Retry delay (ms)", + "retryDelayPlaceholder": "1000", + "retryDelayHint": "Delay between retries (ms)" + }, + "settingsTerminal": { + "title": "Terminal", + "description": "Run commands on the server for ops and debugging. Commands run on the server; avoid sensitive or destructive operations.", + "terminalTab": "Terminal {{n}}", + "close": "Close", + "newTerminal": "New terminal" + }, + "settingsSecurity": { + "changePasswordTitle": "Change password", + "changePasswordDesc": "After changing password, sign in again with the new password.", + "currentPassword": "Current password", + "currentPasswordPlaceholder": "Enter current password", + "newPassword": "New password", + "newPasswordPlaceholder": "New password (at least 8 characters)", + "confirmPassword": "Confirm new password", + "confirmPasswordPlaceholder": "Enter new password again", + "clear": "Clear", + "changePasswordBtn": "Change password" + }, + "settingsRobotsExtra": { + "botCommandsTitle": "Bot commands", + "botCommandsDesc": "You can send these commands in chat (Chinese and English supported):" + }, + "mcpDetailModal": { + "title": "Tool call details", + "execInfo": "Execution info", + "tool": "Tool", + "status": "Status", + "time": "Time", + "executionId": "Execution ID", + "requestParams": "Request params", + "copyJson": "Copy JSON", + "responseResult": "Response", + "copyContent": "Copy content", + "correctInfo": "Correct info", + "errorInfo": "Error info", + "copyError": "Copy error" + }, + "attackChainModal": { + "title": "Attack chain", + "regenerate": "Regenerate", + "regenerateTitle": "Regenerate attack chain (include latest conversation)", + "exportPng": "Export PNG", + "exportSvg": "Export SVG", + "refreshTitle": "Refresh current attack chain", + "nodesEdges": "Nodes: {{nodes}} | Edges: {{edges}}", + "searchPlaceholder": "Search nodes...", + "allTypes": "All types", + "target": "Target", + "action": "Action", + "vulnerability": "Vulnerability", + "allRisks": "All risks", + "highRisk": "High (80-100)", + "mediumHighRisk": "Medium-high (60-79)", + "mediumRisk": "Medium (40-59)", + "lowRisk": "Low (0-39)", + "resetFilter": "Reset filter", + "loading": "Loading...", + "riskLevel": "Risk level", + "lineMeaning": "Line meaning", + "blueLine": "Blue: action finds vulnerability", + "redLine": "Red: enables/contributes", + "grayLine": "Gray: logical order", + "nodeDetails": "Node details", + "closeDetails": "Close details" + }, + "externalMcpModal": { + "configJson": "Config JSON", + "formatLabel": "Format:", + "formatDesc": "JSON object; key = config name, value = config. Use Start/Stop buttons to control state.", + "formatJson": "Format JSON", + "loadExample": "Load example" + }, + "skillModal": { + "addSkill": "Add Skill", + "editSkill": "Edit Skill", + "skillName": "Skill name", + "skillNamePlaceholder": "e.g. sql-injection-testing", + "skillNameHint": "Letters, numbers, hyphens and underscores only", + "description": "Description", + "descriptionPlaceholder": "Short description", + "contentLabel": "Content (Markdown)", + "contentPlaceholder": "Enter skill content in Markdown...", + "contentHint": "YAML front matter supported (optional)" + }, + "knowledgeItemModal": { + "addKnowledge": "Add knowledge", + "editKnowledge": "Edit knowledge", + "category": "Category (risk type)", + "categoryPlaceholder": "e.g. SQL injection", + "title": "Title", + "titlePlaceholder": "Knowledge item title", + "contentLabel": "Content (Markdown)", + "contentPlaceholder": "Enter content in Markdown..." + }, + "batchManageModal": { + "title": "Manage conversations · {{count}} total", + "searchPlaceholder": "Search history", + "conversationName": "Conversation name", + "lastTime": "Last activity", + "action": "Action", + "selectAll": "Select all", + "deleteSelected": "Delete selected", + "confirmDeleteNone": "Please select at least one conversation to delete", + "confirmDeleteN": "Delete {{count}} selected conversation(s)?", + "deleteFailed": "Delete failed", + "unnamedConversation": "Unnamed conversation" + }, + "createGroupModal": { + "title": "Create group", + "description": "Group conversations for easier management.", + "selectIcon": "Click to choose icon", + "groupNamePlaceholder": "Enter group name", + "pickIcon": "Pick icon", + "customIcon": "Custom", + "confirmIcon": "OK", + "create": "Create", + "cancel": "Cancel", + "suggestionPenetrationTest": "Penetration Testing", + "suggestionCtf": "CTF", + "suggestionRedTeam": "Red Team", + "suggestionVulnerabilityMining": "Vulnerability Mining", + "nameExists": "Group name already exists, please use another name.", + "createFailed": "Create failed", + "unknownError": "Unknown error" + }, + "contextMenu": { + "viewAttackChain": "View attack chain", + "rename": "Rename", + "pinConversation": "Pin conversation", + "unpinConversation": "Unpin", + "batchManage": "Batch manage", + "moveToGroup": "Move to group", + "deleteConversation": "Delete conversation", + "pinGroup": "Pin group", + "unpinGroup": "Unpin", + "deleteGroup": "Delete group" + }, + "batchImportModal": { + "title": "New task", + "queueTitle": "Queue title", + "queueTitlePlaceholder": "Enter queue title (optional, for identification and filtering)", + "queueTitleHint": "Set a title for the batch task queue to make it easier to find and manage later.", + "role": "Role", + "defaultRole": "Default", + "roleHint": "Select a role; all tasks will be executed using that role's configuration (prompt and tools).", + "tasksList": "Task list (one task per line)", + "tasksListPlaceholder": "Enter task list, one per line", + "tasksListPlaceholderExample": "Enter task list, one per line, for example:\nScan open ports of 192.168.1.1\nCheck if https://example.com has SQL injection\nEnumerate subdomains of example.com", + "tasksListHint": "Enter one task command per line; the system will execute them in order. Empty lines are ignored.", + "tasksListHintFull": "Hint: Enter one task command per line; the system will execute these tasks in order. Empty lines are ignored.", + "createQueue": "Create queue" + }, + "batchQueueDetailModal": { + "title": "Batch queue details", + "addTask": "Add task", + "startExecute": "Start", + "pauseQueue": "Pause queue", + "deleteQueue": "Delete queue", + "queueTitle": "Task title", + "role": "Role", + "defaultRole": "Default", + "queueId": "Queue ID", + "status": "Status", + "createdAt": "Created at", + "startedAt": "Started at", + "completedAt": "Completed at", + "taskTotal": "Total tasks", + "taskList": "Task list", + "startLabel": "Start", + "completeLabel": "Complete", + "errorLabel": "Error", + "resultLabel": "Result" + }, + "editBatchTaskModal": { + "title": "Edit task", + "taskMessage": "Task message", + "taskMessagePlaceholder": "Enter task message" + }, + "addBatchTaskModal": { + "title": "Add task", + "taskMessage": "Task message", + "taskMessagePlaceholder": "Enter task message", + "add": "Add" + }, + "vulnerabilityModal": { + "conversationId": "Conversation ID", + "conversationIdPlaceholder": "Enter conversation ID", + "title": "Title", + "titlePlaceholder": "Vulnerability title", + "description": "Description", + "descriptionPlaceholder": "Detailed description", + "severity": "Severity", + "pleaseSelect": "Please select", + "severityCritical": "Critical", + "severityHigh": "High", + "severityMedium": "Medium", + "severityLow": "Low", + "severityInfo": "Info", + "status": "Status", + "statusOpen": "Open", + "statusConfirmed": "Confirmed", + "statusFixed": "Fixed", + "statusFalsePositive": "False positive", + "type": "Vulnerability type", + "typePlaceholder": "e.g. SQL injection, XSS, CSRF", + "target": "Target", + "targetPlaceholder": "Affected target (URL, IP, etc.)", + "proof": "Proof (POC)", + "proofPlaceholder": "Proof: request/response, screenshots, etc.", + "impact": "Impact", + "impactPlaceholder": "Impact description", + "recommendation": "Recommendation", + "recommendationPlaceholder": "Remediation" + }, + "roleModal": { + "addRole": "Add role", + "editRole": "Edit role", + "roleName": "Role name", + "roleNamePlaceholder": "Enter role name", + "roleDescription": "Role description", + "roleDescriptionPlaceholder": "Enter role description", + "roleIcon": "Role icon", + "roleIconPlaceholder": "Enter emoji, e.g. 🏆", + "roleIconHint": "Emoji shown in role selector.", + "userPrompt": "User prompt", + "userPromptPlaceholder": "Appended before user message...", + "userPromptHint": "This prompt is appended before user message to guide AI. It does not change system prompt.", + "relatedTools": "Related tools (optional)", + "defaultRoleToolsTitle": "Default role uses all tools", + "defaultRoleToolsDesc": "Default role uses all tools enabled in MCP management.", + "searchToolsPlaceholder": "Search tools...", + "loadingTools": "Loading tools...", + "relatedToolsHint": "Select tools to link; empty = use all from MCP management.", + "relatedSkills": "Related Skills (optional)", + "searchSkillsPlaceholder": "Search skill...", + "loadingSkills": "Loading skills...", + "relatedSkillsHint": "Selected skills are injected into system prompt before task execution.", + "enableRole": "Enable this role" + } +} diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json new file mode 100644 index 00000000..ef253cd6 --- /dev/null +++ b/web/static/i18n/zh-CN.json @@ -0,0 +1,925 @@ +{ + "common": { + "ok": "确定", + "cancel": "取消", + "refresh": "刷新", + "close": "关闭", + "edit": "编辑", + "delete": "删除", + "save": "保存", + "loading": "加载中…", + "search": "搜索", + "clearSearch": "清除搜索", + "noData": "暂无数据", + "confirm": "确认", + "copy": "复制", + "copied": "已复制", + "copyFailed": "复制失败" + }, + "header": { + "title": "CyberStrikeAI", + "apiDocs": "API 文档", + "logout": "退出登录", + "language": "界面语言", + "backToDashboard": "返回仪表盘", + "userMenu": "用户菜单", + "version": "当前版本", + "toggleSidebar": "折叠/展开侧边栏" + }, + "login": { + "title": "登录 CyberStrikeAI", + "subtitle": "请输入配置中的访问密码", + "passwordLabel": "密码", + "passwordPlaceholder": "输入登录密码", + "submit": "登录" + }, + "nav": { + "dashboard": "仪表盘", + "chat": "对话", + "infoCollect": "信息收集", + "tasks": "任务管理", + "vulnerabilities": "漏洞管理", + "mcp": "MCP", + "mcpMonitor": "MCP状态监控", + "mcpManagement": "MCP管理", + "knowledge": "知识", + "knowledgeRetrievalLogs": "检索历史", + "knowledgeManagement": "知识管理", + "skills": "Skills", + "skillsMonitor": "Skills状态监控", + "skillsManagement": "Skills管理", + "roles": "角色", + "rolesManagement": "角色管理", + "settings": "系统设置" + }, + "dashboard": { + "title": "仪表盘", + "refresh": "刷新", + "refreshData": "刷新数据", + "runningTasks": "运行中任务", + "vulnTotal": "漏洞总数", + "toolCalls": "工具调用次数", + "successRate": "工具执行成功率", + "clickToViewTasks": "点击查看任务管理", + "clickToViewVuln": "点击查看漏洞管理", + "clickToViewMCP": "点击查看 MCP 监控", + "severityDistribution": "漏洞严重程度分布", + "severityCritical": "严重", + "severityHigh": "高危", + "severityMedium": "中危", + "severityLow": "低危", + "severityInfo": "信息", + "runOverview": "运行概览", + "batchQueues": "批量任务队列", + "pending": "待执行", + "executing": "执行中", + "completed": "已完成", + "toolInvocations": "工具调用", + "callsUnit": "次调用", + "toolsUnit": "个工具", + "knowledgeLabel": "知识", + "knowledgeItems": "项知识", + "categoriesUnit": "个分类", + "skillsLabel": "Skills", + "skillUnit": "个 Skill", + "quickLinks": "快捷入口", + "toolsExecCount": "工具执行次数", + "ctaTitle": "开始你的安全之旅", + "ctaSub": "在对话中描述目标,AI 将协助执行扫描与漏洞分析", + "goToChat": "前往对话", + "noTasks": "暂无任务", + "totalCount": "共 {{count}} 个", + "notEnabled": "未启用", + "enabled": "已启用", + "toConfigure": "待配置", + "toUse": "待使用", + "active": "活跃", + "highFreq": "高频", + "noCallData": "暂无调用数据" + }, + "chat": { + "newChat": "新对话", + "searchHistory": "搜索历史记录...", + "conversationGroups": "对话分组", + "addGroup": "新建分组", + "recentConversations": "最近对话", + "batchManage": "批量管理", + "attackChain": "攻击链", + "viewAttackChain": "查看攻击链", + "selectRole": "选择角色", + "defaultRole": "默认", + "inputPlaceholder": "输入测试目标或命令... (输入 @ 选择工具 | Shift+Enter 换行,Enter 发送)", + "selectFile": "选择文件", + "uploadFile": "上传文件(可多选或拖拽到此处)", + "send": "发送", + "searchInGroup": "搜索分组中的对话...", + "loadingTools": "正在加载工具...", + "noMatchTools": "没有匹配的工具", + "penetrationTestDetail": "渗透测试详情", + "expandDetail": "展开详情", + "noProcessDetail": "暂无过程详情(可能执行过快或未触发详细事件)", + "copyMessageTitle": "复制消息内容", + "emptyGroupConversations": "该分组暂无对话", + "noMatchingConversationsInGroup": "未找到匹配的对话", + "renameGroupPrompt": "请输入新名称:", + "deleteGroupConfirm": "确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。", + "deleteConversationConfirm": "确定要删除此对话吗?", + "renameFailed": "重命名失败", + "viewAttackChainSelectConv": "请选择一个对话以查看攻击链", + "viewAttackChainCurrentConv": "查看当前对话的攻击链", + "executeFailed": "执行失败", + "callOpenAIFailed": "调用OpenAI失败", + "systemReadyMessage": "系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。", + "addNewGroup": "+ 新增分组" + }, + "tasks": { + "title": "任务管理", + "newTask": "新建任务", + "autoRefresh": "自动刷新", + "historyHint": "提示:有已完成的任务历史,请勾选\"显示历史记录\"查看", + "statusRunning": "执行中", + "statusCancelling": "取消中", + "statusFailed": "执行失败", + "statusTimeout": "执行超时", + "statusCancelled": "已取消", + "statusCompleted": "已完成", + "historyBadge": "历史记录", + "duration": "执行时长", + "completedAt": "完成时间", + "startedAt": "开始时间", + "clickToCopy": "点击复制", + "unnamedTask": "未命名任务", + "unknown": "未知", + "unknownTime": "未知时间", + "clearHistoryConfirm": "确定要清空所有任务历史记录吗?", + "cancelTaskFailed": "取消任务失败", + "copiedToast": "已复制!", + "cancelling": "取消中...", + "enterTaskPrompt": "请输入至少一个任务", + "noValidTask": "没有有效的任务", + "createBatchQueueFailed": "创建批量任务队列失败", + "noBatchQueues": "当前没有批量任务队列", + "recentCompletedTasks": "最近完成的任务(最近24小时)", + "clearHistory": "清空历史", + "cancelTask": "取消任务", + "viewConversation": "查看对话", + "conversationIdLabel": "对话ID", + "statusPending": "待执行", + "statusPaused": "已暂停", + "confirmCancelTasks": "确定要取消 {{n}} 个任务吗?", + "batchCancelResultPartial": "批量取消完成:成功 {{success}} 个,失败 {{fail}} 个", + "batchCancelResultSuccess": "成功取消 {{n}} 个任务", + "taskCount": "共 {{count}} 个任务", + "queueIdLabel": "队列ID", + "createdTimeLabel": "创建时间", + "totalLabel": "总计", + "pendingLabel": "待执行", + "runningLabel": "执行中", + "completedLabel": "已完成", + "failedLabel": "失败", + "cancelledLabel": "已取消", + "loadingTasks": "加载中...", + "loadFailedRetry": "加载失败", + "loadTaskListFailed": "获取任务列表失败", + "getQueueDetailFailed": "获取队列详情失败", + "startBatchQueueFailed": "启动批量任务失败", + "pauseQueueFailed": "暂停批量任务失败", + "pauseQueueConfirm": "确定要暂停这个批量任务队列吗?当前正在执行的任务将被停止,后续任务将保留待执行状态。", + "deleteQueueConfirm": "确定要删除这个批量任务队列吗?此操作不可恢复。", + "deleteQueueFailed": "删除批量任务队列失败", + "batchQueueTitle": "批量任务队列", + "resumeExecute": "继续执行", + "taskIncomplete": "任务信息不完整", + "cannotGetTaskMessageInput": "无法获取任务消息输入框", + "taskMessageRequired": "任务消息不能为空", + "saveTaskFailed": "保存任务失败", + "queueInfoMissing": "队列信息不存在", + "addTaskFailed": "添加任务失败", + "confirmDeleteTask": "确定要删除这个任务吗?\n\n任务内容: {{message}}\n\n此操作不可恢复。", + "deleteTaskFailed": "删除任务失败", + "paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 条", + "paginationPerPage": "每页显示", + "paginationFirst": "首页", + "paginationPrev": "上一页", + "paginationNext": "下一页", + "paginationLast": "末页", + "paginationPage": "第 {{current}} / {{total}} 页", + "deleteQueue": "删除队列", + "retry": "重试", + "noMatchingTasks": "当前没有符合条件的任务", + "updateTaskFailed": "更新任务失败", + "durationSeconds": "秒", + "durationMinutes": "分", + "durationHours": "小时" + }, + "infoCollect": { + "enterFofaQuery": "请输入 FOFA 查询语法", + "querying": "查询中...", + "queryFailed": "查询失败", + "enterNaturalLanguage": "请输入自然语言描述", + "cancelParse": "取消解析", + "clickToCancelParse": "点击取消 AI 解析", + "parseToFofa": "将自然语言解析为 FOFA 查询语法", + "parseResultEmpty": "解析结果为空:请在弹窗中补充/修改 FOFA 查询语法", + "queryPlaceholder": "例如:app=\"Apache\" && country=\"CN\"", + "selectAll": "全选/全不选", + "selectRow": "选择该行", + "copyTarget": "复制目标", + "sendToChat": "发送到对话(可编辑;Ctrl/⌘+点击可直接发送)", + "noTargetToCopy": "没有可复制的目标", + "targetCopied": "已复制目标", + "manualCopyHint": "复制失败,请手动复制:", + "cannotInferTarget": "无法从该行推断扫描目标(建议在 fields 中包含 host/ip/port/domain)", + "noSendMessage": "未找到 sendMessage(),请刷新页面后重试", + "filledToInput": "已填入对话输入框,可编辑后发送", + "noExportResult": "暂无可导出的结果", + "xlsxNotLoaded": "未加载 XLSX 库,请刷新页面后重试", + "noResults": "暂无结果", + "selectRowsFirst": "请先勾选需要扫描的行", + "noScanTarget": "未能从所选行推断任何可扫描目标(建议 fields 中包含 host/ip/port/domain)", + "batchScanFailed": "批量扫描失败", + "batchQueueCreated": "已创建批量扫描队列", + "field": "字段", + "parsePending": "AI 解析中...", + "parsePendingClickCancel": "AI 解析中...(点击按钮可取消)", + "parseSlow": "AI 解析耗时较长,仍在处理中…", + "parseDone": "AI 解析完成", + "parseCancelled": "已取消 AI 解析", + "parseFailed": "AI 解析失败:", + "parseResultTitle": "AI 解析结果", + "naturalLanguageLabel": "自然语言", + "fofaQueryEditable": "FOFA 查询语法(可编辑)", + "confirmBeforeQuery": "请人工确认语法与范围无误后再执行查询。", + "reminder": "提醒", + "explanation": "解析说明", + "actions": "操作", + "batchScanTitle": "FOFA 批量扫描", + "queueCreatedSkipped": "已创建队列(跳过 {{n}} 条无目标行)", + "createQueueFailed": "创建批量队列失败", + "loading": "加载中...", + "none": "无", + "truncated": "已截断", + "resultsMeta": "共 {{total}} 条 · 本页 {{count}} 条 · page={{page}} · size={{size}}", + "parseModalCancel": "取消", + "parseModalApply": "填入查询框", + "parseModalApplyRun": "填入并查询" + }, + "vulnerability": { + "title": "漏洞管理", + "addVuln": "添加漏洞", + "editVuln": "编辑漏洞", + "loadFailed": "加载漏洞失败", + "deleteConfirm": "确定要删除此漏洞吗?" + }, + "mcp": { + "monitorTitle": "MCP 状态监控", + "execStats": "执行统计", + "latestExecutions": "最新执行记录", + "toolSearch": "工具搜索", + "toolSearchPlaceholder": "输入工具名称...", + "statusFilter": "状态筛选", + "filterAll": "全部", + "selectedCount": "已选择 {{count}} 项", + "selectAll": "全选", + "deselectAll": "全不选", + "deleteSelected": "批量删除", + "deleteExecConfirm": "确定要删除此执行记录吗?", + "batchDeleteFailed": "批量删除执行记录失败", + "managementTitle": "MCP 管理", + "addExternal": "添加外部MCP", + "toolConfig": "MCP 工具配置", + "saveToolConfig": "保存工具配置", + "externalConfig": "外部 MCP 配置", + "loadingTools": "正在加载工具列表...", + "loadToolsTimeout": "加载工具列表超时,可能是外部MCP连接较慢。请点击\"刷新\"按钮重试,或检查外部MCP连接状态。", + "loadToolsFailed": "加载工具列表失败", + "noTools": "暂无工具", + "externalBadge": "外部", + "externalFrom": "外部 ({{name}})", + "externalToolFrom": "外部MCP工具 - 来源:{{name}}", + "noDescription": "无描述", + "paginationInfo": "显示 {{start}}-{{end}} / 共 {{total}} 个工具", + "perPage": "每页:", + "firstPage": "首页", + "prevPage": "上一页", + "nextPage": "下一页", + "lastPage": "末页", + "pageInfo": "第 {{page}} / {{total}} 页", + "currentPageEnabled": "当前页已启用", + "totalEnabled": "总计已启用", + "toolsConfigSaved": "工具配置已成功保存!", + "saveToolsConfigFailed": "保存工具配置失败", + "getConfigFailed": "获取配置失败", + "noExternalMCP": "暂无外部MCP配置", + "clickToAddExternal": "点击\"添加外部MCP\"按钮开始配置", + "connected": "已连接", + "connecting": "连接中...", + "connectionFailed": "连接失败", + "disabled": "已禁用", + "disconnected": "未连接", + "stopConnection": "停止连接", + "startConnection": "启动连接", + "stop": "停止", + "start": "启动", + "editConfig": "编辑配置", + "deleteConfig": "删除配置", + "transportMode": "传输模式", + "toolCount": "工具数量", + "description": "描述", + "timeout": "超时时间", + "command": "命令", + "addExternalMCP": "添加外部MCP", + "editExternalMCP": "编辑外部MCP", + "jsonEmpty": "JSON不能为空", + "jsonError": "JSON格式错误", + "configMustBeObject": "配置错误: 必须是JSON对象格式,key为配置名称,value为配置内容", + "configNeedOne": "配置错误: 至少需要一个配置项", + "configNameEmpty": "配置错误: 配置名称不能为空", + "configMustBeObj": "配置错误: \"{{name}}\" 的配置必须是对象", + "configNeedCommand": "配置错误: \"{{name}}\" 需要指定command(stdio模式)或url(http/sse模式)", + "configStdioNeedCommand": "配置错误: \"{{name}}\" stdio模式需要command字段", + "configHttpNeedUrl": "配置错误: \"{{name}}\" http模式需要url字段", + "configSseNeedUrl": "配置错误: \"{{name}}\" sse模式需要url字段", + "saveSuccess": "保存成功", + "deleteSuccess": "删除成功", + "deleteExternalConfirm": "确定要删除外部MCP \"{{name}}\" 吗?", + "operationFailed": "操作失败", + "connectionFailedCheck": "连接失败,请检查配置和网络连接", + "connectionTimeout": "连接超时,请检查配置和网络连接", + "totalCount": "总数", + "enabledCount": "已启用", + "disabledCount": "已停用", + "connectedCount": "已连接" + }, + "settings": { + "title": "系统设置", + "nav": { + "basic": "基本设置", + "robots": "机器人设置", + "terminal": "终端", + "security": "安全设置" + }, + "robots": { + "title": "机器人设置", + "description": "配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。", + "wecom": { + "title": "企业微信", + "enabled": "启用企业微信机器人" + }, + "dingtalk": { + "title": "钉钉", + "enabled": "启用钉钉机器人" + }, + "lark": { + "title": "飞书 (Lark)", + "enabled": "启用飞书机器人" + } + }, + "apply": { + "button": "应用配置", + "loadFailed": "加载配置失败", + "fillRequired": "请填写所有必填字段(标记为 * 的字段)", + "applyFailed": "应用配置失败", + "applySuccess": "配置已成功应用!" + }, + "security": { + "changePassword": "修改密码", + "fillPasswordHint": "请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。", + "changePasswordFailed": "修改密码失败", + "passwordUpdated": "密码已更新,请使用新密码重新登录。" + } + }, + "auth": { + "sessionExpired": "认证已过期,请重新登录", + "unauthorized": "未授权访问", + "enterPassword": "请输入密码", + "loginFailedCheck": "登录失败,请检查密码", + "loginFailedRetry": "登录失败,请稍后重试", + "loggedOut": "已退出登录" + }, + "knowledge": { + "title": "知识管理", + "retrievalLogs": "检索历史", + "totalItems": "总知识项", + "categories": "分类数", + "addKnowledge": "添加知识", + "rebuildIndex": "重建索引", + "rebuildIndexConfirm": "确定要重建索引吗?", + "deleteItemConfirm": "确定要删除这个知识项吗?", + "notEnabledTitle": "知识库功能未启用", + "notEnabledHint": "请前往系统设置启用知识检索功能", + "goToSettings": "前往设置" + }, + "roles": { + "title": "角色管理", + "createRole": "创建角色", + "searchPlaceholder": "搜索角色...", + "deleteConfirm": "确定要删除角色..." + }, + "skills": { + "title": "Skills管理", + "monitorTitle": "Skills状态监控", + "createSkill": "创建Skill", + "callStats": "调用统计", + "addSkill": "添加Skill", + "editSkill": "编辑Skill", + "loadListFailed": "加载skills列表失败", + "noSkills": "暂无skills,点击\"创建Skill\"创建第一个skill", + "noMatch": "没有找到匹配的skills", + "searchFailed": "搜索失败", + "refreshed": "已刷新", + "loadDetailFailed": "加载skill详情失败", + "viewFailed": "查看skill失败", + "saving": "保存中...", + "saveFailed": "保存skill失败", + "deleteFailed": "删除skill失败", + "loadStatsFailed": "加载skills监控数据失败", + "clearStatsConfirm": "确定要清空所有Skills统计数据吗?此操作不可恢复。", + "statsCleared": "已清空所有Skills统计数据", + "clearStatsFailed": "清空统计数据失败" + }, + "apiDocs": { + "curlCopied": "curl命令已复制到剪贴板!" + }, + "chatGroup": { + "search": "搜索", + "edit": "编辑", + "delete": "删除", + "clearSearch": "清除搜索", + "searchInGroupPlaceholder": "搜索分组中的对话...", + "attackChain": "攻击链", + "viewAttackChain": "查看攻击链", + "selectRole": "选择角色", + "close": "关闭", + "selectFile": "选择文件", + "uploadFile": "上传文件(可多选或拖拽到此处)", + "send": "发送", + "rolePanelTitle": "选择角色", + "copyMessage": "复制消息内容", + "remove": "移除" + }, + "mcpMonitor": { + "deselectAll": "取消全选", + "statusPending": "等待中", + "statusCompleted": "已完成", + "statusRunning": "执行中", + "statusFailed": "失败", + "loading": "加载中...", + "noStatsData": "暂无统计数据", + "noExecutions": "暂无执行记录", + "noRecordsWithFilter": "当前筛选条件下暂无记录", + "paginationInfo": "显示 {{start}}-{{end}} / 共 {{total}} 条记录", + "perPageLabel": "每页显示", + "loadStatsError": "无法加载统计信息", + "loadExecutionsError": "无法加载执行记录", + "totalCalls": "总调用次数", + "successFailed": "成功 {{success}} / 失败 {{failed}}", + "successRate": "成功率", + "statsFromAllTools": "统计自全部工具调用", + "lastCall": "最近一次调用", + "lastRefreshTime": "最后刷新时间", + "noCallsYet": "暂无调用", + "unknownTool": "未知工具", + "successFailedRate": "成功 {{success}} / 失败 {{failed}} · 成功率 {{rate}}%", + "columnTool": "工具", + "columnStatus": "状态", + "columnStartTime": "开始时间", + "columnDuration": "耗时", + "columnActions": "操作", + "viewDetail": "查看详情", + "delete": "删除", + "deleteExecTitle": "删除此执行记录", + "deleteExecConfirmSingle": "确定要删除此执行记录吗?此操作不可恢复。", + "deleteExecFailed": "删除执行记录失败", + "execDeleted": "执行记录已删除", + "selectExecFirst": "请先选择要删除的执行记录", + "batchDeleteConfirm": "确定要删除选中的 {{count}} 条执行记录吗?此操作不可恢复。", + "batchDeleteSuccess": "成功删除 {{count}} 条执行记录", + "unknown": "未知", + "durationSeconds": "{{n}} 秒", + "durationMinutes": "{{minutes}} 分 {{seconds}} 秒", + "durationMinutesOnly": "{{minutes}} 分", + "durationHours": "{{hours}} 小时 {{minutes}} 分", + "durationHoursOnly": "{{hours}} 小时" + }, + "knowledgePage": { + "totalContent": "总内容", + "categoryFilter": "分类筛选", + "all": "全部", + "searchPlaceholder": "搜索知识...", + "loading": "加载中..." + }, + "retrievalLogs": { + "totalRetrievals": "总检索次数", + "successRetrievals": "成功检索", + "successRate": "成功率", + "retrievedItems": "检索到知识项", + "conversationId": "对话ID", + "messageId": "消息ID", + "filter": "筛选", + "optionalConversation": "可选:筛选特定对话", + "optionalMessage": "可选:筛选特定消息", + "loading": "加载中...", + "noRecords": "暂无检索记录", + "noQuery": "无查询内容", + "itemsUnit": "项", + "hasResults": "有结果", + "noResults": "无结果", + "clickToCopy": "点击复制", + "retrievalResult": "检索结果", + "foundCount": "找到 {{count}} 个相关知识项", + "foundUnknown": "找到相关知识项(数量未知)", + "noMatch": "未找到匹配的知识项", + "retrievedItemsLabel": "检索到的知识项:", + "viewDetails": "查看详情", + "loadError": "加载检索日志失败", + "detailError": "无法获取检索详情", + "deleteError": "删除检索日志失败", + "detailsTitle": "检索详情", + "queryInfo": "查询信息", + "queryContent": "查询内容:", + "retrievalInfo": "检索信息", + "riskType": "风险类型", + "retrievalTime": "检索时间", + "noItemDetails": "未找到知识项详情", + "noContentPreview": "无内容预览", + "untitled": "未命名", + "uncategorized": "未分类", + "relatedInfo": "关联信息", + "itemsCount": "{{count}} 个知识项", + "deleteConfirm": "确定要删除这条检索记录吗?" + }, + "infoCollectPage": { + "title": "信息收集", + "reset": "重置", + "confirm": "确定", + "fofaQuerySyntax": "FOFA 查询语法", + "naturalLanguage": "自然语言(AI 解析为 FOFA 语法)", + "returnCount": "返回数量", + "pageNum": "页码", + "returnFields": "返回字段名(逗号分隔)", + "queryResults": "查询结果", + "selectedRows": "已选择 {{count}} 条", + "selectedRowsZero": "已选择 0 条", + "columns": "列", + "exportCsv": "导出 CSV", + "exportJson": "导出 JSON", + "exportXlsx": "导出 XLSX", + "batchScan": "批量扫描", + "showColumns": "显示字段", + "columnsPanelAll": "全选", + "columnsPanelNone": "全不选", + "columnsPanelClose": "关闭", + "formHint": "查询语法参考 FOFA 文档,支持 && / || / () 等。", + "parseBtn": "AI 解析", + "parseHint": "解析后会弹窗展示 FOFA 语法(可编辑),确认无误后再填入查询框并执行查询。", + "minFields": "最小字段", + "webCommon": "Web 常用", + "intelEnhanced": "情报增强", + "presetApache": "Apache + 中国", + "presetLogin": "登录页 + 中国", + "presetDomain": "指定域名", + "presetIp": "指定 IP", + "nlPlaceholder": "例如:找美国 Missouri 的 Apache 站点,标题包含 Home", + "showHideColumns": "显示/隐藏字段", + "exportCsvTitle": "导出当前结果为 CSV(UTF-8,兼容中文)", + "exportJsonTitle": "导出当前结果为 JSON", + "exportXlsxTitle": "导出当前结果为 Excel", + "batchScanTitle": "将所选行创建为批量任务队列" + }, + "vulnerabilityPage": { + "statTotal": "总漏洞数", + "filter": "筛选", + "clear": "清除", + "vulnId": "漏洞ID", + "conversationId": "会话ID", + "severity": "严重程度", + "status": "状态", + "statusOpen": "待处理", + "statusConfirmed": "已确认", + "statusFixed": "已修复", + "statusFalsePositive": "误报", + "searchVulnId": "搜索漏洞ID", + "filterConversation": "筛选特定会话", + "loading": "加载中...", + "noRecords": "暂无漏洞记录" + }, + "tasksPage": { + "statusFilter": "状态筛选", + "statusPending": "待执行", + "statusPaused": "已暂停", + "statusCancelled": "已取消", + "searchQueuePlaceholder": "搜索队列ID、标题或创建时间", + "searchKeywordPlaceholder": "输入关键字搜索..." + }, + "skillsPage": { + "clearStats": "清空统计", + "clearStatsTitle": "清空所有统计数据", + "skillsCallStats": "Skills调用统计", + "searchPlaceholder": "搜索Skills...", + "loading": "加载中..." + }, + "settingsBasic": { + "basicTitle": "基本设置", + "openaiConfig": "OpenAI 配置", + "fofaConfig": "FOFA 配置", + "agentConfig": "Agent 配置", + "knowledgeConfig": "知识库配置", + "baseUrl": "Base URL", + "apiKey": "API Key", + "model": "模型", + "openaiBaseUrlPlaceholder": "https://api.openai.com/v1", + "openaiApiKeyPlaceholder": "输入OpenAI API Key", + "modelPlaceholder": "gpt-4", + "fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all(可选)", + "fofaBaseUrlHint": "留空则使用默认地址。", + "email": "Email", + "fofaEmailPlaceholder": "输入 FOFA 账号邮箱", + "fofaApiKeyPlaceholder": "输入 FOFA API Key", + "fofaApiKeyHint": "仅保存在服务器配置中(`config.yaml`)。", + "maxIterations": "最大迭代次数", + "iterationsPlaceholder": "30", + "enableKnowledge": "启用知识检索功能", + "knowledgeBasePath": "知识库路径", + "knowledgeBasePathPlaceholder": "knowledge_base", + "knowledgeBasePathHint": "相对于配置文件所在目录的路径", + "embeddingConfig": "嵌入模型配置", + "provider": "提供商", + "embeddingBaseUrlPlaceholder": "留空则使用OpenAI配置的base_url", + "embeddingApiKeyPlaceholder": "留空则使用OpenAI配置的api_key", + "modelName": "模型名称", + "embeddingModelPlaceholder": "text-embedding-v4", + "retrievalConfig": "检索配置", + "topK": "Top-K 结果数量", + "topKPlaceholder": "5", + "topKHint": "检索返回的Top-K结果数量", + "similarityThreshold": "相似度阈值", + "similarityPlaceholder": "0.7", + "similarityHint": "相似度阈值(0-1),低于此值的结果将被过滤", + "hybridWeight": "混合检索权重", + "hybridPlaceholder": "0.7", + "hybridHint": "向量检索的权重(0-1),1.0表示纯向量检索,0.0表示纯关键词检索", + "indexConfig": "索引配置", + "chunkSize": "分块大小(Chunk Size)", + "chunkSizePlaceholder": "512", + "chunkSizeHint": "每个块的最大 token 数(默认 512),长文本会被分割成多个块", + "chunkOverlap": "分块重叠(Chunk Overlap)", + "chunkOverlapPlaceholder": "50", + "chunkOverlapHint": "块之间的重叠 token 数(默认 50),保持上下文连贯性", + "maxChunksPerItem": "单个知识项最大块数", + "maxChunksPlaceholder": "0", + "maxChunksHint": "单个知识项的最大块数量(0 表示不限制),防止单个文件消耗过多 API 配额", + "maxRpm": "每分钟最大请求数(Max RPM)", + "maxRpmPlaceholder": "0", + "maxRpmHint": "每分钟最大请求数(默认 0 表示不限制),如 OpenAI 默认 200 RPM", + "rateLimitDelay": "请求间隔延迟(毫秒)", + "rateLimitPlaceholder": "300", + "rateLimitHint": "请求间隔毫秒数(默认 300),用于避免 API 速率限制,设为 0 不限制", + "maxRetries": "最大重试次数", + "maxRetriesPlaceholder": "3", + "maxRetriesHint": "最大重试次数(默认 3),遇到速率限制或服务器错误时自动重试", + "retryDelay": "重试间隔(毫秒)", + "retryDelayPlaceholder": "1000", + "retryDelayHint": "重试间隔毫秒数(默认 1000),每次重试会递增延迟" + }, + "settingsTerminal": { + "title": "终端", + "description": "在服务器上执行命令,便于运维与调试。命令在服务端执行,请勿执行敏感或破坏性操作。", + "terminalTab": "终端 {{n}}", + "close": "关闭", + "newTerminal": "新终端" + }, + "settingsSecurity": { + "changePasswordTitle": "修改密码", + "changePasswordDesc": "修改登录密码后,需要使用新密码重新登录。", + "currentPassword": "当前密码", + "currentPasswordPlaceholder": "输入当前登录密码", + "newPassword": "新密码", + "newPasswordPlaceholder": "设置新密码(至少 8 位)", + "confirmPassword": "确认新密码", + "confirmPasswordPlaceholder": "再次输入新密码", + "clear": "清空", + "changePasswordBtn": "修改密码" + }, + "settingsRobotsExtra": { + "botCommandsTitle": "机器人命令说明", + "botCommandsDesc": "在对话中可发送以下命令(支持中英文):" + }, + "mcpDetailModal": { + "title": "工具调用详情", + "execInfo": "执行信息", + "tool": "工具", + "status": "状态", + "time": "时间", + "executionId": "执行 ID", + "requestParams": "请求参数", + "copyJson": "复制 JSON", + "responseResult": "响应结果", + "copyContent": "复制内容", + "correctInfo": "正确信息", + "errorInfo": "错误信息", + "copyError": "复制错误" + }, + "attackChainModal": { + "title": "攻击链可视化", + "regenerate": "重新生成", + "regenerateTitle": "重新生成攻击链(包含最新对话内容)", + "exportPng": "导出为PNG", + "exportSvg": "导出为SVG", + "refreshTitle": "刷新当前攻击链(不重新生成)", + "nodesEdges": "节点: {{nodes}} | 边: {{edges}}", + "searchPlaceholder": "搜索节点...", + "allTypes": "所有类型", + "target": "目标", + "action": "行动", + "vulnerability": "漏洞", + "allRisks": "所有风险", + "highRisk": "高风险 (80-100)", + "mediumHighRisk": "中高风险 (60-79)", + "mediumRisk": "中风险 (40-59)", + "lowRisk": "低风险 (0-39)", + "resetFilter": "重置筛选", + "loading": "加载中...", + "riskLevel": "风险等级", + "lineMeaning": "连接线含义", + "blueLine": "蓝色线:行动发现漏洞", + "redLine": "红色线:使能/促成关系", + "grayLine": "灰色线:逻辑顺序", + "nodeDetails": "节点详情", + "closeDetails": "关闭详情" + }, + "externalMcpModal": { + "configJson": "配置JSON", + "formatLabel": "配置格式:", + "formatDesc": "JSON对象,key为配置名称,value为配置内容。状态通过\"启动/停止\"按钮控制,无需在JSON中配置。", + "formatJson": "格式化JSON", + "loadExample": "加载示例" + }, + "skillModal": { + "addSkill": "添加Skill", + "editSkill": "编辑Skill", + "skillName": "Skill名称", + "skillNamePlaceholder": "例如: sql-injection-testing", + "skillNameHint": "只能包含字母、数字、连字符和下划线", + "description": "描述", + "descriptionPlaceholder": "Skill的简短描述", + "contentLabel": "内容(Markdown格式)", + "contentPlaceholder": "输入skill内容,支持Markdown格式...", + "contentHint": "支持YAML front matter格式(可选)" + }, + "knowledgeItemModal": { + "addKnowledge": "添加知识", + "editKnowledge": "编辑知识", + "category": "分类(风险类型)", + "categoryPlaceholder": "例如:SQL注入", + "title": "标题", + "titlePlaceholder": "知识项标题", + "contentLabel": "内容(Markdown格式)", + "contentPlaceholder": "输入知识内容,支持Markdown格式..." + }, + "batchManageModal": { + "title": "管理对话记录·共{{count}}条", + "searchPlaceholder": "搜索历史记录", + "conversationName": "对话名称", + "lastTime": "最近一次对话时间", + "action": "操作", + "selectAll": "全选", + "deleteSelected": "删除所选", + "confirmDeleteNone": "请先选择要删除的对话", + "confirmDeleteN": "确定要删除选中的 {{count}} 条对话吗?", + "deleteFailed": "删除失败", + "unnamedConversation": "未命名对话" + }, + "createGroupModal": { + "title": "创建分组", + "description": "分组功能可将对话集中归类管理,让对话更加井然有序。", + "selectIcon": "点击选择图标", + "groupNamePlaceholder": "请输入分组名称", + "pickIcon": "选择图标", + "customIcon": "自定义", + "confirmIcon": "确定", + "create": "创建", + "cancel": "取消", + "suggestionPenetrationTest": "渗透测试", + "suggestionCtf": "CTF", + "suggestionRedTeam": "红队", + "suggestionVulnerabilityMining": "漏洞挖掘", + "nameExists": "分组名称已存在,请使用其他名称", + "createFailed": "创建失败", + "unknownError": "未知错误" + }, + "contextMenu": { + "viewAttackChain": "查看攻击链", + "rename": "重命名", + "pinConversation": "置顶此对话", + "unpinConversation": "取消置顶", + "batchManage": "批量管理", + "moveToGroup": "移动到分组", + "deleteConversation": "删除此对话", + "pinGroup": "置顶此分组", + "unpinGroup": "取消置顶", + "deleteGroup": "删除此分组" + }, + "batchImportModal": { + "title": "新建任务", + "queueTitle": "任务标题", + "queueTitlePlaceholder": "请输入任务标题(可选,用于标识和筛选)", + "queueTitleHint": "为批量任务队列设置一个标题,方便后续查找和管理。", + "role": "角色", + "defaultRole": "默认", + "roleHint": "选择一个角色,所有任务将使用该角色的配置(提示词和工具)执行。", + "tasksList": "任务列表(每行一个任务)", + "tasksListPlaceholder": "请输入任务列表,每行一个任务", + "tasksListPlaceholderExample": "请输入任务列表,每行一个任务,例如:\n扫描 192.168.1.1 的开放端口\n检查 https://example.com 是否存在SQL注入\n枚举 example.com 的子域名", + "tasksListHint": "每行输入一个任务指令,系统将依次执行这些任务。空行会被自动忽略。", + "tasksListHintFull": "提示:每行输入一个任务指令,系统将依次执行这些任务。空行会被自动忽略。", + "createQueue": "创建队列" + }, + "batchQueueDetailModal": { + "title": "批量任务队列详情", + "addTask": "添加任务", + "startExecute": "开始执行", + "pauseQueue": "暂停队列", + "deleteQueue": "删除队列", + "queueTitle": "任务标题", + "role": "角色", + "defaultRole": "默认", + "queueId": "队列ID", + "status": "状态", + "createdAt": "创建时间", + "startedAt": "开始时间", + "completedAt": "完成时间", + "taskTotal": "任务总数", + "taskList": "任务列表", + "startLabel": "开始", + "completeLabel": "完成", + "errorLabel": "错误", + "resultLabel": "结果" + }, + "editBatchTaskModal": { + "title": "编辑任务", + "taskMessage": "任务消息", + "taskMessagePlaceholder": "请输入任务消息" + }, + "addBatchTaskModal": { + "title": "添加任务", + "taskMessage": "任务消息", + "taskMessagePlaceholder": "请输入任务消息", + "add": "添加" + }, + "vulnerabilityModal": { + "conversationId": "会话ID", + "conversationIdPlaceholder": "输入会话ID", + "title": "标题", + "titlePlaceholder": "漏洞标题", + "description": "描述", + "descriptionPlaceholder": "漏洞详细描述", + "severity": "严重程度", + "pleaseSelect": "请选择", + "severityCritical": "严重", + "severityHigh": "高危", + "severityMedium": "中危", + "severityLow": "低危", + "severityInfo": "信息", + "status": "状态", + "statusOpen": "待处理", + "statusConfirmed": "已确认", + "statusFixed": "已修复", + "statusFalsePositive": "误报", + "type": "漏洞类型", + "typePlaceholder": "如:SQL注入、XSS、CSRF等", + "target": "目标", + "targetPlaceholder": "受影响的目标(URL、IP地址等)", + "proof": "证明(POC)", + "proofPlaceholder": "漏洞证明,如请求/响应、截图等", + "impact": "影响", + "impactPlaceholder": "漏洞影响说明", + "recommendation": "修复建议", + "recommendationPlaceholder": "修复建议" + }, + "roleModal": { + "addRole": "添加角色", + "editRole": "编辑角色", + "roleName": "角色名称", + "roleNamePlaceholder": "输入角色名称", + "roleDescription": "角色描述", + "roleDescriptionPlaceholder": "输入角色描述", + "roleIcon": "角色图标", + "roleIconPlaceholder": "输入emoji图标,例如: 🏆", + "roleIconHint": "输入一个emoji作为角色的图标,将显示在角色选择器中。", + "userPrompt": "用户提示词", + "userPromptPlaceholder": "输入用户提示词,会在用户消息前追加此提示词...", + "userPromptHint": "此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。", + "relatedTools": "关联的工具(可选)", + "defaultRoleToolsTitle": "默认角色使用所有工具", + "defaultRoleToolsDesc": "默认角色会自动使用MCP管理中启用的所有工具,无需单独配置。", + "searchToolsPlaceholder": "搜索工具...", + "loadingTools": "正在加载工具列表...", + "relatedToolsHint": "勾选要关联的工具,留空则使用MCP管理中的全部工具配置。", + "relatedSkills": "关联的Skills(可选)", + "searchSkillsPlaceholder": "搜索skill...", + "loadingSkills": "正在加载skills列表...", + "relatedSkillsHint": "勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。", + "enableRole": "启用此角色" + } +} diff --git a/web/static/js/auth.js b/web/static/js/auth.js index ab0495f4..4d607b04 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -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 { @@ -375,7 +395,7 @@ async function logout() { // 无论如何都清除本地认证信息 clearAuthStorage(); hideLoginOverlay(); - showLoginOverlay('已退出登录'); + showLoginOverlay(typeof window.t === 'function' ? window.t('auth.loggedOut') : '已退出登录'); } } diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 132053fd..10df6d75 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -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 = '
正在加载工具...
'; + mentionSuggestionsEl.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.loadingTools') : '正在加载工具...') + '
'; mentionSuggestionsEl.style.display = 'block'; delete mentionSuggestionsEl.dataset.lastMentionQuery; return; } if (!mentionFilteredTools.length) { - mentionSuggestionsEl.innerHTML = '
没有匹配的工具
'; + mentionSuggestionsEl.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.noMatchTools') : '没有匹配的工具') + '
'; mentionSuggestionsEl.style.display = 'block'; mentionSuggestionsEl.dataset.lastMentionQuery = currentQuery; return; @@ -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, '
'); } else if (typeof DOMPurify !== 'undefined') { // 直接解析Markdown(代码块会被包裹在/
中,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, '
'); + formattedContent = escapeHtml(rawForParse).replace(/\n/g, '
'); } } else { - formattedContent = escapeHtml(content).replace(/\n/g, '
'); + const rawForEscape = role === 'assistant' ? displayContent : content; + formattedContent = escapeHtml(rawForEscape).replace(/\n/g, '
'); } 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 = '复制'; - copyBtn.title = '复制消息内容'; + copyBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('common.copy') : '复制') + ''; + copyBtn.title = typeof window.t === 'function' ? window.t('chat.copyMessageTitle') : '复制消息内容'; copyBtn.onclick = function(e) { e.stopPropagation(); copyMessageToClipboard(messageDiv, this); @@ -1169,7 +1198,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'); @@ -1192,7 +1221,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 = '展开详情'; + progressDetailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + ''; progressDetailBtn.onclick = () => toggleProcessDetails(progressId, messageDiv.id); buttonsContainer.appendChild(progressDetailBtn); // 存储进度ID到消息元素 @@ -1259,7 +1288,7 @@ function copyMessageToClipboard(messageDiv, button) { function showCopySuccess(button) { if (button) { const originalText = button.innerHTML; - button.innerHTML = '已复制'; + button.innerHTML = '' + (typeof window.t === 'function' ? window.t('common.copied') : '已复制') + ''; button.style.color = '#10b981'; button.style.background = 'rgba(16, 185, 129, 0.1)'; button.style.borderColor = 'rgba(16, 185, 129, 0.3)'; @@ -1301,11 +1330,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 +1349,7 @@ function renderProcessDetails(messageId, processDetails) { if (!processDetailBtn) { processDetailBtn = document.createElement('button'); processDetailBtn.className = 'mcp-detail-btn process-detail-btn'; - processDetailBtn.innerHTML = '展开详情'; + processDetailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + ''; processDetailBtn.onclick = () => toggleProcessDetails(null, messageId); buttonsContainer.appendChild(processDetailBtn); } @@ -1360,7 +1389,7 @@ function renderProcessDetails(messageId, processDetails) { // 如果没有processDetails或为空,显示空状态 if (!processDetails || processDetails.length === 0) { // 显示空状态提示 - timeline.innerHTML = '
暂无过程详情(可能执行过快或未触发详细事件)
'; + timeline.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '
'; // 默认折叠 timeline.classList.remove('expanded'); return; @@ -1425,7 +1454,7 @@ function renderProcessDetails(messageId, processDetails) { // 更新按钮文本为"展开详情" const processDetailBtn = messageElement.querySelector('.process-detail-btn'); if (processDetailBtn) { - processDetailBtn.innerHTML = '展开详情'; + processDetailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + ''; } } } @@ -1679,7 +1708,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(); // 刷新分组列表,清除分组高亮 @@ -2087,7 +2117,8 @@ async function loadConversation(conversationId) { } }); } else { - addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'); + const readyMsgEmpty = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'; + addMessage('assistant', readyMsgEmpty); } // 滚动到底部 @@ -2127,7 +2158,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); } @@ -4171,13 +4203,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 +4242,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 +4369,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 +4482,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 +4677,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 = ` - + 新增分组 + ${addGroupLabel} `; addItem.onclick = () => { showCreateGroupModal(true); @@ -4917,7 +4959,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 +4987,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] || '') + '' + (count || 0) + '' + (parts[1] || ''); +} + async function showBatchManageModal() { try { const response = await apiFetch('/api/conversations?limit=1000'); @@ -4957,10 +5009,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 +5020,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 +5087,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 +5097,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 +5152,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 +5171,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 +5190,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 +5266,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 +5366,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 +5387,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 +5412,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 +5444,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)); } } @@ -5517,10 +5588,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 = '
未找到匹配的对话
'; + list.innerHTML = '
' + (noMatchMsg || '未找到匹配的对话') + '
'; } else { - list.innerHTML = '
该分组暂无对话
'; + list.innerHTML = '
' + (emptyMsg || '该分组暂无对话') + '
'; } return; } @@ -5651,7 +5724,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 +5746,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 +5786,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 +5833,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 +5858,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 +5893,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 +5945,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; } diff --git a/web/static/js/dashboard.js b/web/static/js/dashboard.js index edf17fb1..9dbd1a1b 100644 --- a/web/static/js/dashboard.js +++ b/web/static/js/dashboard.js @@ -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; diff --git a/web/static/js/i18n.js b/web/static/js/i18n.js new file mode 100644 index 00000000..9b75ccf7 --- /dev/null +++ b/web/static/js/i18n.js @@ -0,0 +1,202 @@ +// 前端国际化初始化(基于 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); + + // 仅当未使用 data-i18n-attr 时才替换元素文本内容(否则会覆盖卡片内的数字、子节点等) + // input/textarea:永不设置 textContent(会变成 value),只更新属性 + if (!attrList && !skipText && !isFormControl && text && typeof text === 'string') { + el.textContent = text; + } + + if (attrList) { + attrList.split(',').map(function (s) { return s.trim(); }).forEach(function (attr) { + if (!attr) return; + if (text && typeof text === 'string') { + el.setAttribute(attr, text); + } + }); + } + }); + + // 对话输入框:若 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 = '中文'; + } else { + label.textContent = 'English'; + } + } + + 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 { + 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(); + + // 导出全局函数供其他脚本调用(支持插值参数,如 _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); + }); + }); +})(); + diff --git a/web/static/js/info-collect.js b/web/static/js/info-collect.js index 91fa26ff..a0d0cfa1 100644 --- a/web/static/js/info-collect.js +++ b/web/static/js/info-collect.js @@ -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 ? `
    ${warnings.map(w => `
  • ${escapeHtml(w)}
  • `).join('')}
` - : `
`; + : '
' + _t('infoCollect.none') + '
'; const modal = document.createElement('div'); modal.id = 'fofa-parse-modal'; @@ -345,23 +348,23 @@ function showFofaParseModal(nlText, parsed) { modal.innerHTML = ` `; @@ -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(); +} + diff --git a/web/static/js/knowledge.js b/web/static/js/knowledge.js index 950120df..3457b4bb 100644 --- a/web/static/js/knowledge.js +++ b/web/static/js/knowledge.js @@ -1,4 +1,37 @@ // 知识库管理相关功能 +function _t(key, opts) { + return typeof window.t === 'function' ? window.t(key, opts) : key; +} + +// 返回「知识库未启用」提示区块的 HTML(使用 data-i18n 以便语言切换时自动更新) +function getKnowledgeNotEnabledHTML() { + return ` +
+
📚
+

+

+ +
+ `; +} + +// 渲染「知识库未启用」状态到容器,并应用当前语言 +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 = ` -
-
📚
-

知识库功能未启用

-

${data.message || '请前往系统设置启用知识检索功能'}

- -
- `; - } + // 功能未启用,显示友好提示(使用 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 = ` -
-
📚
-

知识库功能未启用

-

${data.message || '请前往系统设置启用知识检索功能'}

- -
- `; + 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 = ` -
-
📚
-

知识库功能未启用

-

${data.message || '请前往系统设置启用知识检索功能'}

- -
- `; - } + 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 = '
暂无检索记录
'; + container.innerHTML = '
' + _t('retrievalLogs.noRecords') + '
'; retrievalLogsData = []; return; } @@ -1386,7 +1368,7 @@ function renderRetrievalLogs(logs) {
- ${escapeHtml(log.query || '无查询内容')} + ${escapeHtml(log.query || _t('retrievalLogs.noQuery'))}
@@ -1396,33 +1378,33 @@ function renderRetrievalLogs(logs) {
- ${hasResults ? (itemCount > 0 ? `${itemCount} 项` : '有结果') : '无结果'} + ${hasResults ? (itemCount > 0 ? itemCount + ' ' + _t('retrievalLogs.itemsUnit') : _t('retrievalLogs.hasResults')) : _t('retrievalLogs.noResults')}
${log.conversationId ? `
- 对话ID - ${escapeHtml(log.conversationId)} + ${_t('retrievalLogs.conversationId')} + ${escapeHtml(log.conversationId)}
` : ''} ${log.messageId ? `
- 消息ID - ${escapeHtml(log.messageId)} + ${_t('retrievalLogs.messageId')} + ${escapeHtml(log.messageId)}
` : ''}
- 检索结果 + ${_t('retrievalLogs.retrievalResult')} - ${hasResults ? (itemCount > 0 ? `找到 ${itemCount} 个相关知识项` : '找到相关知识项(数量未知)') : '未找到匹配的知识项'} + ${hasResults ? (itemCount > 0 ? _t('retrievalLogs.foundCount', { count: itemCount }) : _t('retrievalLogs.foundUnknown')) : _t('retrievalLogs.noMatch')}
${hasResults && log.retrievedItems && log.retrievedItems.length > 0 ? `
-
检索到的知识项:
+
${_t('retrievalLogs.retrievedItemsLabel')}
${log.retrievedItems.slice(0, 3).map((itemId, idx) => ` ${idx + 1} @@ -1437,13 +1419,13 @@ function renderRetrievalLogs(logs) { - 查看详情 + ${_t('retrievalLogs.viewDetails')} -
@@ -1480,22 +1462,25 @@ function updateRetrievalStats(logs) { statsContainer.innerHTML = `
- 总检索次数 + 总检索次数 ${totalLogs}
- 成功检索 + 成功检索 ${successfulLogs}
- 成功率 + 成功率 ${successRate}%
- 检索到知识项 + 检索到知识项 ${totalItems}
`; + 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 = `
- 总检索次数 + 总检索次数 ${totalLogs}
- 成功检索 + 成功检索 ${successfulLogs}
- 成功率 + 成功率 ${successRate}%
- 检索到知识项 + 检索到知识项 ${totalItems}
`; + 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 = ` `; + if (typeof window.applyTranslations === 'function') { + window.applyTranslations(modal); + } document.body.appendChild(modal); } @@ -1816,57 +1806,57 @@ function showRetrievalLogDetailsModal(log, retrievedItems) { return `
-

${idx + 1}. ${escapeHtml(item.title || '未命名')}

- ${escapeHtml(item.category || '未分类')} +

${idx + 1}. ${escapeHtml(item.title || _t('retrievalLogs.untitled'))}

+ ${escapeHtml(item.category || _t('retrievalLogs.uncategorized'))}
${item.filePath ? `
📁 ${escapeHtml(item.filePath)}
` : ''}
- ${escapeHtml(previewText || '无内容预览')} + ${escapeHtml(previewText || _t('retrievalLogs.noContentPreview'))}
`; }).join(''); } else { - itemsHtml = '
未找到知识项详情
'; + itemsHtml = '
' + _t('retrievalLogs.noItemDetails') + '
'; } content.innerHTML = `
-

查询信息

+

${_t('retrievalLogs.queryInfo')}

-
查询内容:
-
${escapeHtml(log.query || '无查询内容')}
+
${_t('retrievalLogs.queryContent')}
+
${escapeHtml(log.query || _t('retrievalLogs.noQuery'))}
-

检索信息

+

${_t('retrievalLogs.retrievalInfo')}

${log.riskType ? `
-
风险类型
+
${_t('retrievalLogs.riskType')}
${escapeHtml(log.riskType)}
` : ''}
-
检索时间
+
${_t('retrievalLogs.retrievalTime')}
${timeAgo}
-
检索结果
-
${retrievedItems.length} 个知识项
+
${_t('retrievalLogs.retrievalResult')}
+
${_t('retrievalLogs.itemsCount', { count: retrievedItems.length })}
${log.conversationId || log.messageId ? `
-

关联信息

+

${_t('retrievalLogs.relatedInfo')}

${log.conversationId ? `
-
对话ID
+
${_t('retrievalLogs.conversationId')}
${escapeHtml(log.conversationId)} @@ -1874,7 +1864,7 @@ function showRetrievalLogDetailsModal(log, retrievedItems) { ` : ''} ${log.messageId ? `
-
消息ID
+
${_t('retrievalLogs.messageId')}
${escapeHtml(log.messageId)} @@ -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-i18n,applyTranslations 已处理;此处可选地重新应用一次以兼容旧 DOM + var listEl = document.getElementById('knowledge-items-list'); + if (listEl && typeof window.applyTranslations === 'function') { + window.applyTranslations(listEl); + } + } +}); + // 页面切换时加载数据 if (typeof switchPage === 'function') { const originalSwitchPage = switchPage; diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index c86b9aa9..b64535f9 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -1138,10 +1138,10 @@ async function refreshMonitorPanel(page = null) { } catch (error) { console.error('刷新监控面板失败:', error); if (statsContainer) { - statsContainer.innerHTML = `
无法加载统计信息:${escapeHtml(error.message)}
`; + statsContainer.innerHTML = `
${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}:${escapeHtml(error.message)}
`; } if (execContainer) { - execContainer.innerHTML = `
无法加载执行记录:${escapeHtml(error.message)}
`; + execContainer.innerHTML = `
${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadExecutionsError') : '无法加载执行记录')}:${escapeHtml(error.message)}
`; } } } @@ -1215,10 +1215,10 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter = } catch (error) { console.error('刷新监控面板失败:', error); if (statsContainer) { - statsContainer.innerHTML = `
无法加载统计信息:${escapeHtml(error.message)}
`; + statsContainer.innerHTML = `
${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}:${escapeHtml(error.message)}
`; } if (execContainer) { - execContainer.innerHTML = `
无法加载执行记录:${escapeHtml(error.message)}
`; + execContainer.innerHTML = `
${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadExecutionsError') : '无法加载执行记录')}:${escapeHtml(error.message)}
`; } } } @@ -1232,7 +1232,8 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) { const entries = Object.values(statsMap); if (entries.length === 0) { - container.innerHTML = '
暂无统计数据
'; + const noStats = typeof window.t === 'function' ? window.t('mcpMonitor.noStatsData') : '暂无统计数据'; + container.innerHTML = '
' + escapeHtml(noStats) + '
'; return; } @@ -1252,24 +1253,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 = `
-

总调用次数

+

${escapeHtml(totalCallsLabel)}

${totals.total}
-
成功 ${totals.success} / 失败 ${totals.failed}
+
${escapeHtml(successFailedLabel)}
-

成功率

+

${escapeHtml(successRateLabel)}

${successRate}%
-
统计自全部工具调用
+
${escapeHtml(statsFromAll)}
-

最近一次调用

-
${lastCallText}
-
最后刷新时间:${lastUpdatedText}
+

${escapeHtml(lastCallLabel)}

+
${escapeHtml(lastCallText)}
+
${escapeHtml(lastRefreshLabel)}:${escapeHtml(lastUpdatedText)}
`; @@ -1280,14 +1289,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 += `
-

${escapeHtml(tool.toolName || '未知工具')}

+

${escapeHtml(tool.toolName || unknownToolLabel)}

${tool.totalCalls || 0}
- 成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}% + ${escapeHtml(toolMeta)}
`; @@ -1307,10 +1318,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 = '
当前筛选条件下暂无记录
'; + container.innerHTML = '
' + escapeHtml(noRecordsFilter) + '
'; } else { - container.innerHTML = '
暂无执行记录
'; + container.innerHTML = '
' + escapeHtml(noExecutions) + '
'; } // 隐藏批量操作栏 const batchActions = document.getElementById('monitor-batch-actions'); @@ -1322,14 +1335,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 ` @@ -1337,13 +1358,13 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { ${toolName} - ${statusLabel} - ${startTime} - ${duration} + ${escapeHtml(statusLabel)} + ${escapeHtml(startTime)} + ${escapeHtml(duration)}
- - + +
@@ -1365,6 +1386,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 = ` @@ -1372,11 +1398,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') { - - - - - + + + + + ${rows} @@ -1415,12 +1441,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 = `
- 显示 ${startItem}-${endItem} / 共 ${total} 条记录 + ${escapeHtml(paginationInfoText)}
- - - 第 ${page} / ${totalPages} 页 - - + + + ${paginationT('mcp.pageInfo', { page: page, total: totalPages })} + +
`; @@ -693,9 +703,10 @@ async function updateToolsStats() { totalEnabled = currentPageEnabled; } + const tStats = typeof window.t === 'function' ? window.t : (k) => k; statsEl.innerHTML = ` - ✅ 当前页已启用: ${currentPageEnabled} / ${currentPageTotal} - 📊 总计已启用: ${totalEnabled} / ${totalTools} + ✅ ${tStats('mcp.currentPageEnabled')}: ${currentPageEnabled} / ${currentPageTotal} + 📊 ${tStats('mcp.totalEnabled')}: ${totalEnabled} / ${totalTools} `; } @@ -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 = '
📋 暂无外部MCP配置
点击"添加外部MCP"按钮开始配置
'; + const emptyT = typeof window.t === 'function' ? window.t : (k) => k; + list.innerHTML = '
📋 ' + emptyT('mcp.noExternalMCP') + '
' + emptyT('mcp.clickToAddExternal') + '
'; 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) {
${status === 'connected' || status === 'disconnected' || status === 'error' ? - `` : status === 'connecting' ? `` : ''} - - + +
${status === 'error' && server.error ? ` @@ -1217,31 +1246,31 @@ function renderExternalMCPList(servers) { ` : ''}
- 传输模式 + ${statusT('mcp.transportMode')} ${transportIcon} ${escapeHtml(transport.toUpperCase())}
${server.tool_count !== undefined && server.tool_count > 0 ? `
- 工具数量 + ${statusT('mcp.toolCount')} 🔧 ${server.tool_count} 个工具
` : server.tool_count === 0 && status === 'connected' ? `
- 工具数量 - 暂无工具 + ${statusT('mcp.toolCount')} + ${statusT('mcp.noTools')}
` : ''} ${server.config.description ? `
- 描述 + ${statusT('mcp.description')} ${escapeHtml(server.config.description)}
` : ''} ${server.config.timeout ? `
- 超时时间 + ${statusT('mcp.timeout')} ${server.config.timeout} 秒
` : ''} ${transport === 'stdio' && server.config.command ? `
- 命令 + ${statusT('mcp.command')} ${escapeHtml(server.config.command)}
` : ''} ${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 = ` - 📊 总数: ${total} - ✅ 已启用: ${enabled} - ⏸ 已停用: ${disabled} - 🔗 已连接: ${connected} + 📊 ${statsT('mcp.totalCount')}: ${total} + ✅ ${statsT('mcp.enabledCount')}: ${enabled} + ⏸ ${statsT('mcp.disabledCount')}: ${disabled} + 🔗 ${statsT('mcp.connectedCount')}: ${connected} `; } // 显示添加外部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); } } @@ -1528,7 +1558,7 @@ 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; @@ -1539,7 +1569,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 +1588,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 +1656,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 +1709,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 +1731,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列表 diff --git a/web/static/js/tasks.js b/web/static/js/tasks.js index 21746e07..22b8aa19 100644 --- a/web/static/js/tasks.js +++ b/web/static/js/tasks.js @@ -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 = '
加载中...
'; + listContainer.innerHTML = '
' + _t('tasks.loadingTasks') + '
'; 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 = `
-

加载失败: ${escapeHtml(error.message)}

- +

${_t('tasks.loadFailedRetry')}: ${escapeHtml(error.message)}

+
`; } @@ -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 = `
-

当前没有符合条件的任务

+

${_t('tasks.noMatchingTasks')}

${tasksState.allTasks.length === 0 && tasksState.completedTasksHistory.length > 0 ? - '

提示:有已完成的任务历史,请勾选"显示历史记录"查看

' : ''} + '

' + _t('tasks.historyHint') + '

' : ''}
`; 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 += `
- 📜 最近完成的任务(最近24小时) - + 📜 ` + _t('tasks.recentCompletedTasks') + ` +
${historyTasks.map(task => renderTaskItem(task, statusMap, true)).join('')}
`; @@ -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) { ` : '
'} ${status.text} - ${isHistory ? '📜' : ''} - ${escapeHtml(task.message || '未命名任务')} + ${isHistory ? '📜' : ''} + ${escapeHtml(task.message || _t('tasks.unnamedTask'))}
- ${duration ? `⏱ ${duration}` : ''} - + ${duration ? `⏱ ${duration}` : ''} + ${isHistory && completedText ? completedText : timeText} - ${canCancel ? `` : ''} - ${task.conversationId ? `` : ''} + ${canCancel ? `` : ''} + ${task.conversationId ? `` : ''}
${task.conversationId ? `
- 对话ID: - ${escapeHtml(task.conversationId)} + ` + _t('tasks.conversationIdLabel') + `: + ${escapeHtml(task.conversationId)}
` : ''} @@ -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 = ''; + roleSelect.innerHTML = ''; // 添加已启用的角色 const sortedRoles = loadedRoles.sort((a, b) => { @@ -782,7 +785,7 @@ function updateBatchImportStats(text) { const count = lines.length; if (count > 0) { - statsEl.innerHTML = `
共 ${count} 个任务
`; + statsEl.innerHTML = '
' + _t('tasks.taskCount', { count: count }) + '
'; 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 = '

加载失败: ' + escapeHtml(error.message) + '

'; + list.innerHTML = '

' + _t('tasks.loadFailedRetry') + ': ' + escapeHtml(error.message) + '

'; } } } @@ -964,7 +967,7 @@ function renderBatchQueues() { const queues = batchQueuesState.queues; if (queues.length === 0) { - list.innerHTML = '

当前没有批量任务队列

'; + list.innerHTML = '

' + _t('tasks.noBatchQueues') + '

'; 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 = `${roleIcon} ${escapeHtml(roleName)}`; + const roleName = queue.role && queue.role !== '' ? queue.role : _t('batchQueueDetailModal.defaultRole'); + const roleDisplay = `${roleIcon} ${escapeHtml(roleName)}`; return `
@@ -1022,8 +1025,8 @@ function renderBatchQueues() { ${titleDisplay} ${roleDisplay} ${status.text} - 队列ID: ${escapeHtml(queue.id)} - 创建时间: ${new Date(queue.createdAt).toLocaleString('zh-CN')} + ${_t('tasks.queueIdLabel')}: ${escapeHtml(queue.id)} + ${_t('tasks.createdTimeLabel')}: ${new Date(queue.createdAt).toLocaleString()}
@@ -1032,16 +1035,16 @@ function renderBatchQueues() { ${progress}% (${stats.completed + stats.failed + stats.cancelled}/${stats.total})
- ${canDelete ? `` : ''} + ${canDelete ? `` : ''}
- 总计: ${stats.total} - 待执行: ${stats.pending} - 执行中: ${stats.running} - 已完成: ${stats.completed} - 失败: ${stats.failed} - ${stats.cancelled > 0 ? `已取消: ${stats.cancelled}` : ''} + ${_t('tasks.totalLabel')}: ${stats.total} + ${_t('tasks.pendingLabel')}: ${stats.pending} + ${_t('tasks.runningLabel')}: ${stats.running} + ${_t('tasks.completedLabel')}: ${stats.completed} + ${_t('tasks.failedLabel')}: ${stats.failed} + ${stats.cancelled > 0 ? `${_t('tasks.cancelledLabel')}: ${stats.cancelled}` : ''}
`; @@ -1073,9 +1076,9 @@ function renderBatchQueuesPagination() { // 左侧:显示范围信息和每页数量选择器(参考Skills样式) paginationHTML += `
- 显示 ${start}-${end} / 共 ${total} 条 + ` + _t('tasks.paginationShow', { start: start, end: end, total: total }) + `
@@ -32,18 +32,33 @@
-
工具状态开始时间耗时操作${escapeHtml(colTool)}${escapeHtml(colStatus)}${escapeHtml(colStartTime)}${escapeHtml(colDuration)}${escapeHtml(colActions)}
- +
暂无数据
暂无数据
@@ -846,10 +861,10 @@
@@ -857,27 +872,27 @@
-
总漏洞数
+
总漏洞数
-
-
严重
+
严重
-
-
高危
+
高危
-
-
中危
+
中危
-
-
低危
+
低危
-
-
信息
+
信息
-
@@ -887,42 +902,42 @@
- - + +
-
加载中...
+
加载中...
@@ -933,14 +948,14 @@
@@ -949,19 +964,19 @@
@@ -975,17 +990,17 @@
-
加载中...
+
加载中...
@@ -1002,30 +1017,30 @@
-

调用统计

+

调用统计

-
加载中...
+
加载中...
-

Skills调用统计

+

Skills调用统计

- +
-
加载中...
+
加载中...
@@ -1035,17 +1050,17 @@
-
加载中...
+
加载中...
@@ -1063,23 +1078,23 @@
@@ -1089,180 +1104,180 @@
-

基本设置

+

基本设置

-

OpenAI 配置

+

OpenAI 配置

- +
- +
- - + +
-

FOFA 配置

+

FOFA 配置

- - 留空则使用默认地址。 + + 留空则使用默认地址。
- - + +
- - 仅保存在服务器配置中(`config.yaml`)。 + + 仅保存在服务器配置中(`config.yaml`)。
-

Agent 配置

+

Agent 配置

- - + +
-

知识库配置

+

知识库配置

- - - 相对于配置文件所在目录的路径 + + + 相对于配置文件所在目录的路径
-
嵌入模型配置
+
嵌入模型配置
- +
- - 留空则使用OpenAI配置的base_url + + 留空则使用OpenAI配置的base_url
- - 留空则使用OpenAI配置的api_key + + 留空则使用OpenAI配置的api_key
- - + +
-
检索配置
+
检索配置
- - - 检索返回的Top-K结果数量 + + + 检索返回的Top-K结果数量
- - - 相似度阈值(0-1),低于此值的结果将被过滤 + + + 相似度阈值(0-1),低于此值的结果将被过滤
- - - 向量检索的权重(0-1),1.0表示纯向量检索,0.0表示纯关键词检索 + + + 向量检索的权重(0-1),1.0表示纯向量检索,0.0表示纯关键词检索
-
索引配置
+
索引配置
- - - 每个块的最大 token 数(默认 512),长文本会被分割成多个块 + + + 每个块的最大 token 数(默认 512),长文本会被分割成多个块
- - - 块之间的重叠 token 数(默认 50),保持上下文连贯性 + + + 块之间的重叠 token 数(默认 50),保持上下文连贯性
- - - 单个知识项的最大块数量(0 表示不限制),防止单个文件消耗过多 API 配额 + + + 单个知识项的最大块数量(0 表示不限制),防止单个文件消耗过多 API 配额
- - - 每分钟最大请求数(默认 0 表示不限制),如 OpenAI 默认 200 RPM + + + 每分钟最大请求数(默认 0 表示不限制),如 OpenAI 默认 200 RPM
- - - 请求间隔毫秒数(默认 300),用于避免 API 速率限制,设为 0 不限制 + + + 请求间隔毫秒数(默认 300),用于避免 API 速率限制,设为 0 不限制
- - - 最大重试次数(默认 3),遇到速率限制或服务器错误时自动重试 + + + 最大重试次数(默认 3),遇到速率限制或服务器错误时自动重试
- - - 重试间隔毫秒数(默认 1000),每次重试会递增延迟 + + + 重试间隔毫秒数(默认 1000),每次重试会递增延迟
- +
-

机器人设置

-

配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。

+

机器人设置

+

配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。

-

企业微信

+

企业微信

@@ -1290,13 +1305,13 @@
-

钉钉

+

钉钉

@@ -1313,13 +1328,13 @@
-

飞书 (Lark)

+

飞书 (Lark)

@@ -1364,13 +1379,13 @@
-

终端

-

在服务器上执行命令,便于运维与调试。命令在服务端执行,请勿执行敏感或破坏性操作。

+

终端

+

在服务器上执行命令,便于运维与调试。命令在服务端执行,请勿执行敏感或破坏性操作。

-
终端 1
- +
终端 1
+
@@ -1383,28 +1398,28 @@
-

安全设置

+

安全设置

-

修改密码

-

修改登录密码后,需要使用新密码重新登录。

+

修改密码

+

修改登录密码后,需要使用新密码重新登录。

- - + +
- - + +
- - + +
- - + +
@@ -1421,37 +1436,37 @@