diff --git a/internal/handler/openapi.go b/internal/handler/openapi.go index 95ef3814..a3227626 100644 --- a/internal/handler/openapi.go +++ b/internal/handler/openapi.go @@ -4411,6 +4411,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) { }, } + enrichSpecWithI18nKeys(spec) c.JSON(http.StatusOK, spec) } diff --git a/internal/handler/openapi_i18n.go b/internal/handler/openapi_i18n.go new file mode 100644 index 00000000..3479766e --- /dev/null +++ b/internal/handler/openapi_i18n.go @@ -0,0 +1,139 @@ +package handler + +// apiDocI18n 为 OpenAPI 文档提供 x-i18n-* 扩展键,供前端 apiDocs 国际化使用。 +// 前端通过 apiDocs.tags.* / apiDocs.summary.* / apiDocs.response.* 翻译。 + +var apiDocI18nTagToKey = map[string]string{ + "认证": "auth", "对话管理": "conversationManagement", "对话交互": "conversationInteraction", + "批量任务": "batchTasks", "对话分组": "conversationGroups", "漏洞管理": "vulnerabilityManagement", + "角色管理": "roleManagement", "Skills管理": "skillsManagement", "监控": "monitoring", + "配置管理": "configManagement", "外部MCP管理": "externalMCPManagement", "攻击链": "attackChain", + "知识库": "knowledgeBase", "MCP": "mcp", +} + +var apiDocI18nSummaryToKey = map[string]string{ + "用户登录": "login", "用户登出": "logout", "修改密码": "changePassword", "验证Token": "validateToken", + "创建对话": "createConversation", "列出对话": "listConversations", "查看对话详情": "getConversationDetail", + "更新对话": "updateConversation", "删除对话": "deleteConversation", "获取对话结果": "getConversationResult", + "发送消息并获取AI回复(非流式)": "sendMessageNonStream", "发送消息并获取AI回复(流式)": "sendMessageStream", + "取消任务": "cancelTask", "列出运行中的任务": "listRunningTasks", "列出已完成的任务": "listCompletedTasks", + "创建批量任务队列": "createBatchQueue", "列出批量任务队列": "listBatchQueues", "获取批量任务队列": "getBatchQueue", + "删除批量任务队列": "deleteBatchQueue", "启动批量任务队列": "startBatchQueue", "暂停批量任务队列": "pauseBatchQueue", + "添加任务到队列": "addTaskToQueue", "SQL注入扫描": "sqlInjectionScan", "端口扫描": "portScan", + "更新批量任务": "updateBatchTask", "删除批量任务": "deleteBatchTask", + "创建分组": "createGroup", "列出分组": "listGroups", "获取分组": "getGroup", "更新分组": "updateGroup", + "删除分组": "deleteGroup", "获取分组中的对话": "getGroupConversations", "添加对话到分组": "addConversationToGroup", + "从分组移除对话": "removeConversationFromGroup", + "列出漏洞": "listVulnerabilities", "创建漏洞": "createVulnerability", "获取漏洞统计": "getVulnerabilityStats", + "获取漏洞": "getVulnerability", "更新漏洞": "updateVulnerability", "删除漏洞": "deleteVulnerability", + "列出角色": "listRoles", "创建角色": "createRole", "获取角色": "getRole", "更新角色": "updateRole", "删除角色": "deleteRole", + "获取可用Skills列表": "getAvailableSkills", "列出Skills": "listSkills", "创建Skill": "createSkill", + "获取Skill统计": "getSkillStats", "清空Skill统计": "clearSkillStats", "获取Skill": "getSkill", + "更新Skill": "updateSkill", "删除Skill": "deleteSkill", "获取绑定角色": "getBoundRoles", + "获取监控信息": "getMonitorInfo", "获取执行记录": "getExecutionRecords", "删除执行记录": "deleteExecutionRecord", + "批量删除执行记录": "batchDeleteExecutionRecords", "获取统计信息": "getStats", + "获取配置": "getConfig", "更新配置": "updateConfig", "获取工具配置": "getToolConfig", "应用配置": "applyConfig", + "列出外部MCP": "listExternalMCP", "获取外部MCP统计": "getExternalMCPStats", "获取外部MCP": "getExternalMCP", + "添加或更新外部MCP": "addOrUpdateExternalMCP", "stdio模式配置": "stdioModeConfig", "SSE模式配置": "sseModeConfig", + "删除外部MCP": "deleteExternalMCP", "启动外部MCP": "startExternalMCP", "停止外部MCP": "stopExternalMCP", + "获取攻击链": "getAttackChain", "重新生成攻击链": "regenerateAttackChain", + "设置对话置顶": "pinConversation", "设置分组置顶": "pinGroup", "设置分组中对话的置顶": "pinGroupConversation", + "获取分类": "getCategories", "列出知识项": "listKnowledgeItems", "创建知识项": "createKnowledgeItem", + "获取知识项": "getKnowledgeItem", "更新知识项": "updateKnowledgeItem", "删除知识项": "deleteKnowledgeItem", + "获取索引状态": "getIndexStatus", "重建索引": "rebuildIndex", "扫描知识库": "scanKnowledgeBase", + "搜索知识库": "searchKnowledgeBase", "基础搜索": "basicSearch", "按风险类型搜索": "searchByRiskType", + "获取检索日志": "getRetrievalLogs", "删除检索日志": "deleteRetrievalLog", + "MCP端点": "mcpEndpoint", "列出所有工具": "listAllTools", "调用工具": "invokeTool", "初始化连接": "initConnection", + "成功响应": "successResponse", "错误响应": "errorResponse", +} + +var apiDocI18nResponseDescToKey = map[string]string{ + "获取成功": "getSuccess", "未授权": "unauthorized", "未授权,需要有效的Token": "unauthorizedToken", + "创建成功": "createSuccess", "请求参数错误": "badRequest", "对话不存在": "conversationNotFound", + "对话不存在或结果不存在": "conversationOrResultNotFound", "请求参数错误(如task为空)": "badRequestTaskEmpty", + "请求参数错误或分组名称已存在": "badRequestGroupNameExists", "分组不存在": "groupNotFound", + "请求参数错误(如配置格式不正确、缺少必需字段等)": "badRequestConfig", + "请求参数错误(如query为空)": "badRequestQueryEmpty", "方法不允许(仅支持POST请求)": "methodNotAllowed", + "登录成功": "loginSuccess", "密码错误": "invalidPassword", "登出成功": "logoutSuccess", + "密码修改成功": "passwordChanged", "Token有效": "tokenValid", "Token无效或已过期": "tokenInvalid", + "对话创建成功": "conversationCreated", "服务器内部错误": "internalError", "更新成功": "updateSuccess", + "删除成功": "deleteSuccess", "队列不存在": "queueNotFound", "启动成功": "startSuccess", + "暂停成功": "pauseSuccess", "添加成功": "addSuccess", + "任务不存在": "taskNotFound", "对话或分组不存在": "conversationOrGroupNotFound", + "取消请求已提交": "cancelSubmitted", "未找到正在执行的任务": "noRunningTask", + "消息发送成功,返回AI回复": "messageSent", "流式响应(Server-Sent Events)": "streamResponse", +} + +// enrichSpecWithI18nKeys 在 spec 的每个 operation 上写入 x-i18n-tags、x-i18n-summary, +// 在每个 response 上写入 x-i18n-description,供前端按 key 做国际化。 +func enrichSpecWithI18nKeys(spec map[string]interface{}) { + paths, _ := spec["paths"].(map[string]interface{}) + if paths == nil { + return + } + for _, pathItem := range paths { + pm, _ := pathItem.(map[string]interface{}) + if pm == nil { + continue + } + for _, method := range []string{"get", "post", "put", "delete", "patch"} { + opVal, ok := pm[method] + if !ok { + continue + } + op, _ := opVal.(map[string]interface{}) + if op == nil { + continue + } + // x-i18n-tags: 与 tags 一一对应的 i18n 键数组(spec 中 tags 为 []string) + switch tags := op["tags"].(type) { + case []string: + if len(tags) > 0 { + keys := make([]string, 0, len(tags)) + for _, s := range tags { + if k := apiDocI18nTagToKey[s]; k != "" { + keys = append(keys, k) + } else { + keys = append(keys, s) + } + } + op["x-i18n-tags"] = keys + } + case []interface{}: + if len(tags) > 0 { + keys := make([]interface{}, 0, len(tags)) + for _, t := range tags { + if s, ok := t.(string); ok { + if k := apiDocI18nTagToKey[s]; k != "" { + keys = append(keys, k) + } else { + keys = append(keys, s) + } + } + } + if len(keys) > 0 { + op["x-i18n-tags"] = keys + } + } + } + // x-i18n-summary + if summary, _ := op["summary"].(string); summary != "" { + if k := apiDocI18nSummaryToKey[summary]; k != "" { + op["x-i18n-summary"] = k + } + } + // responses -> 每个 status -> x-i18n-description + if respMap, _ := op["responses"].(map[string]interface{}); respMap != nil { + for _, rv := range respMap { + if r, _ := rv.(map[string]interface{}); r != nil { + if desc, _ := r["description"].(string); desc != "" { + if k := apiDocI18nResponseDescToKey[desc]; k != "" { + r["x-i18n-description"] = k + } + } + } + } + } + } + } +} diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 949eea63..38105db0 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1,4 +1,8 @@ { + "lang": { + "zhCN": "中文", + "enUS": "English" + }, "common": { "ok": "OK", "cancel": "Cancel", @@ -132,10 +136,51 @@ "executeFailed": "Execution failed", "callOpenAIFailed": "Call OpenAI failed", "systemReadyMessage": "System is ready. Please enter your test requirements, and the system will automatically perform the corresponding security tests.", - "addNewGroup": "+ New group" + "addNewGroup": "+ New group", + "callNumber": "Call #{{n}}", + "iterationRound": "Iteration {{n}}", + "aiThinking": "AI thinking", + "toolCallsDetected": "Detected {{count}} tool call(s)", + "callTool": "Call tool: {{name}} ({{index}}/{{total}})", + "toolExecComplete": "Tool {{name}} completed", + "toolExecFailed": "Tool {{name}} failed", + "knowledgeRetrieval": "Knowledge retrieval", + "knowledgeRetrievalTag": "Knowledge retrieval", + "error": "Error", + "taskCancelled": "Task cancelled", + "unknownTool": "Unknown tool", + "noDescription": "No description", + "noResponseData": "No response data", + "loading": "Loading...", + "loadFailed": "Load failed: {{message}}", + "noAttackChainData": "No attack chain data", + "copyFailedManual": "Copy failed, please select and copy manually", + "searching": "Searching...", + "loadFailedRetry": "Load failed, please retry", + "dataFormatError": "Data format error", + "progressInProgress": "Penetration test in progress..." + }, + "progress": { + "callingAI": "Calling AI model...", + "callingTool": "Calling tool: {{name}}", + "lastIterSummary": "Last iteration: generating summary and next steps...", + "summaryDone": "Summary complete", + "generatingFinalReply": "Generating final reply...", + "maxIterSummary": "Max iterations reached, generating summary..." + }, + "timeline": { + "params": "Parameters:", + "executionResult": "Execution result:", + "executionId": "Execution ID:", + "noResult": "No result", + "running": "Running...", + "completed": "Completed", + "execFailed": "Execution failed" }, "tasks": { "title": "Task management", + "stopTask": "Stop task", + "collapseDetail": "Collapse details", "newTask": "New task", "autoRefresh": "Auto refresh", "historyHint": "Tip: Completed task history available. Check \"Show history\" to view.", @@ -500,7 +545,214 @@ "loadCallStatsError": "Failed to load call statistics" }, "apiDocs": { - "curlCopied": "curl command copied to clipboard!" + "pageTitle": "API Docs - CyberStrikeAI", + "title": "API Docs", + "subtitle": "CyberStrikeAI platform API documentation with online testing", + "authTitle": "API Authentication", + "authAllNeedToken": "All API endpoints require Token authentication.", + "authGetToken": "1. Get Token:", + "authGetTokenDesc": "After logging in on the frontend, the Token is saved automatically. You can also get it via:", + "authUseToken": "2. Use Token:", + "authUseTokenDesc": "Add the Authorization header:", + "authTip": "💡 This page will use your logged-in Token automatically; no need to fill it manually.", + "tokenDetected": "✓ Token detected - You can test API endpoints directly", + "tokenNotDetected": "⚠ No Token detected - Please log in on the frontend first, then refresh this page. When testing, add Authorization: Bearer token in the request header", + "sidebarGroupTitle": "API Groups", + "allApis": "All APIs", + "loading": "Loading...", + "loadingDesc": "Loading API documentation", + "errorLoginRequired": "Login required to view API docs. Please log in on the frontend first, then refresh this page.", + "errorLoadSpec": "Failed to load API spec: ", + "errorLoadFailed": "Failed to load API docs: ", + "errorSpecInvalid": "Invalid API spec format", + "loadFailed": "Load failed", + "backToLogin": "Back to login", + "noApis": "No APIs", + "noEndpointsInGroup": "No API endpoints in this group", + "sectionDescription": "Description", + "viewDetailDesc": "View details", + "hideDetailDesc": "Hide details", + "noDescription": "No description", + "sectionParams": "Parameters", + "paramName": "Parameter", + "type": "Type", + "description": "Description", + "required": "Required", + "optional": "Optional", + "sectionRequestBody": "Request body", + "example": "Example", + "exampleJson": "Example JSON:", + "sectionResponse": "Response", + "testSection": "Test", + "requestBodyJson": "Request body (JSON)", + "queryParams": "Query parameters:", + "sendRequest": "Send request", + "copyCurl": "Copy cURL", + "clearResult": "Clear result", + "copyCurlTitle": "Copy cURL command", + "clearResultTitle": "Clear test result", + "sendingRequest": "Sending request...", + "errorPathParamRequired": "Path parameter {{name}} is required", + "errorQueryParamRequired": "Query parameter {{name}} is required", + "errorTokenRequired": "No Token detected. Please log in on the frontend and refresh, or add Authorization: Bearer your_token in the request header", + "errorJsonInvalid": "Invalid request body JSON: ", + "requestFailed": "Request failed: ", + "copied": "Copied", + "curlCopied": "curl command copied to clipboard!", + "copyFailedManual": "Copy failed, please copy manually:\n\n", + "curlGenFailed": "Failed to generate cURL command: ", + "requestBodyPlaceholder": "Enter request body in JSON format", + "tags": { + "auth": "Authentication", + "conversationManagement": "Conversation Management", + "conversationInteraction": "Conversation Interaction", + "batchTasks": "Batch Tasks", + "conversationGroups": "Conversation Groups", + "vulnerabilityManagement": "Vulnerability Management", + "roleManagement": "Role Management", + "skillsManagement": "Skills Management", + "monitoring": "Monitoring", + "configManagement": "Configuration Management", + "externalMCPManagement": "External MCP Management", + "attackChain": "Attack Chain", + "knowledgeBase": "Knowledge Base", + "mcp": "MCP" + }, + "summary": { + "login": "User login", + "logout": "User logout", + "changePassword": "Change password", + "validateToken": "Validate Token", + "createConversation": "Create conversation", + "listConversations": "List conversations", + "getConversationDetail": "Get conversation detail", + "updateConversation": "Update conversation", + "deleteConversation": "Delete conversation", + "getConversationResult": "Get conversation result", + "sendMessageNonStream": "Send message and get AI reply (non-stream)", + "sendMessageStream": "Send message and get AI reply (stream)", + "cancelTask": "Cancel task", + "listRunningTasks": "List running tasks", + "listCompletedTasks": "List completed tasks", + "createBatchQueue": "Create batch task queue", + "listBatchQueues": "List batch task queues", + "getBatchQueue": "Get batch task queue", + "deleteBatchQueue": "Delete batch task queue", + "startBatchQueue": "Start batch task queue", + "pauseBatchQueue": "Pause batch task queue", + "addTaskToQueue": "Add task to queue", + "sqlInjectionScan": "SQL injection scan", + "portScan": "Port scan", + "updateBatchTask": "Update batch task", + "deleteBatchTask": "Delete batch task", + "createGroup": "Create group", + "listGroups": "List groups", + "getGroup": "Get group", + "updateGroup": "Update group", + "deleteGroup": "Delete group", + "getGroupConversations": "Get conversations in group", + "addConversationToGroup": "Add conversation to group", + "removeConversationFromGroup": "Remove conversation from group", + "listVulnerabilities": "List vulnerabilities", + "createVulnerability": "Create vulnerability", + "getVulnerabilityStats": "Get vulnerability statistics", + "getVulnerability": "Get vulnerability", + "updateVulnerability": "Update vulnerability", + "deleteVulnerability": "Delete vulnerability", + "listRoles": "List roles", + "createRole": "Create role", + "getRole": "Get role", + "updateRole": "Update role", + "deleteRole": "Delete role", + "getAvailableSkills": "Get available Skills list", + "listSkills": "List Skills", + "createSkill": "Create Skill", + "getSkillStats": "Get Skill statistics", + "clearSkillStats": "Clear Skill statistics", + "getSkill": "Get Skill", + "updateSkill": "Update Skill", + "deleteSkill": "Delete Skill", + "getBoundRoles": "Get bound roles", + "clearSkillStatsAlt": "Clear Skill statistics", + "getMonitorInfo": "Get monitoring info", + "getExecutionRecords": "Get execution records", + "deleteExecutionRecord": "Delete execution record", + "batchDeleteExecutionRecords": "Batch delete execution records", + "getStats": "Get statistics", + "getConfig": "Get configuration", + "updateConfig": "Update configuration", + "getToolConfig": "Get tool configuration", + "applyConfig": "Apply configuration", + "listExternalMCP": "List external MCP", + "getExternalMCPStats": "Get external MCP statistics", + "getExternalMCP": "Get external MCP", + "addOrUpdateExternalMCP": "Add or update external MCP", + "stdioModeConfig": "stdio mode config", + "sseModeConfig": "SSE mode config", + "deleteExternalMCP": "Delete external MCP", + "startExternalMCP": "Start external MCP", + "stopExternalMCP": "Stop external MCP", + "getAttackChain": "Get attack chain", + "regenerateAttackChain": "Regenerate attack chain", + "pinConversation": "Pin conversation", + "pinGroup": "Pin group", + "pinGroupConversation": "Pin conversation in group", + "getCategories": "Get categories", + "listKnowledgeItems": "List knowledge items", + "createKnowledgeItem": "Create knowledge item", + "getKnowledgeItem": "Get knowledge item", + "updateKnowledgeItem": "Update knowledge item", + "deleteKnowledgeItem": "Delete knowledge item", + "getIndexStatus": "Get index status", + "rebuildIndex": "Rebuild index", + "scanKnowledgeBase": "Scan knowledge base", + "searchKnowledgeBase": "Search knowledge base", + "basicSearch": "Basic search", + "searchByRiskType": "Search by risk type", + "getRetrievalLogs": "Get retrieval logs", + "deleteRetrievalLog": "Delete retrieval log", + "mcpEndpoint": "MCP endpoint", + "listAllTools": "List all tools", + "invokeTool": "Invoke tool", + "initConnection": "Initialize connection", + "successResponse": "Success response", + "errorResponse": "Error response" + }, + "response": { + "getSuccess": "Success", + "unauthorized": "Unauthorized", + "unauthorizedToken": "Unauthorized, valid Token required", + "createSuccess": "Created successfully", + "badRequest": "Bad request", + "conversationNotFound": "Conversation not found", + "conversationOrResultNotFound": "Conversation or result not found", + "badRequestTaskEmpty": "Bad request (e.g. task is empty)", + "badRequestGroupNameExists": "Bad request or group name already exists", + "groupNotFound": "Group not found", + "badRequestConfig": "Bad request (e.g. invalid config or missing required fields)", + "badRequestQueryEmpty": "Bad request (e.g. query is empty)", + "methodNotAllowed": "Method not allowed (POST only)", + "loginSuccess": "Login successful", + "invalidPassword": "Invalid password", + "logoutSuccess": "Logout successful", + "passwordChanged": "Password changed successfully", + "tokenValid": "Token valid", + "tokenInvalid": "Token invalid or expired", + "conversationCreated": "Conversation created", + "internalError": "Internal server error", + "updateSuccess": "Updated successfully", + "deleteSuccess": "Deleted successfully", + "queueNotFound": "Queue not found", + "startSuccess": "Started successfully", + "pauseSuccess": "Paused successfully", + "addSuccess": "Added successfully", + "taskNotFound": "Task not found", + "conversationOrGroupNotFound": "Conversation or group not found", + "cancelSubmitted": "Cancel request submitted", + "noRunningTask": "No running task found", + "messageSent": "Message sent, AI reply returned", + "streamResponse": "Stream response (Server-Sent Events)" + } }, "chatGroup": { "search": "Search", @@ -799,6 +1051,13 @@ "execInfo": "Execution info", "tool": "Tool", "status": "Status", + "statusPending": "Pending", + "statusRunning": "Running", + "statusCompleted": "Completed", + "statusFailed": "Failed", + "unknown": "Unknown", + "getDetailFailed": "Failed to get details", + "execSuccessNoContent": "Execution succeeded with no displayable content.", "time": "Time", "executionId": "Execution ID", "requestParams": "Request params", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 072de55d..637db159 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1,4 +1,8 @@ { + "lang": { + "zhCN": "中文", + "enUS": "English" + }, "common": { "ok": "确定", "cancel": "取消", @@ -132,10 +136,51 @@ "executeFailed": "执行失败", "callOpenAIFailed": "调用OpenAI失败", "systemReadyMessage": "系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。", - "addNewGroup": "+ 新增分组" + "addNewGroup": "+ 新增分组", + "callNumber": "调用 #{{n}}", + "iterationRound": "第 {{n}} 轮迭代", + "aiThinking": "AI思考", + "toolCallsDetected": "检测到 {{count}} 个工具调用", + "callTool": "调用工具: {{name}} ({{index}}/{{total}})", + "toolExecComplete": "工具 {{name}} 执行完成", + "toolExecFailed": "工具 {{name}} 执行失败", + "knowledgeRetrieval": "知识检索", + "knowledgeRetrievalTag": "知识检索", + "error": "错误", + "taskCancelled": "任务已取消", + "unknownTool": "未知工具", + "noDescription": "暂无描述", + "noResponseData": "暂无响应数据", + "loading": "加载中...", + "loadFailed": "加载失败: {{message}}", + "noAttackChainData": "暂无攻击链数据", + "copyFailedManual": "复制失败,请手动选择内容复制", + "searching": "搜索中...", + "loadFailedRetry": "加载失败,请重试", + "dataFormatError": "数据格式错误", + "progressInProgress": "渗透测试进行中..." + }, + "progress": { + "callingAI": "正在调用AI模型...", + "callingTool": "正在调用工具: {{name}}", + "lastIterSummary": "最后一次迭代:正在生成总结和下一步计划...", + "summaryDone": "总结生成完成", + "generatingFinalReply": "正在生成最终回复...", + "maxIterSummary": "达到最大迭代次数,正在生成总结..." + }, + "timeline": { + "params": "参数:", + "executionResult": "执行结果:", + "executionId": "执行ID:", + "noResult": "无结果", + "running": "执行中...", + "completed": "已完成", + "execFailed": "执行失败" }, "tasks": { "title": "任务管理", + "stopTask": "停止任务", + "collapseDetail": "收起详情", "newTask": "新建任务", "autoRefresh": "自动刷新", "historyHint": "提示:有已完成的任务历史,请勾选\"显示历史记录\"查看", @@ -500,7 +545,214 @@ "loadCallStatsError": "无法加载调用统计" }, "apiDocs": { - "curlCopied": "curl命令已复制到剪贴板!" + "pageTitle": "API 文档 - CyberStrikeAI", + "title": "API 文档", + "subtitle": "CyberStrikeAI 平台 API 接口文档,支持在线测试", + "authTitle": "API 认证说明", + "authAllNeedToken": "所有 API 接口都需要 Token 认证。", + "authGetToken": "1. 获取 Token:", + "authGetTokenDesc": "在前端页面登录后,Token 会自动保存。您也可以通过以下方式获取:", + "authUseToken": "2. 使用 Token:", + "authUseTokenDesc": "在请求头中添加 Authorization 字段:", + "authTip": "💡 提示:本页面会自动使用您已登录的 Token,无需手动填写。", + "tokenDetected": "✓ 已检测到 Token - 您可以直接测试 API 接口", + "tokenNotDetected": "⚠ 未检测到 Token - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token", + "sidebarGroupTitle": "API 分组", + "allApis": "全部接口", + "loading": "加载中...", + "loadingDesc": "正在加载 API 文档", + "errorLoginRequired": "需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。", + "errorLoadSpec": "加载API规范失败: ", + "errorLoadFailed": "加载API文档失败: ", + "errorSpecInvalid": "API规范格式错误", + "loadFailed": "加载失败", + "backToLogin": "返回首页登录", + "noApis": "暂无API", + "noEndpointsInGroup": "该分组下没有API端点", + "sectionDescription": "描述", + "viewDetailDesc": "查看详细说明", + "hideDetailDesc": "隐藏详细说明", + "noDescription": "无描述", + "sectionParams": "参数", + "paramName": "参数名", + "type": "类型", + "description": "描述", + "required": "必需", + "optional": "可选", + "sectionRequestBody": "请求体", + "example": "示例", + "exampleJson": "示例JSON:", + "sectionResponse": "响应", + "testSection": "测试接口", + "requestBodyJson": "请求体 (JSON)", + "queryParams": "查询参数:", + "sendRequest": "发送请求", + "copyCurl": "复制curl", + "clearResult": "清除结果", + "copyCurlTitle": "复制curl命令", + "clearResultTitle": "清除测试结果", + "sendingRequest": "发送请求中...", + "errorPathParamRequired": "路径参数 {{name}} 不能为空", + "errorQueryParamRequired": "查询参数 {{name}} 不能为空", + "errorTokenRequired": "未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token", + "errorJsonInvalid": "请求体JSON格式错误: ", + "requestFailed": "请求失败: ", + "copied": "已复制", + "curlCopied": "curl命令已复制到剪贴板!", + "copyFailedManual": "复制失败,请手动复制:\n\n", + "curlGenFailed": "生成curl命令失败: ", + "requestBodyPlaceholder": "请输入JSON格式的请求体", + "tags": { + "auth": "认证", + "conversationManagement": "对话管理", + "conversationInteraction": "对话交互", + "batchTasks": "批量任务", + "conversationGroups": "对话分组", + "vulnerabilityManagement": "漏洞管理", + "roleManagement": "角色管理", + "skillsManagement": "Skills管理", + "monitoring": "监控", + "configManagement": "配置管理", + "externalMCPManagement": "外部MCP管理", + "attackChain": "攻击链", + "knowledgeBase": "知识库", + "mcp": "MCP" + }, + "summary": { + "login": "用户登录", + "logout": "用户登出", + "changePassword": "修改密码", + "validateToken": "验证Token", + "createConversation": "创建对话", + "listConversations": "列出对话", + "getConversationDetail": "查看对话详情", + "updateConversation": "更新对话", + "deleteConversation": "删除对话", + "getConversationResult": "获取对话结果", + "sendMessageNonStream": "发送消息并获取AI回复(非流式)", + "sendMessageStream": "发送消息并获取AI回复(流式)", + "cancelTask": "取消任务", + "listRunningTasks": "列出运行中的任务", + "listCompletedTasks": "列出已完成的任务", + "createBatchQueue": "创建批量任务队列", + "listBatchQueues": "列出批量任务队列", + "getBatchQueue": "获取批量任务队列", + "deleteBatchQueue": "删除批量任务队列", + "startBatchQueue": "启动批量任务队列", + "pauseBatchQueue": "暂停批量任务队列", + "addTaskToQueue": "添加任务到队列", + "sqlInjectionScan": "SQL注入扫描", + "portScan": "端口扫描", + "updateBatchTask": "更新批量任务", + "deleteBatchTask": "删除批量任务", + "createGroup": "创建分组", + "listGroups": "列出分组", + "getGroup": "获取分组", + "updateGroup": "更新分组", + "deleteGroup": "删除分组", + "getGroupConversations": "获取分组中的对话", + "addConversationToGroup": "添加对话到分组", + "removeConversationFromGroup": "从分组移除对话", + "listVulnerabilities": "列出漏洞", + "createVulnerability": "创建漏洞", + "getVulnerabilityStats": "获取漏洞统计", + "getVulnerability": "获取漏洞", + "updateVulnerability": "更新漏洞", + "deleteVulnerability": "删除漏洞", + "listRoles": "列出角色", + "createRole": "创建角色", + "getRole": "获取角色", + "updateRole": "更新角色", + "deleteRole": "删除角色", + "getAvailableSkills": "获取可用Skills列表", + "listSkills": "列出Skills", + "createSkill": "创建Skill", + "getSkillStats": "获取Skill统计", + "clearSkillStats": "清空Skill统计", + "getSkill": "获取Skill", + "updateSkill": "更新Skill", + "deleteSkill": "删除Skill", + "getBoundRoles": "获取绑定角色", + "clearSkillStatsAlt": "清空Skill统计", + "getMonitorInfo": "获取监控信息", + "getExecutionRecords": "获取执行记录", + "deleteExecutionRecord": "删除执行记录", + "batchDeleteExecutionRecords": "批量删除执行记录", + "getStats": "获取统计信息", + "getConfig": "获取配置", + "updateConfig": "更新配置", + "getToolConfig": "获取工具配置", + "applyConfig": "应用配置", + "listExternalMCP": "列出外部MCP", + "getExternalMCPStats": "获取外部MCP统计", + "getExternalMCP": "获取外部MCP", + "addOrUpdateExternalMCP": "添加或更新外部MCP", + "stdioModeConfig": "stdio模式配置", + "sseModeConfig": "SSE模式配置", + "deleteExternalMCP": "删除外部MCP", + "startExternalMCP": "启动外部MCP", + "stopExternalMCP": "停止外部MCP", + "getAttackChain": "获取攻击链", + "regenerateAttackChain": "重新生成攻击链", + "pinConversation": "设置对话置顶", + "pinGroup": "设置分组置顶", + "pinGroupConversation": "设置分组中对话的置顶", + "getCategories": "获取分类", + "listKnowledgeItems": "列出知识项", + "createKnowledgeItem": "创建知识项", + "getKnowledgeItem": "获取知识项", + "updateKnowledgeItem": "更新知识项", + "deleteKnowledgeItem": "删除知识项", + "getIndexStatus": "获取索引状态", + "rebuildIndex": "重建索引", + "scanKnowledgeBase": "扫描知识库", + "searchKnowledgeBase": "搜索知识库", + "basicSearch": "基础搜索", + "searchByRiskType": "按风险类型搜索", + "getRetrievalLogs": "获取检索日志", + "deleteRetrievalLog": "删除检索日志", + "mcpEndpoint": "MCP端点", + "listAllTools": "列出所有工具", + "invokeTool": "调用工具", + "initConnection": "初始化连接", + "successResponse": "成功响应", + "errorResponse": "错误响应" + }, + "response": { + "getSuccess": "获取成功", + "unauthorized": "未授权", + "unauthorizedToken": "未授权,需要有效的Token", + "createSuccess": "创建成功", + "badRequest": "请求参数错误", + "conversationNotFound": "对话不存在", + "conversationOrResultNotFound": "对话不存在或结果不存在", + "badRequestTaskEmpty": "请求参数错误(如task为空)", + "badRequestGroupNameExists": "请求参数错误或分组名称已存在", + "groupNotFound": "分组不存在", + "badRequestConfig": "请求参数错误(如配置格式不正确、缺少必需字段等)", + "badRequestQueryEmpty": "请求参数错误(如query为空)", + "methodNotAllowed": "方法不允许(仅支持POST请求)", + "loginSuccess": "登录成功", + "invalidPassword": "密码错误", + "logoutSuccess": "登出成功", + "passwordChanged": "密码修改成功", + "tokenValid": "Token有效", + "tokenInvalid": "Token无效或已过期", + "conversationCreated": "对话创建成功", + "internalError": "服务器内部错误", + "updateSuccess": "更新成功", + "deleteSuccess": "删除成功", + "queueNotFound": "队列不存在", + "startSuccess": "启动成功", + "pauseSuccess": "暂停成功", + "addSuccess": "添加成功", + "taskNotFound": "任务不存在", + "conversationOrGroupNotFound": "对话或分组不存在", + "cancelSubmitted": "取消请求已提交", + "noRunningTask": "未找到正在执行的任务", + "messageSent": "消息发送成功,返回AI回复", + "streamResponse": "流式响应(Server-Sent Events)" + } }, "chatGroup": { "search": "搜索", @@ -799,6 +1051,13 @@ "execInfo": "执行信息", "tool": "工具", "status": "状态", + "statusPending": "等待中", + "statusRunning": "执行中", + "statusCompleted": "已完成", + "statusFailed": "失败", + "unknown": "未知", + "getDetailFailed": "获取详情失败", + "execSuccessNoContent": "执行成功,未返回可展示的文本内容。", "time": "时间", "executionId": "执行 ID", "requestParams": "请求参数", diff --git a/web/static/js/api-docs.js b/web/static/js/api-docs.js index be7266c8..2e0ae1db 100644 --- a/web/static/js/api-docs.js +++ b/web/static/js/api-docs.js @@ -3,13 +3,74 @@ let apiSpec = null; let currentToken = null; +function _t(key, opts) { + return typeof window.t === 'function' ? window.t(key, opts) : key; +} + +function waitForI18n() { + return new Promise(function (resolve) { + if (window.t) return resolve(); + var n = 0; + var iv = setInterval(function () { + if (window.t) { clearInterval(iv); resolve(); return; } + n++; + if (n >= 100) { clearInterval(iv); resolve(); } + }, 50); + }); +} + +// 从 OpenAPI spec 的 x-i18n-tags 构建 tag -> i18n key 映射(方案 A:后端提供键) +var apiSpecTagToKey = {}; +function buildApiSpecTagToKey() { + apiSpecTagToKey = {}; + if (!apiSpec || !apiSpec.paths) return; + Object.keys(apiSpec.paths).forEach(function (path) { + var pathItem = apiSpec.paths[path]; + if (!pathItem || typeof pathItem !== 'object') return; + ['get', 'post', 'put', 'delete', 'patch'].forEach(function (method) { + var op = pathItem[method]; + if (!op || !op.tags || !op['x-i18n-tags']) return; + var tags = op.tags; + var keys = op['x-i18n-tags']; + for (var i = 0; i < tags.length && i < keys.length; i++) { + apiSpecTagToKey[tags[i]] = typeof keys[i] === 'string' ? keys[i] : keys[i]; + } + }); + }); +} +function translateApiDocTag(tag) { + if (!tag) return tag; + var key = apiSpecTagToKey[tag]; + return key ? _t('apiDocs.tags.' + key) : tag; +} +function translateApiDocSummaryFromOp(op) { + var key = op && op['x-i18n-summary']; + if (key) return _t('apiDocs.summary.' + key); + return op && op.summary ? op.summary : ''; +} +function translateApiDocResponseDescFromResp(resp) { + if (!resp) return ''; + var key = resp['x-i18n-description']; + if (key) return _t('apiDocs.response.' + key); + return resp.description || ''; +} + // 初始化 document.addEventListener('DOMContentLoaded', async () => { + await waitForI18n(); await loadToken(); await loadAPISpec(); if (apiSpec) { renderAPIDocs(); } + document.addEventListener('languagechange', function () { + if (typeof window.applyTranslations === 'function') { + window.applyTranslations(document); + } + if (apiSpec) { + renderAPIDocs(); + } + }); }); // 加载token @@ -43,22 +104,25 @@ async function loadAPISpec() { const response = await fetch(url); if (!response.ok) { if (response.status === 401) { - showError('需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。'); + showError(_t('apiDocs.errorLoginRequired')); return; } - throw new Error('加载API规范失败: ' + response.status); + throw new Error(_t('apiDocs.errorLoadSpec') + response.status); } apiSpec = await response.json(); + buildApiSpecTagToKey(); } catch (error) { console.error('加载API规范失败:', error); - showError('加载API文档失败: ' + error.message); + showError(_t('apiDocs.errorLoadFailed') + error.message); } } // 显示错误 function showError(message) { const main = document.getElementById('api-docs-main'); + const loadFailed = _t('apiDocs.loadFailed'); + const backToLogin = _t('apiDocs.backToLogin'); main.innerHTML = `
@@ -66,10 +130,10 @@ function showError(message) { -

加载失败

-

${message}

+

${escapeHtml(loadFailed)}

+

${escapeHtml(message)}

- 返回首页登录 + ${escapeHtml(backToLogin)}
`; @@ -78,7 +142,7 @@ function showError(message) { // 渲染API文档 function renderAPIDocs() { if (!apiSpec || !apiSpec.paths) { - showError('API规范格式错误'); + showError(_t('apiDocs.errorSpecInvalid')); return; } @@ -109,7 +173,7 @@ function renderAuthInfo() { tokenStatus.style.display = 'block'; tokenStatus.style.background = 'rgba(255, 152, 0, 0.1)'; tokenStatus.style.borderLeftColor = '#ff9800'; - tokenStatus.innerHTML = '

⚠ 未检测到 Token - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token

'; + tokenStatus.innerHTML = '

' + escapeHtml(_t('apiDocs.tokenNotDetected')) + '

'; } } @@ -127,11 +191,14 @@ function renderSidebar() { const groupList = document.getElementById('api-group-list'); const allGroups = Array.from(groups).sort(); - + while (groupList.children.length > 1) { + groupList.removeChild(groupList.lastChild); + } allGroups.forEach(group => { const li = document.createElement('li'); li.className = 'api-group-item'; - li.innerHTML = `${group}`; + const groupLabel = translateApiDocTag(group); + li.innerHTML = `${escapeHtml(groupLabel)}`; groupList.appendChild(li); }); @@ -176,7 +243,7 @@ function renderEndpoints(filterGroup = null) { }); if (endpoints.length === 0) { - main.innerHTML = '

暂无API

该分组下没有API端点

'; + main.innerHTML = '

' + escapeHtml(_t('apiDocs.noApis')) + '

' + escapeHtml(_t('apiDocs.noEndpointsInGroup')) + '

'; return; } @@ -192,8 +259,8 @@ function createEndpointCard(endpoint) { const methodClass = endpoint.method.toLowerCase(); const tags = endpoint.tags || []; - const tagHtml = tags.map(tag => `${tag}`).join(''); - + const tagHtml = tags.map(tag => `${escapeHtml(translateApiDocTag(tag))}`).join(''); + const summaryText = translateApiDocSummaryFromOp(endpoint); card.innerHTML = `
@@ -204,21 +271,21 @@ function createEndpointCard(endpoint) {
-
描述
- ${endpoint.summary ? `
${escapeHtml(endpoint.summary)}
` : ''} +
${escapeHtml(_t('apiDocs.sectionDescription'))}
+ ${summaryText ? `
${escapeHtml(summaryText)}
` : ''} ${endpoint.description ? `
- ` : endpoint.summary ? '' : '
无描述
'} + ` : endpoint.summary ? '' : '
' + escapeHtml(_t('apiDocs.noDescription')) + '
'}
${renderParameters(endpoint)} @@ -236,8 +303,10 @@ function renderParameters(endpoint) { const params = endpoint.parameters || []; if (params.length === 0) return ''; + const requiredLabel = escapeHtml(_t('apiDocs.required')); + const optionalLabel = escapeHtml(_t('apiDocs.optional')); const rows = params.map(param => { - const required = param.required ? '必需' : '可选'; + const required = param.required ? '' + requiredLabel + '' : '' + optionalLabel + ''; // 处理描述文本,将换行符转换为
let descriptionHtml = '-'; if (param.description) { @@ -255,17 +324,20 @@ function renderParameters(endpoint) { `; }).join(''); + const paramName = escapeHtml(_t('apiDocs.paramName')); + const typeLabel = escapeHtml(_t('apiDocs.type')); + const descLabel = escapeHtml(_t('apiDocs.description')); return `
-
参数
+
${escapeHtml(_t('apiDocs.sectionParams'))}
- - - - + + + + @@ -297,11 +369,13 @@ function renderRequestBody(endpoint) { let paramsTable = ''; if (schema.properties) { const requiredFields = schema.required || []; + const reqLabel = escapeHtml(_t('apiDocs.required')); + const optLabel = escapeHtml(_t('apiDocs.optional')); const rows = Object.keys(schema.properties).map(key => { const prop = schema.properties[key]; const required = requiredFields.includes(key) - ? '必需' - : '可选'; + ? '' + reqLabel + '' + : '' + optLabel + ''; // 处理嵌套类型 let typeDisplay = prop.type || 'object'; @@ -338,16 +412,20 @@ function renderRequestBody(endpoint) { }).join(''); if (rows) { + const pName = escapeHtml(_t('apiDocs.paramName')); + const tLabel = escapeHtml(_t('apiDocs.type')); + const dLabel = escapeHtml(_t('apiDocs.description')); + const exLabel = escapeHtml(_t('apiDocs.example')); paramsTable = `
参数名类型描述必需${paramName}${typeLabel}${descLabel}${requiredLabel}
- - - - - + + + + + @@ -389,12 +467,12 @@ function renderRequestBody(endpoint) { return `
-
请求体
+
${escapeHtml(_t('apiDocs.sectionRequestBody'))}
${endpoint.requestBody.description ? `
${endpoint.requestBody.description}
` : ''} ${paramsTable} ${example ? `
-
示例JSON:
+
${escapeHtml(_t('apiDocs.exampleJson'))}
${escapeHtml(example)}
@@ -414,11 +492,11 @@ function renderResponses(endpoint) { if (schema.example) { example = JSON.stringify(schema.example, null, 2); } - + const descText = translateApiDocResponseDescFromResp(response); return `
${status} - ${response.description ? `${response.description}` : ''} + ${descText ? `${escapeHtml(descText)}` : ''} ${example ? `
${escapeHtml(example)}
@@ -432,7 +510,7 @@ function renderResponses(endpoint) { return `
-
响应
+
${escapeHtml(_t('apiDocs.sectionResponse'))}
${responseItems}
`; @@ -462,8 +540,8 @@ function renderTestSection(endpoint) { const bodyInputId = `test-body-${escapeId(path)}-${method}`; bodyInput = `
- - + +
`; } @@ -491,7 +569,7 @@ function renderTestSection(endpoint) { const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`; const defaultValue = param.schema?.default !== undefined ? param.schema.default : ''; const placeholder = param.description || param.name; - const required = param.required ? '*' : '可选'; + const required = param.required ? '*' : '' + escapeHtml(_t('apiDocs.optional')) + ''; return `
@@ -505,33 +583,40 @@ function renderTestSection(endpoint) { }).join(''); } + const testSectionTitle = escapeHtml(_t('apiDocs.testSection')); + const queryParamsTitle = escapeHtml(_t('apiDocs.queryParams')); + const sendRequestLabel = escapeHtml(_t('apiDocs.sendRequest')); + const copyCurlLabel = escapeHtml(_t('apiDocs.copyCurl')); + const clearResultLabel = escapeHtml(_t('apiDocs.clearResult')); + const copyCurlTitle = escapeHtml(_t('apiDocs.copyCurlTitle')); + const clearResultTitle = escapeHtml(_t('apiDocs.clearResultTitle')); return `
-
测试接口
+
${testSectionTitle}
${pathParamsInput} - ${queryParamsInput ? `
查询参数:
${queryParamsInput}
` : ''} + ${queryParamsInput ? `
${queryParamsTitle}
${queryParamsInput}
` : ''} ${bodyInput}
- -
@@ -548,7 +633,7 @@ async function testAPI(method, path, operationId) { resultDiv.style.display = 'block'; resultDiv.className = 'api-test-result loading'; - resultDiv.textContent = '发送请求中...'; + resultDiv.textContent = _t('apiDocs.sendingRequest'); try { // 替换路径参数 @@ -561,7 +646,7 @@ async function testAPI(method, path, operationId) { if (input && input.value) { actualPath = actualPath.replace(param, encodeURIComponent(input.value)); } else { - throw new Error(`路径参数 ${paramName} 不能为空`); + throw new Error(_t('apiDocs.errorPathParamRequired', { name: paramName })); } }); @@ -580,7 +665,7 @@ async function testAPI(method, path, operationId) { if (input && input.value !== '' && input.value !== null && input.value !== undefined) { queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`); } else if (param.required) { - throw new Error(`查询参数 ${param.name} 不能为空`); + throw new Error(_t('apiDocs.errorQueryParamRequired', { name: param.name })); } }); } @@ -602,8 +687,7 @@ async function testAPI(method, path, operationId) { if (currentToken) { options.headers['Authorization'] = 'Bearer ' + currentToken; } else { - // 如果没有token,提示用户 - throw new Error('未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token'); + throw new Error(_t('apiDocs.errorTokenRequired')); } // 添加请求体 @@ -614,7 +698,7 @@ async function testAPI(method, path, operationId) { try { options.body = JSON.stringify(JSON.parse(bodyInput.value.trim())); } catch (e) { - throw new Error('请求体JSON格式错误: ' + e.message); + throw new Error(_t('apiDocs.errorJsonInvalid') + e.message); } } } @@ -636,7 +720,7 @@ async function testAPI(method, path, operationId) { } catch (error) { resultDiv.className = 'api-test-result error'; - resultDiv.textContent = '请求失败: ' + error.message; + resultDiv.textContent = _t('apiDocs.requestFailed') + error.message; } } @@ -727,17 +811,17 @@ function copyCurlCommand(event, method, path) { // 复制到剪贴板 const button = event ? event.target.closest('button') : null; navigator.clipboard.writeText(curlCommand).then(() => { - // 显示成功提示 if (button) { const originalText = button.innerHTML; - button.innerHTML = '已复制'; + const copiedLabel = escapeHtml(_t('apiDocs.copied')); + button.innerHTML = '' + copiedLabel; button.style.color = 'var(--success-color)'; setTimeout(() => { button.innerHTML = originalText; button.style.color = ''; }, 2000); } else { - alert('curl命令已复制到剪贴板!'); + alert(_t('apiDocs.curlCopied')); } }).catch(err => { console.error('复制失败:', err); @@ -752,24 +836,25 @@ function copyCurlCommand(event, method, path) { document.execCommand('copy'); if (button) { const originalText = button.innerHTML; - button.innerHTML = '已复制'; + const copiedLabel = escapeHtml(_t('apiDocs.copied')); + button.innerHTML = '' + copiedLabel; button.style.color = 'var(--success-color)'; setTimeout(() => { button.innerHTML = originalText; button.style.color = ''; }, 2000); } else { - alert('curl命令已复制到剪贴板!'); + alert(_t('apiDocs.curlCopied')); } } catch (e) { - alert('复制失败,请手动复制:\n\n' + curlCommand); + alert(_t('apiDocs.copyFailedManual') + curlCommand); } document.body.removeChild(textarea); }); } catch (error) { console.error('生成curl命令失败:', error); - alert('生成curl命令失败: ' + error.message); + alert(_t('apiDocs.curlGenFailed') + error.message); } } @@ -935,10 +1020,10 @@ function toggleDescription(button) { if (detail.style.display === 'none') { detail.style.display = 'block'; icon.style.transform = 'rotate(180deg)'; - span.textContent = '隐藏详细说明'; + span.textContent = typeof window.t === 'function' ? window.t('apiDocs.hideDetailDesc') : '隐藏详细说明'; } else { detail.style.display = 'none'; icon.style.transform = 'rotate(0deg)'; - span.textContent = '查看详细说明'; + span.textContent = typeof window.t === 'function' ? window.t('apiDocs.viewDetailDesc') : '查看详细说明'; } } diff --git a/web/static/js/auth.js b/web/static/js/auth.js index 4d607b04..64810c86 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -250,13 +250,13 @@ async function bootstrapApp() { // 通用工具函数 function getStatusText(status) { - const statusMap = { - 'pending': '等待中', - 'running': '执行中', - 'completed': '已完成', - 'failed': '失败' - }; - return statusMap[status] || status; + if (typeof window.t !== 'function') { + const fallback = { pending: '等待中', running: '执行中', completed: '已完成', failed: '失败' }; + return fallback[status] || status; + } + const keyMap = { pending: 'mcpDetailModal.statusPending', running: 'mcpDetailModal.statusRunning', completed: 'mcpDetailModal.statusCompleted', failed: 'mcpDetailModal.statusFailed' }; + const key = keyMap[status]; + return key ? window.t(key) : status; } function formatDuration(ms) { diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 67e59698..4f0d8755 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -755,7 +755,7 @@ function renderMentionSuggestions({ showLoading = false } = {}) { const disabledClass = toolEnabled ? '' : 'disabled'; const badge = tool.isExternal ? '外部' : '内置'; const nameHtml = escapeHtml(tool.name); - const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : '暂无描述'; + const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : (typeof window.t === 'function' ? window.t('chat.noDescription') : '暂无描述'); const descHtml = `
${description}
`; // 根据工具在当前角色中的启用状态显示状态标签 const statusLabel = toolEnabled ? '可用' : (tool.roleEnabled !== undefined ? '已禁用(当前角色)' : '已禁用'); @@ -1209,7 +1209,7 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr mcpExecutionIds.forEach((execId, index) => { const detailBtn = document.createElement('button'); detailBtn.className = 'mcp-detail-btn'; - detailBtn.innerHTML = `调用 #${index + 1}`; + detailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + ''; detailBtn.onclick = () => showMCPDetail(execId); buttonsContainer.appendChild(detailBtn); // 异步获取工具名称并更新按钮文本 @@ -1265,7 +1265,7 @@ function copyMessageToClipboard(messageDiv, button) { showCopySuccess(button); }).catch(err => { console.error('复制失败:', err); - alert('复制失败,请手动选择内容复制'); + alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制'); }); } return; @@ -1276,11 +1276,11 @@ function copyMessageToClipboard(messageDiv, button) { showCopySuccess(button); }).catch(err => { console.error('复制失败:', err); - alert('复制失败,请手动选择内容复制'); + alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制'); }); } catch (error) { console.error('复制消息时出错:', error); - alert('复制失败,请手动选择内容复制'); + alert(typeof window.t === 'function' ? window.t('chat.copyFailedManual') : '复制失败,请手动选择内容复制'); } } @@ -1408,32 +1408,31 @@ function renderProcessDetails(messageId, processDetails) { // 根据事件类型渲染不同的内容 let itemTitle = title; if (eventType === 'iteration') { - itemTitle = `第 ${data.iteration || 1} 轮迭代`; + itemTitle = (typeof window.t === 'function' ? window.t('chat.iterationRound', { n: data.iteration || 1 }) : '第 ' + (data.iteration || 1) + ' 轮迭代'); } else if (eventType === 'thinking') { - itemTitle = '🤔 AI思考'; + itemTitle = '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考'); } else if (eventType === 'tool_calls_detected') { - itemTitle = `🔧 检测到 ${data.count || 0} 个工具调用`; + itemTitle = '🔧 ' + (typeof window.t === 'function' ? window.t('chat.toolCallsDetected', { count: data.count || 0 }) : '检测到 ' + (data.count || 0) + ' 个工具调用'); } else if (eventType === 'tool_call') { - const toolName = data.toolName || '未知工具'; + const toolName = data.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); const index = data.index || 0; const total = data.total || 0; - itemTitle = `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`; + itemTitle = '🔧 ' + (typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')'); } else if (eventType === 'tool_result') { - const toolName = data.toolName || '未知工具'; + const toolName = data.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); const success = data.success !== false; const statusIcon = success ? '✅' : '❌'; - itemTitle = `${statusIcon} 工具 ${escapeHtml(toolName)} 执行${success ? '完成' : '失败'}`; - - // 如果是知识检索工具,添加特殊标记 + const execText = success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(toolName) }) : '工具 ' + escapeHtml(toolName) + ' 执行失败'); + itemTitle = statusIcon + ' ' + execText; if (toolName === BuiltinTools.SEARCH_KNOWLEDGE_BASE && success) { - itemTitle = `📚 ${itemTitle} - 知识检索`; + itemTitle = '📚 ' + itemTitle + ' - ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrievalTag') : '知识检索'); } } else if (eventType === 'knowledge_retrieval') { - itemTitle = '📚 知识检索'; + itemTitle = '📚 ' + (typeof window.t === 'function' ? window.t('chat.knowledgeRetrieval') : '知识检索'); } else if (eventType === 'error') { - itemTitle = '❌ 错误'; + itemTitle = '❌ ' + (typeof window.t === 'function' ? window.t('chat.error') : '错误'); } else if (eventType === 'cancelled') { - itemTitle = '⛔ 任务已取消'; + itemTitle = '⛔ ' + (typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消'); } addTimelineItem(timeline, eventType, { @@ -1509,7 +1508,7 @@ async function updateButtonWithToolName(button, executionId, index) { const response = await apiFetch(`/api/monitor/execution/${executionId}`); if (response.ok) { const exec = await response.json(); - const toolName = exec.toolName || '未知工具'; + const toolName = exec.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); // 格式化工具名称(如果是 name::toolName 格式,只显示 toolName 部分) const displayToolName = toolName.includes('::') ? toolName.split('::')[1] : toolName; button.querySelector('span').textContent = `${displayToolName} #${index}`; @@ -1528,7 +1527,7 @@ async function showMCPDetail(executionId) { if (response.ok) { // 填充模态框内容 - document.getElementById('detail-tool-name').textContent = exec.toolName || 'Unknown'; + document.getElementById('detail-tool-name').textContent = exec.toolName || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : 'Unknown'); document.getElementById('detail-execution-id').textContent = exec.id || 'N/A'; const statusEl = document.getElementById('detail-status'); const normalizedStatus = (exec.status || 'unknown').toLowerCase(); @@ -1598,22 +1597,22 @@ async function showMCPDetail(executionId) { successText = content.text; } if (!successText) { - successText = '执行成功,未返回可展示的文本内容。'; + successText = typeof window.t === 'function' ? window.t('mcpDetailModal.execSuccessNoContent') : '执行成功,未返回可展示的文本内容。'; } successElement.textContent = successText; } } } else { - responseElement.textContent = '暂无响应数据'; + responseElement.textContent = typeof window.t === 'function' ? window.t('chat.noResponseData') : '暂无响应数据'; } // 显示模态框 document.getElementById('mcp-detail-modal').style.display = 'block'; } else { - alert('获取详情失败: ' + (exec.error || '未知错误')); + alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + (exec.error || (typeof window.t === 'function' ? window.t('mcpDetailModal.unknown') : '未知错误'))); } } catch (error) { - alert('获取详情失败: ' + error.message); + alert((typeof window.t === 'function' ? window.t('mcpDetailModal.getDetailFailed') : '获取详情失败') + ': ' + error.message); } } @@ -2255,7 +2254,7 @@ async function showAttackChain(conversationId) { // 清空容器 const container = document.getElementById('attack-chain-container'); if (container) { - container.innerHTML = '
加载中...
'; + container.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.loading') : '加载中...') + '
'; } // 隐藏详情面板 @@ -2355,7 +2354,7 @@ async function loadAttackChain(conversationId) { console.error('加载攻击链失败:', error); const container = document.getElementById('attack-chain-container'); if (container) { - container.innerHTML = `
加载失败: ${error.message}
`; + container.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.loadFailed', { message: error.message }) : '加载失败: ' + error.message) + '
'; } // 错误时也重置加载状态 setAttackChainLoading(conversationId, false); @@ -2381,7 +2380,7 @@ function renderAttackChain(chainData) { container.innerHTML = ''; if (!chainData.nodes || chainData.nodes.length === 0) { - container.innerHTML = '
暂无攻击链数据
'; + container.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.noAttackChainData') : '暂无攻击链数据') + '
'; return; } @@ -5544,9 +5543,9 @@ async function loadGroupConversations(groupId, searchQuery = '') { // 显示加载状态 if (searchQuery) { - list.innerHTML = '
搜索中...
'; + list.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.searching') : '搜索中...') + '
'; } else { - list.innerHTML = '
加载中...
'; + list.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.loading') : '加载中...') + '
'; } // 构建URL,如果有搜索关键词则添加search参数 @@ -5558,7 +5557,7 @@ async function loadGroupConversations(groupId, searchQuery = '') { const response = await apiFetch(url); if (!response.ok) { console.error(`Failed to load conversations for group ${groupId}:`, response.statusText); - list.innerHTML = '
加载失败,请重试
'; + list.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.loadFailedRetry') : '加载失败,请重试') + '
'; return; } @@ -5572,7 +5571,7 @@ async function loadGroupConversations(groupId, searchQuery = '') { // 验证返回的数据类型 if (!Array.isArray(groupConvs)) { console.error(`Invalid response for group ${groupId}:`, groupConvs); - list.innerHTML = '
数据格式错误
'; + list.innerHTML = '
' + (typeof window.t === 'function' ? window.t('chat.dataFormatError') : '数据格式错误') + '
'; return; } diff --git a/web/static/js/i18n.js b/web/static/js/i18n.js index 40cd0486..6e7b117f 100644 --- a/web/static/js/i18n.js +++ b/web/static/js/i18n.js @@ -109,9 +109,9 @@ if (!label || typeof i18next === 'undefined') return; const lang = (i18next.language || DEFAULT_LANG).toLowerCase(); if (lang.indexOf('zh') === 0) { - label.textContent = '中文'; + label.textContent = i18next.t('lang.zhCN'); } else { - label.textContent = 'English'; + label.textContent = i18next.t('lang.enUS'); } } diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index b64535f9..8a68c46d 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -3,6 +3,27 @@ let activeTaskInterval = null; const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次 const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']); +// 将后端下发的进度文案转为当前语言的翻译(已知中文 key 映射) +function translateProgressMessage(message) { + if (!message || typeof message !== 'string') return message; + if (typeof window.t !== 'function') return message; + const trim = message.trim(); + const map = { + '正在调用AI模型...': 'progress.callingAI', + '最后一次迭代:正在生成总结和下一步计划...': 'progress.lastIterSummary', + '总结生成完成': 'progress.summaryDone', + '正在生成最终回复...': 'progress.generatingFinalReply', + '达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary' + }; + if (map[trim]) return window.t(map[trim]); + const callingToolPrefix = '正在调用工具: '; + if (trim.indexOf(callingToolPrefix) === 0) { + const name = trim.slice(callingToolPrefix.length); + return window.t('progress.callingTool', { name: name }); + } + return message; +} + // 存储工具调用ID到DOM元素的映射,用于更新执行状态 const toolCallStatusMap = new Map(); @@ -57,11 +78,15 @@ function markProgressCancelling(progressId) { } } -function finalizeProgressTask(progressId, finalLabel = '已完成') { +function finalizeProgressTask(progressId, finalLabel) { const stopBtn = document.getElementById(`${progressId}-stop-btn`); if (stopBtn) { stopBtn.disabled = true; - stopBtn.textContent = finalLabel; + if (finalLabel !== undefined && finalLabel !== '') { + stopBtn.textContent = finalLabel; + } else { + stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成'; + } } progressTaskState.delete(progressId); } @@ -94,12 +119,15 @@ function addProgressMessage() { const bubble = document.createElement('div'); bubble.className = 'message-bubble progress-container'; + const progressTitleText = typeof window.t === 'function' ? window.t('chat.progressInProgress') : '渗透测试进行中...'; + const stopTaskText = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务'; + const collapseDetailText = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; bubble.innerHTML = `
- 🔍 渗透测试进行中... + 🔍 ${progressTitleText}
- - + +
@@ -123,10 +151,10 @@ function toggleProgressDetails(progressId) { if (timeline.classList.contains('expanded')) { timeline.classList.remove('expanded'); - toggleBtn.textContent = '展开详情'; + toggleBtn.textContent = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; } else { timeline.classList.add('expanded'); - toggleBtn.textContent = '收起详情'; + toggleBtn.textContent = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; } } @@ -143,7 +171,7 @@ function collapseAllProgressDetails(assistantMessageId, progressId) { timeline.classList.remove('expanded'); const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`); if (btn) { - btn.innerHTML = '展开详情'; + btn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + ''; } } } @@ -246,7 +274,7 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) { // 设置详情内容(如果有错误,默认折叠;否则默认折叠) detailsContainer.innerHTML = `
- ${hasContent ? `
${timelineHTML}
` : '
暂无过程详情
'} + ${hasContent ? `
${timelineHTML}
` : '
' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '
'}
`; @@ -258,10 +286,9 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) { timeline.classList.remove('expanded'); } - // 更新按钮文本为"展开详情"(因为默认折叠) const processDetailBtn = buttonsContainer.querySelector('.process-detail-btn'); if (processDetailBtn) { - processDetailBtn.innerHTML = '展开详情'; + processDetailBtn.innerHTML = '' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + ''; } } @@ -279,22 +306,23 @@ function toggleProcessDetails(progressId, assistantMessageId) { const timeline = detailsContainer.querySelector('.progress-timeline'); const btn = document.querySelector(`#${assistantMessageId} .process-detail-btn`); + const expandT = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; + const collapseT = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; if (content && timeline) { if (timeline.classList.contains('expanded')) { timeline.classList.remove('expanded'); - if (btn) btn.innerHTML = '展开详情'; + if (btn) btn.innerHTML = '' + expandT + ''; } else { timeline.classList.add('expanded'); - if (btn) btn.innerHTML = '收起详情'; + if (btn) btn.innerHTML = '' + collapseT + ''; } } else if (timeline) { - // 如果只有timeline,直接切换 if (timeline.classList.contains('expanded')) { timeline.classList.remove('expanded'); - if (btn) btn.innerHTML = '展开详情'; + if (btn) btn.innerHTML = '' + expandT + ''; } else { timeline.classList.add('expanded'); - if (btn) btn.innerHTML = '收起详情'; + if (btn) btn.innerHTML = '' + collapseT + ''; } } @@ -338,10 +366,10 @@ async function cancelProgressTask(progressId) { loadActiveTasks(); } catch (error) { console.error('取消任务失败:', error); - alert('取消任务失败: ' + error.message); + alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '取消任务失败') + ': ' + error.message); if (stopBtn) { stopBtn.disabled = false; - stopBtn.textContent = '停止任务'; + stopBtn.textContent = typeof window.t === 'function' ? window.t('tasks.stopTask') : '停止任务'; } const currentState = progressTaskState.get(progressId); if (currentState) { @@ -391,15 +419,17 @@ function convertProgressToDetails(progressId, assistantMessageId) { // 如果有错误,默认折叠;否则默认展开 const shouldExpand = !hasError; const expandedClass = shouldExpand ? 'expanded' : ''; - const toggleText = shouldExpand ? '收起详情' : '展开详情'; - - // 总是显示详情组件,即使没有内容也显示 + const collapseDetailText = typeof window.t === 'function' ? window.t('tasks.collapseDetail') : '收起详情'; + const expandDetailText = typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情'; + const toggleText = shouldExpand ? collapseDetailText : expandDetailText; + const penetrationDetailText = typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情'; + const noProcessDetailText = typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)'; bubble.innerHTML = `
- 📋 渗透测试详情 + 📋 ${penetrationDetailText} ${hasContent ? `` : ''}
- ${hasContent ? `
${timelineHTML}
` : '
暂无过程详情(可能执行过快或未触发详细事件)
'} + ${hasContent ? `
${timelineHTML}
` : '
' + noProcessDetailText + '
'} `; contentWrapper.appendChild(bubble); @@ -466,41 +496,37 @@ function handleStreamEvent(event, progressElement, progressId, case 'iteration': // 添加迭代标记 addTimelineItem(timeline, 'iteration', { - title: `第 ${event.data?.iteration || 1} 轮迭代`, + title: typeof window.t === 'function' ? window.t('chat.iterationRound', { n: event.data?.iteration || 1 }) : '第 ' + (event.data?.iteration || 1) + ' 轮迭代', message: event.message, data: event.data }); break; case 'thinking': - // 显示AI思考内容 addTimelineItem(timeline, 'thinking', { - title: '🤔 AI思考', + title: '🤔 ' + (typeof window.t === 'function' ? window.t('chat.aiThinking') : 'AI思考'), message: event.message, data: event.data }); break; case 'tool_calls_detected': - // 工具调用检测 addTimelineItem(timeline, 'tool_calls_detected', { - title: `🔧 检测到 ${event.data?.count || 0} 个工具调用`, + title: '🔧 ' + (typeof window.t === 'function' ? window.t('chat.toolCallsDetected', { count: event.data?.count || 0 }) : '检测到 ' + (event.data?.count || 0) + ' 个工具调用'), message: event.message, data: event.data }); break; case 'tool_call': - // 显示工具调用信息 const toolInfo = event.data || {}; - const toolName = toolInfo.toolName || '未知工具'; + const toolName = toolInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); const index = toolInfo.index || 0; const total = toolInfo.total || 0; const toolCallId = toolInfo.toolCallId || null; - - // 添加工具调用项,并标记为执行中 + const toolCallTitle = typeof window.t === 'function' ? window.t('chat.callTool', { name: escapeHtml(toolName), index: index, total: total }) : '调用工具: ' + escapeHtml(toolName) + ' (' + index + '/' + total + ')'; const toolCallItemId = addTimelineItem(timeline, 'tool_call', { - title: `🔧 调用工具: ${escapeHtml(toolName)} (${index}/${total})`, + title: '🔧 ' + toolCallTitle, message: event.message, data: toolInfo, expanded: false @@ -519,22 +545,18 @@ function handleStreamEvent(event, progressElement, progressId, break; case 'tool_result': - // 显示工具执行结果 const resultInfo = event.data || {}; - const resultToolName = resultInfo.toolName || '未知工具'; + const resultToolName = resultInfo.toolName || (typeof window.t === 'function' ? window.t('chat.unknownTool') : '未知工具'); const success = resultInfo.success !== false; const statusIcon = success ? '✅' : '❌'; const resultToolCallId = resultInfo.toolCallId || null; - - // 如果有关联的toolCallId,更新工具调用项的状态 + const resultExecText = success ? (typeof window.t === 'function' ? window.t('chat.toolExecComplete', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行完成') : (typeof window.t === 'function' ? window.t('chat.toolExecFailed', { name: escapeHtml(resultToolName) }) : '工具 ' + escapeHtml(resultToolName) + ' 执行失败'); if (resultToolCallId && toolCallStatusMap.has(resultToolCallId)) { updateToolCallStatus(resultToolCallId, success ? 'completed' : 'failed'); - // 从映射中移除(已完成) toolCallStatusMap.delete(resultToolCallId); } - addTimelineItem(timeline, 'tool_result', { - title: `${statusIcon} 工具 ${escapeHtml(resultToolName)} 执行${success ? '完成' : '失败'}`, + title: statusIcon + ' ' + resultExecText, message: event.message, data: resultInfo, expanded: false @@ -542,36 +564,30 @@ function handleStreamEvent(event, progressElement, progressId, break; case 'progress': - // 更新进度状态 const progressTitle = document.querySelector(`#${progressId} .progress-title`); if (progressTitle) { - progressTitle.textContent = '🔍 ' + event.message; + const progressMsg = translateProgressMessage(event.message); + progressTitle.textContent = '🔍 ' + progressMsg; } break; case 'cancelled': - // 显示错误 + const taskCancelledText = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消'; addTimelineItem(timeline, 'cancelled', { - title: '⛔ 任务已取消', + title: '⛔ ' + taskCancelledText, message: event.message, data: event.data }); - - // 更新进度标题为取消状态 const cancelTitle = document.querySelector(`#${progressId} .progress-title`); if (cancelTitle) { - cancelTitle.textContent = '⛔ 任务已取消'; + cancelTitle.textContent = '⛔ ' + taskCancelledText; } - - // 更新进度容器为已完成状态(添加completed类) const cancelProgressContainer = document.querySelector(`#${progressId} .progress-container`); if (cancelProgressContainer) { cancelProgressContainer.classList.add('completed'); } - - // 完成进度任务(标记为已取消) if (progressTaskState.has(progressId)) { - finalizeProgressTask(progressId, '已取消'); + finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCancelled') : '已取消'); } // 如果取消事件包含messageId,说明有助手消息,需要显示取消内容 @@ -689,7 +705,7 @@ function handleStreamEvent(event, progressElement, progressId, // 完成进度任务(标记为失败) if (progressTaskState.has(progressId)) { - finalizeProgressTask(progressId, '已失败'); + finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusFailed') : '执行失败'); } // 如果错误事件包含messageId,说明有助手消息,需要显示错误内容 @@ -753,7 +769,7 @@ function handleStreamEvent(event, progressElement, progressId, updateProgressConversation(progressId, event.data.conversationId); } if (progressTaskState.has(progressId)) { - finalizeProgressTask(progressId, '已完成'); + finalizeProgressTask(progressId, typeof window.t === 'function' ? window.t('tasks.statusCompleted') : '已完成'); } // 检查时间线中是否有错误项 @@ -807,17 +823,19 @@ function updateToolCallStatus(toolCallId, status) { // 移除之前的状态类 item.classList.remove('tool-call-running', 'tool-call-completed', 'tool-call-failed'); - // 根据状态更新样式和文本 + const runningLabel = typeof window.t === 'function' ? window.t('timeline.running') : '执行中...'; + const completedLabel = typeof window.t === 'function' ? window.t('timeline.completed') : '已完成'; + const failedLabel = typeof window.t === 'function' ? window.t('timeline.execFailed') : '执行失败'; let statusText = ''; if (status === 'running') { item.classList.add('tool-call-running'); - statusText = ' 执行中...'; + statusText = ' ' + escapeHtml(runningLabel) + ''; } else if (status === 'completed') { item.classList.add('tool-call-completed'); - statusText = ' ✅ 已完成'; + statusText = ' ✅ ' + escapeHtml(completedLabel) + ''; } else if (status === 'failed') { item.classList.add('tool-call-failed'); - statusText = ' ❌ 执行失败'; + statusText = ' ❌ ' + escapeHtml(failedLabel) + ''; } // 更新标题(保留原有文本,追加状态) @@ -869,11 +887,12 @@ function addTimelineItem(timeline, type, options) { } else if (type === 'tool_call' && options.data) { const data = options.data; const args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : {}); + const paramsLabel = typeof window.t === 'function' ? window.t('timeline.params') : '参数:'; content += `
- 参数: + ${escapeHtml(paramsLabel)}
${escapeHtml(JSON.stringify(args, null, 2))}
@@ -882,22 +901,25 @@ function addTimelineItem(timeline, type, options) { } else if (type === 'tool_result' && options.data) { const data = options.data; const isError = data.isError || !data.success; - const result = data.result || data.error || '无结果'; - // 确保 result 是字符串 + const noResultText = typeof window.t === 'function' ? window.t('timeline.noResult') : '无结果'; + const result = data.result || data.error || noResultText; const resultStr = typeof result === 'string' ? result : JSON.stringify(result); + const execResultLabel = typeof window.t === 'function' ? window.t('timeline.executionResult') : '执行结果:'; + const execIdLabel = typeof window.t === 'function' ? window.t('timeline.executionId') : '执行ID:'; content += `
- 执行结果: + ${escapeHtml(execResultLabel)}
${escapeHtml(resultStr)}
- ${data.executionId ? `
执行ID: ${escapeHtml(data.executionId)}
` : ''} + ${data.executionId ? `
${escapeHtml(execIdLabel)} ${escapeHtml(data.executionId)}
` : ''}
`; } else if (type === 'cancelled') { + const taskCancelledLabel = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消'; content += `
- ${escapeHtml(options.message || '任务已取消')} + ${escapeHtml(options.message || taskCancelledLabel)}
`; } @@ -964,26 +986,28 @@ function renderActiveTasks(tasks) { ? startedTime.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) : ''; - // 根据任务状态显示不同的文本 + const _t = function (k) { return typeof window.t === 'function' ? window.t(k) : k; }; const statusMap = { - 'running': '执行中', - 'cancelling': '取消中', - 'failed': '执行失败', - 'timeout': '执行超时', - 'cancelled': '已取消', - 'completed': '已完成' + 'running': _t('tasks.statusRunning'), + 'cancelling': _t('tasks.statusCancelling'), + 'failed': _t('tasks.statusFailed'), + 'timeout': _t('tasks.statusTimeout'), + 'cancelled': _t('tasks.statusCancelled'), + 'completed': _t('tasks.statusCompleted') }; - const statusText = statusMap[task.status] || '执行中'; + const statusText = statusMap[task.status] || _t('tasks.statusRunning'); const isFinalStatus = ['failed', 'timeout', 'cancelled', 'completed'].includes(task.status); + const unnamedTaskText = _t('tasks.unnamedTask'); + const stopTaskBtnText = _t('tasks.stopTask'); item.innerHTML = `
${statusText} - ${escapeHtml(task.message || '未命名任务')} + ${escapeHtml(task.message || unnamedTaskText)}
${timeText ? `${timeText}` : ''} - ${!isFinalStatus ? '' : ''} + ${!isFinalStatus ? '' : ''}
`; @@ -994,7 +1018,7 @@ function renderActiveTasks(tasks) { cancelBtn.onclick = () => cancelActiveTask(task.conversationId, cancelBtn); if (task.status === 'cancelling') { cancelBtn.disabled = true; - cancelBtn.textContent = '取消中...'; + cancelBtn.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...'; } } } @@ -1007,14 +1031,14 @@ async function cancelActiveTask(conversationId, button) { if (!conversationId) return; const originalText = button.textContent; button.disabled = true; - button.textContent = '取消中...'; + button.textContent = typeof window.t === 'function' ? window.t('tasks.cancelling') : '取消中...'; try { await requestCancel(conversationId); loadActiveTasks(); } catch (error) { console.error('取消任务失败:', error); - alert('取消任务失败: ' + error.message); + alert((typeof window.t === 'function' ? window.t('tasks.cancelTaskFailed') : '取消任务失败') + ': ' + error.message); button.disabled = false; button.textContent = originalText; } @@ -1522,14 +1546,14 @@ function updateBatchActionsState() { if (batchActions) { batchActions.style.display = 'flex'; } - if (selectedCountSpan) { - selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : `已选择 ${selectedCount} 项`; - } } else { if (batchActions) { batchActions.style.display = 'none'; } } + if (selectedCountSpan) { + selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : '已选择 ' + selectedCount + ' 项'; + } // 更新全选复选框状态 const selectAllCheckbox = document.getElementById('monitor-select-all'); @@ -1655,3 +1679,7 @@ function formatExecutionDuration(start, end) { } return typeof window.t === 'function' ? window.t('mcpMonitor.durationHoursOnly', { hours: hours }) : hours + ' 小时'; } + +document.addEventListener('languagechange', function () { + updateBatchActionsState(); +}); diff --git a/web/static/js/roles.js b/web/static/js/roles.js index 09ad85ac..129343c8 100644 --- a/web/static/js/roles.js +++ b/web/static/js/roles.js @@ -1433,6 +1433,11 @@ document.addEventListener('DOMContentLoaded', () => { updateRoleSelectorDisplay(); }); +// 语言切换后刷新角色选择器显示(默认/自定义角色名) +document.addEventListener('languagechange', () => { + updateRoleSelectorDisplay(); +}); + // 获取当前选中的角色(供chat.js使用) function getCurrentRole() { return currentRole || ''; diff --git a/web/templates/api-docs.html b/web/templates/api-docs.html index 4491d792..ef73d568 100644 --- a/web/templates/api-docs.html +++ b/web/templates/api-docs.html @@ -3,7 +3,7 @@ - API 文档 - CyberStrikeAI + API 文档 - CyberStrikeAI
参数名类型描述必需示例${pName}${tLabel}${dLabel}${reqLabel}${exLabel}