mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-04-21 18:26:38 +02:00
Add files via upload
This commit is contained in:
@@ -4411,6 +4411,7 @@ func (h *OpenAPIHandler) GetOpenAPISpec(c *gin.Context) {
|
||||
},
|
||||
}
|
||||
|
||||
enrichSpecWithI18nKeys(spec)
|
||||
c.JSON(http.StatusOK, spec)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
package handler
|
||||
|
||||
// apiDocI18n 为 OpenAPI 文档提供 x-i18n-* 扩展键,供前端 apiDocs 国际化使用。
|
||||
// 前端通过 apiDocs.tags.* / apiDocs.summary.* / apiDocs.response.* 翻译。
|
||||
|
||||
var apiDocI18nTagToKey = map[string]string{
|
||||
"认证": "auth", "对话管理": "conversationManagement", "对话交互": "conversationInteraction",
|
||||
"批量任务": "batchTasks", "对话分组": "conversationGroups", "漏洞管理": "vulnerabilityManagement",
|
||||
"角色管理": "roleManagement", "Skills管理": "skillsManagement", "监控": "monitoring",
|
||||
"配置管理": "configManagement", "外部MCP管理": "externalMCPManagement", "攻击链": "attackChain",
|
||||
"知识库": "knowledgeBase", "MCP": "mcp",
|
||||
}
|
||||
|
||||
var apiDocI18nSummaryToKey = map[string]string{
|
||||
"用户登录": "login", "用户登出": "logout", "修改密码": "changePassword", "验证Token": "validateToken",
|
||||
"创建对话": "createConversation", "列出对话": "listConversations", "查看对话详情": "getConversationDetail",
|
||||
"更新对话": "updateConversation", "删除对话": "deleteConversation", "获取对话结果": "getConversationResult",
|
||||
"发送消息并获取AI回复(非流式)": "sendMessageNonStream", "发送消息并获取AI回复(流式)": "sendMessageStream",
|
||||
"取消任务": "cancelTask", "列出运行中的任务": "listRunningTasks", "列出已完成的任务": "listCompletedTasks",
|
||||
"创建批量任务队列": "createBatchQueue", "列出批量任务队列": "listBatchQueues", "获取批量任务队列": "getBatchQueue",
|
||||
"删除批量任务队列": "deleteBatchQueue", "启动批量任务队列": "startBatchQueue", "暂停批量任务队列": "pauseBatchQueue",
|
||||
"添加任务到队列": "addTaskToQueue", "SQL注入扫描": "sqlInjectionScan", "端口扫描": "portScan",
|
||||
"更新批量任务": "updateBatchTask", "删除批量任务": "deleteBatchTask",
|
||||
"创建分组": "createGroup", "列出分组": "listGroups", "获取分组": "getGroup", "更新分组": "updateGroup",
|
||||
"删除分组": "deleteGroup", "获取分组中的对话": "getGroupConversations", "添加对话到分组": "addConversationToGroup",
|
||||
"从分组移除对话": "removeConversationFromGroup",
|
||||
"列出漏洞": "listVulnerabilities", "创建漏洞": "createVulnerability", "获取漏洞统计": "getVulnerabilityStats",
|
||||
"获取漏洞": "getVulnerability", "更新漏洞": "updateVulnerability", "删除漏洞": "deleteVulnerability",
|
||||
"列出角色": "listRoles", "创建角色": "createRole", "获取角色": "getRole", "更新角色": "updateRole", "删除角色": "deleteRole",
|
||||
"获取可用Skills列表": "getAvailableSkills", "列出Skills": "listSkills", "创建Skill": "createSkill",
|
||||
"获取Skill统计": "getSkillStats", "清空Skill统计": "clearSkillStats", "获取Skill": "getSkill",
|
||||
"更新Skill": "updateSkill", "删除Skill": "deleteSkill", "获取绑定角色": "getBoundRoles",
|
||||
"获取监控信息": "getMonitorInfo", "获取执行记录": "getExecutionRecords", "删除执行记录": "deleteExecutionRecord",
|
||||
"批量删除执行记录": "batchDeleteExecutionRecords", "获取统计信息": "getStats",
|
||||
"获取配置": "getConfig", "更新配置": "updateConfig", "获取工具配置": "getToolConfig", "应用配置": "applyConfig",
|
||||
"列出外部MCP": "listExternalMCP", "获取外部MCP统计": "getExternalMCPStats", "获取外部MCP": "getExternalMCP",
|
||||
"添加或更新外部MCP": "addOrUpdateExternalMCP", "stdio模式配置": "stdioModeConfig", "SSE模式配置": "sseModeConfig",
|
||||
"删除外部MCP": "deleteExternalMCP", "启动外部MCP": "startExternalMCP", "停止外部MCP": "stopExternalMCP",
|
||||
"获取攻击链": "getAttackChain", "重新生成攻击链": "regenerateAttackChain",
|
||||
"设置对话置顶": "pinConversation", "设置分组置顶": "pinGroup", "设置分组中对话的置顶": "pinGroupConversation",
|
||||
"获取分类": "getCategories", "列出知识项": "listKnowledgeItems", "创建知识项": "createKnowledgeItem",
|
||||
"获取知识项": "getKnowledgeItem", "更新知识项": "updateKnowledgeItem", "删除知识项": "deleteKnowledgeItem",
|
||||
"获取索引状态": "getIndexStatus", "重建索引": "rebuildIndex", "扫描知识库": "scanKnowledgeBase",
|
||||
"搜索知识库": "searchKnowledgeBase", "基础搜索": "basicSearch", "按风险类型搜索": "searchByRiskType",
|
||||
"获取检索日志": "getRetrievalLogs", "删除检索日志": "deleteRetrievalLog",
|
||||
"MCP端点": "mcpEndpoint", "列出所有工具": "listAllTools", "调用工具": "invokeTool", "初始化连接": "initConnection",
|
||||
"成功响应": "successResponse", "错误响应": "errorResponse",
|
||||
}
|
||||
|
||||
var apiDocI18nResponseDescToKey = map[string]string{
|
||||
"获取成功": "getSuccess", "未授权": "unauthorized", "未授权,需要有效的Token": "unauthorizedToken",
|
||||
"创建成功": "createSuccess", "请求参数错误": "badRequest", "对话不存在": "conversationNotFound",
|
||||
"对话不存在或结果不存在": "conversationOrResultNotFound", "请求参数错误(如task为空)": "badRequestTaskEmpty",
|
||||
"请求参数错误或分组名称已存在": "badRequestGroupNameExists", "分组不存在": "groupNotFound",
|
||||
"请求参数错误(如配置格式不正确、缺少必需字段等)": "badRequestConfig",
|
||||
"请求参数错误(如query为空)": "badRequestQueryEmpty", "方法不允许(仅支持POST请求)": "methodNotAllowed",
|
||||
"登录成功": "loginSuccess", "密码错误": "invalidPassword", "登出成功": "logoutSuccess",
|
||||
"密码修改成功": "passwordChanged", "Token有效": "tokenValid", "Token无效或已过期": "tokenInvalid",
|
||||
"对话创建成功": "conversationCreated", "服务器内部错误": "internalError", "更新成功": "updateSuccess",
|
||||
"删除成功": "deleteSuccess", "队列不存在": "queueNotFound", "启动成功": "startSuccess",
|
||||
"暂停成功": "pauseSuccess", "添加成功": "addSuccess",
|
||||
"任务不存在": "taskNotFound", "对话或分组不存在": "conversationOrGroupNotFound",
|
||||
"取消请求已提交": "cancelSubmitted", "未找到正在执行的任务": "noRunningTask",
|
||||
"消息发送成功,返回AI回复": "messageSent", "流式响应(Server-Sent Events)": "streamResponse",
|
||||
}
|
||||
|
||||
// enrichSpecWithI18nKeys 在 spec 的每个 operation 上写入 x-i18n-tags、x-i18n-summary,
|
||||
// 在每个 response 上写入 x-i18n-description,供前端按 key 做国际化。
|
||||
func enrichSpecWithI18nKeys(spec map[string]interface{}) {
|
||||
paths, _ := spec["paths"].(map[string]interface{})
|
||||
if paths == nil {
|
||||
return
|
||||
}
|
||||
for _, pathItem := range paths {
|
||||
pm, _ := pathItem.(map[string]interface{})
|
||||
if pm == nil {
|
||||
continue
|
||||
}
|
||||
for _, method := range []string{"get", "post", "put", "delete", "patch"} {
|
||||
opVal, ok := pm[method]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
op, _ := opVal.(map[string]interface{})
|
||||
if op == nil {
|
||||
continue
|
||||
}
|
||||
// x-i18n-tags: 与 tags 一一对应的 i18n 键数组(spec 中 tags 为 []string)
|
||||
switch tags := op["tags"].(type) {
|
||||
case []string:
|
||||
if len(tags) > 0 {
|
||||
keys := make([]string, 0, len(tags))
|
||||
for _, s := range tags {
|
||||
if k := apiDocI18nTagToKey[s]; k != "" {
|
||||
keys = append(keys, k)
|
||||
} else {
|
||||
keys = append(keys, s)
|
||||
}
|
||||
}
|
||||
op["x-i18n-tags"] = keys
|
||||
}
|
||||
case []interface{}:
|
||||
if len(tags) > 0 {
|
||||
keys := make([]interface{}, 0, len(tags))
|
||||
for _, t := range tags {
|
||||
if s, ok := t.(string); ok {
|
||||
if k := apiDocI18nTagToKey[s]; k != "" {
|
||||
keys = append(keys, k)
|
||||
} else {
|
||||
keys = append(keys, s)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(keys) > 0 {
|
||||
op["x-i18n-tags"] = keys
|
||||
}
|
||||
}
|
||||
}
|
||||
// x-i18n-summary
|
||||
if summary, _ := op["summary"].(string); summary != "" {
|
||||
if k := apiDocI18nSummaryToKey[summary]; k != "" {
|
||||
op["x-i18n-summary"] = k
|
||||
}
|
||||
}
|
||||
// responses -> 每个 status -> x-i18n-description
|
||||
if respMap, _ := op["responses"].(map[string]interface{}); respMap != nil {
|
||||
for _, rv := range respMap {
|
||||
if r, _ := rv.(map[string]interface{}); r != nil {
|
||||
if desc, _ := r["description"].(string); desc != "" {
|
||||
if k := apiDocI18nResponseDescToKey[desc]; k != "" {
|
||||
r["x-i18n-description"] = k
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+261
-2
@@ -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",
|
||||
|
||||
+261
-2
@@ -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": "请求参数",
|
||||
|
||||
+146
-61
@@ -3,13 +3,74 @@
|
||||
let apiSpec = null;
|
||||
let currentToken = null;
|
||||
|
||||
function _t(key, opts) {
|
||||
return typeof window.t === 'function' ? window.t(key, opts) : key;
|
||||
}
|
||||
|
||||
function waitForI18n() {
|
||||
return new Promise(function (resolve) {
|
||||
if (window.t) return resolve();
|
||||
var n = 0;
|
||||
var iv = setInterval(function () {
|
||||
if (window.t) { clearInterval(iv); resolve(); return; }
|
||||
n++;
|
||||
if (n >= 100) { clearInterval(iv); resolve(); }
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
// 从 OpenAPI spec 的 x-i18n-tags 构建 tag -> i18n key 映射(方案 A:后端提供键)
|
||||
var apiSpecTagToKey = {};
|
||||
function buildApiSpecTagToKey() {
|
||||
apiSpecTagToKey = {};
|
||||
if (!apiSpec || !apiSpec.paths) return;
|
||||
Object.keys(apiSpec.paths).forEach(function (path) {
|
||||
var pathItem = apiSpec.paths[path];
|
||||
if (!pathItem || typeof pathItem !== 'object') return;
|
||||
['get', 'post', 'put', 'delete', 'patch'].forEach(function (method) {
|
||||
var op = pathItem[method];
|
||||
if (!op || !op.tags || !op['x-i18n-tags']) return;
|
||||
var tags = op.tags;
|
||||
var keys = op['x-i18n-tags'];
|
||||
for (var i = 0; i < tags.length && i < keys.length; i++) {
|
||||
apiSpecTagToKey[tags[i]] = typeof keys[i] === 'string' ? keys[i] : keys[i];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
function translateApiDocTag(tag) {
|
||||
if (!tag) return tag;
|
||||
var key = apiSpecTagToKey[tag];
|
||||
return key ? _t('apiDocs.tags.' + key) : tag;
|
||||
}
|
||||
function translateApiDocSummaryFromOp(op) {
|
||||
var key = op && op['x-i18n-summary'];
|
||||
if (key) return _t('apiDocs.summary.' + key);
|
||||
return op && op.summary ? op.summary : '';
|
||||
}
|
||||
function translateApiDocResponseDescFromResp(resp) {
|
||||
if (!resp) return '';
|
||||
var key = resp['x-i18n-description'];
|
||||
if (key) return _t('apiDocs.response.' + key);
|
||||
return resp.description || '';
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
await waitForI18n();
|
||||
await loadToken();
|
||||
await loadAPISpec();
|
||||
if (apiSpec) {
|
||||
renderAPIDocs();
|
||||
}
|
||||
document.addEventListener('languagechange', function () {
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(document);
|
||||
}
|
||||
if (apiSpec) {
|
||||
renderAPIDocs();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 加载token
|
||||
@@ -43,22 +104,25 @@ async function loadAPISpec() {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
showError('需要登录才能查看API文档。请先在前端页面登录,然后刷新此页面。');
|
||||
showError(_t('apiDocs.errorLoginRequired'));
|
||||
return;
|
||||
}
|
||||
throw new Error('加载API规范失败: ' + response.status);
|
||||
throw new Error(_t('apiDocs.errorLoadSpec') + response.status);
|
||||
}
|
||||
|
||||
apiSpec = await response.json();
|
||||
buildApiSpecTagToKey();
|
||||
} catch (error) {
|
||||
console.error('加载API规范失败:', error);
|
||||
showError('加载API文档失败: ' + error.message);
|
||||
showError(_t('apiDocs.errorLoadFailed') + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示错误
|
||||
function showError(message) {
|
||||
const main = document.getElementById('api-docs-main');
|
||||
const loadFailed = _t('apiDocs.loadFailed');
|
||||
const backToLogin = _t('apiDocs.backToLogin');
|
||||
main.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -66,10 +130,10 @@ function showError(message) {
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>
|
||||
<h3>加载失败</h3>
|
||||
<p>${message}</p>
|
||||
<h3>${escapeHtml(loadFailed)}</h3>
|
||||
<p>${escapeHtml(message)}</p>
|
||||
<div style="margin-top: 16px;">
|
||||
<a href="/" style="color: var(--accent-color); text-decoration: none;">返回首页登录</a>
|
||||
<a href="/" style="color: var(--accent-color); text-decoration: none;">${escapeHtml(backToLogin)}</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -78,7 +142,7 @@ function showError(message) {
|
||||
// 渲染API文档
|
||||
function renderAPIDocs() {
|
||||
if (!apiSpec || !apiSpec.paths) {
|
||||
showError('API规范格式错误');
|
||||
showError(_t('apiDocs.errorSpecInvalid'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -109,7 +173,7 @@ function renderAuthInfo() {
|
||||
tokenStatus.style.display = 'block';
|
||||
tokenStatus.style.background = 'rgba(255, 152, 0, 0.1)';
|
||||
tokenStatus.style.borderLeftColor = '#ff9800';
|
||||
tokenStatus.innerHTML = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;"><strong>⚠ 未检测到 Token</strong> - 请先在前端页面登录,然后刷新此页面。测试接口时需要在请求头中添加 Authorization: Bearer token</p>';
|
||||
tokenStatus.innerHTML = '<p style="margin: 0; font-size: 0.8125rem; color: #ff9800;">' + escapeHtml(_t('apiDocs.tokenNotDetected')) + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,11 +191,14 @@ function renderSidebar() {
|
||||
|
||||
const groupList = document.getElementById('api-group-list');
|
||||
const allGroups = Array.from(groups).sort();
|
||||
|
||||
while (groupList.children.length > 1) {
|
||||
groupList.removeChild(groupList.lastChild);
|
||||
}
|
||||
allGroups.forEach(group => {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'api-group-item';
|
||||
li.innerHTML = `<a href="#" class="api-group-link" data-group="${group}">${group}</a>`;
|
||||
const groupLabel = translateApiDocTag(group);
|
||||
li.innerHTML = `<a href="#" class="api-group-link" data-group="${escapeHtml(group)}">${escapeHtml(groupLabel)}</a>`;
|
||||
groupList.appendChild(li);
|
||||
});
|
||||
|
||||
@@ -176,7 +243,7 @@ function renderEndpoints(filterGroup = null) {
|
||||
});
|
||||
|
||||
if (endpoints.length === 0) {
|
||||
main.innerHTML = '<div class="empty-state"><h3>暂无API</h3><p>该分组下没有API端点</p></div>';
|
||||
main.innerHTML = '<div class="empty-state"><h3>' + escapeHtml(_t('apiDocs.noApis')) + '</h3><p>' + escapeHtml(_t('apiDocs.noEndpointsInGroup')) + '</p></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -192,8 +259,8 @@ function createEndpointCard(endpoint) {
|
||||
|
||||
const methodClass = endpoint.method.toLowerCase();
|
||||
const tags = endpoint.tags || [];
|
||||
const tagHtml = tags.map(tag => `<span class="api-tag">${tag}</span>`).join('');
|
||||
|
||||
const tagHtml = tags.map(tag => `<span class="api-tag">${escapeHtml(translateApiDocTag(tag))}</span>`).join('');
|
||||
const summaryText = translateApiDocSummaryFromOp(endpoint);
|
||||
card.innerHTML = `
|
||||
<div class="api-endpoint-header">
|
||||
<div class="api-endpoint-title">
|
||||
@@ -204,21 +271,21 @@ function createEndpointCard(endpoint) {
|
||||
</div>
|
||||
<div class="api-endpoint-body">
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">描述</div>
|
||||
${endpoint.summary ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(endpoint.summary)}</div>` : ''}
|
||||
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionDescription'))}</div>
|
||||
${summaryText ? `<div class="api-description" style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(summaryText)}</div>` : ''}
|
||||
${endpoint.description ? `
|
||||
<div class="api-description-toggle">
|
||||
<button class="description-toggle-btn" onclick="toggleDescription(this)">
|
||||
<svg class="description-toggle-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
<span>查看详细说明</span>
|
||||
<span>${escapeHtml(_t('apiDocs.viewDetailDesc'))}</span>
|
||||
</button>
|
||||
<div class="api-description-detail" style="display: none;">
|
||||
${formatDescription(endpoint.description)}
|
||||
</div>
|
||||
</div>
|
||||
` : endpoint.summary ? '' : '<div class="api-description">无描述</div>'}
|
||||
` : endpoint.summary ? '' : '<div class="api-description">' + escapeHtml(_t('apiDocs.noDescription')) + '</div>'}
|
||||
</div>
|
||||
|
||||
${renderParameters(endpoint)}
|
||||
@@ -236,8 +303,10 @@ function renderParameters(endpoint) {
|
||||
const params = endpoint.parameters || [];
|
||||
if (params.length === 0) return '';
|
||||
|
||||
const requiredLabel = escapeHtml(_t('apiDocs.required'));
|
||||
const optionalLabel = escapeHtml(_t('apiDocs.optional'));
|
||||
const rows = params.map(param => {
|
||||
const required = param.required ? '<span class="api-param-required">必需</span>' : '<span class="api-param-optional">可选</span>';
|
||||
const required = param.required ? '<span class="api-param-required">' + requiredLabel + '</span>' : '<span class="api-param-optional">' + optionalLabel + '</span>';
|
||||
// 处理描述文本,将换行符转换为<br>
|
||||
let descriptionHtml = '-';
|
||||
if (param.description) {
|
||||
@@ -255,17 +324,20 @@ function renderParameters(endpoint) {
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
const paramName = escapeHtml(_t('apiDocs.paramName'));
|
||||
const typeLabel = escapeHtml(_t('apiDocs.type'));
|
||||
const descLabel = escapeHtml(_t('apiDocs.description'));
|
||||
return `
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">参数</div>
|
||||
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionParams'))}</div>
|
||||
<div class="api-table-wrapper">
|
||||
<table class="api-params-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>参数名</th>
|
||||
<th>类型</th>
|
||||
<th>描述</th>
|
||||
<th>必需</th>
|
||||
<th>${paramName}</th>
|
||||
<th>${typeLabel}</th>
|
||||
<th>${descLabel}</th>
|
||||
<th>${requiredLabel}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -297,11 +369,13 @@ function renderRequestBody(endpoint) {
|
||||
let paramsTable = '';
|
||||
if (schema.properties) {
|
||||
const requiredFields = schema.required || [];
|
||||
const reqLabel = escapeHtml(_t('apiDocs.required'));
|
||||
const optLabel = escapeHtml(_t('apiDocs.optional'));
|
||||
const rows = Object.keys(schema.properties).map(key => {
|
||||
const prop = schema.properties[key];
|
||||
const required = requiredFields.includes(key)
|
||||
? '<span class="api-param-required">必需</span>'
|
||||
: '<span class="api-param-optional">可选</span>';
|
||||
? '<span class="api-param-required">' + reqLabel + '</span>'
|
||||
: '<span class="api-param-optional">' + optLabel + '</span>';
|
||||
|
||||
// 处理嵌套类型
|
||||
let typeDisplay = prop.type || 'object';
|
||||
@@ -338,16 +412,20 @@ function renderRequestBody(endpoint) {
|
||||
}).join('');
|
||||
|
||||
if (rows) {
|
||||
const pName = escapeHtml(_t('apiDocs.paramName'));
|
||||
const tLabel = escapeHtml(_t('apiDocs.type'));
|
||||
const dLabel = escapeHtml(_t('apiDocs.description'));
|
||||
const exLabel = escapeHtml(_t('apiDocs.example'));
|
||||
paramsTable = `
|
||||
<div class="api-table-wrapper" style="margin-top: 12px;">
|
||||
<table class="api-params-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>参数名</th>
|
||||
<th>类型</th>
|
||||
<th>描述</th>
|
||||
<th>必需</th>
|
||||
<th>示例</th>
|
||||
<th>${pName}</th>
|
||||
<th>${tLabel}</th>
|
||||
<th>${dLabel}</th>
|
||||
<th>${reqLabel}</th>
|
||||
<th>${exLabel}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -389,12 +467,12 @@ function renderRequestBody(endpoint) {
|
||||
|
||||
return `
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">请求体</div>
|
||||
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionRequestBody'))}</div>
|
||||
${endpoint.requestBody.description ? `<div class="api-description">${endpoint.requestBody.description}</div>` : ''}
|
||||
${paramsTable}
|
||||
${example ? `
|
||||
<div style="margin-top: 16px;">
|
||||
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">示例JSON:</div>
|
||||
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${escapeHtml(_t('apiDocs.exampleJson'))}</div>
|
||||
<div class="api-response-example">
|
||||
<pre>${escapeHtml(example)}</pre>
|
||||
</div>
|
||||
@@ -414,11 +492,11 @@ function renderResponses(endpoint) {
|
||||
if (schema.example) {
|
||||
example = JSON.stringify(schema.example, null, 2);
|
||||
}
|
||||
|
||||
const descText = translateApiDocResponseDescFromResp(response);
|
||||
return `
|
||||
<div style="margin-bottom: 16px;">
|
||||
<strong style="color: ${status.startsWith('2') ? 'var(--success-color)' : status.startsWith('4') ? 'var(--error-color)' : 'var(--warning-color)'}">${status}</strong>
|
||||
${response.description ? `<span style="color: var(--text-secondary); margin-left: 8px;">${response.description}</span>` : ''}
|
||||
${descText ? `<span style="color: var(--text-secondary); margin-left: 8px;">${escapeHtml(descText)}</span>` : ''}
|
||||
${example ? `
|
||||
<div class="api-response-example" style="margin-top: 8px;">
|
||||
<pre>${escapeHtml(example)}</pre>
|
||||
@@ -432,7 +510,7 @@ function renderResponses(endpoint) {
|
||||
|
||||
return `
|
||||
<div class="api-section">
|
||||
<div class="api-section-title">响应</div>
|
||||
<div class="api-section-title">${escapeHtml(_t('apiDocs.sectionResponse'))}</div>
|
||||
${responseItems}
|
||||
</div>
|
||||
`;
|
||||
@@ -462,8 +540,8 @@ function renderTestSection(endpoint) {
|
||||
const bodyInputId = `test-body-${escapeId(path)}-${method}`;
|
||||
bodyInput = `
|
||||
<div class="api-test-input-group">
|
||||
<label>请求体 (JSON)</label>
|
||||
<textarea id="${bodyInputId}" class="test-body-input" placeholder='请输入JSON格式的请求体'>${defaultBody}</textarea>
|
||||
<label>${escapeHtml(_t('apiDocs.requestBodyJson'))}</label>
|
||||
<textarea id="${bodyInputId}" class="test-body-input" placeholder='${escapeHtml(_t('apiDocs.requestBodyPlaceholder'))}'>${defaultBody}</textarea>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -491,7 +569,7 @@ function renderTestSection(endpoint) {
|
||||
const inputId = `test-query-${param.name}-${escapeId(path)}-${method}`;
|
||||
const defaultValue = param.schema?.default !== undefined ? param.schema.default : '';
|
||||
const placeholder = param.description || param.name;
|
||||
const required = param.required ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">可选</span>';
|
||||
const required = param.required ? '<span style="color: var(--error-color);">*</span>' : '<span style="color: var(--text-muted);">' + escapeHtml(_t('apiDocs.optional')) + '</span>';
|
||||
return `
|
||||
<div class="api-test-input-group">
|
||||
<label>${param.name} ${required}</label>
|
||||
@@ -505,33 +583,40 @@ function renderTestSection(endpoint) {
|
||||
}).join('');
|
||||
}
|
||||
|
||||
const testSectionTitle = escapeHtml(_t('apiDocs.testSection'));
|
||||
const queryParamsTitle = escapeHtml(_t('apiDocs.queryParams'));
|
||||
const sendRequestLabel = escapeHtml(_t('apiDocs.sendRequest'));
|
||||
const copyCurlLabel = escapeHtml(_t('apiDocs.copyCurl'));
|
||||
const clearResultLabel = escapeHtml(_t('apiDocs.clearResult'));
|
||||
const copyCurlTitle = escapeHtml(_t('apiDocs.copyCurlTitle'));
|
||||
const clearResultTitle = escapeHtml(_t('apiDocs.clearResultTitle'));
|
||||
return `
|
||||
<div class="api-test-section">
|
||||
<div class="api-section-title">测试接口</div>
|
||||
<div class="api-section-title">${testSectionTitle}</div>
|
||||
<div class="api-test-form">
|
||||
${pathParamsInput}
|
||||
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">查询参数:</div>${queryParamsInput}</div>` : ''}
|
||||
${queryParamsInput ? `<div style="margin-top: 16px;"><div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${queryParamsTitle}</div>${queryParamsInput}</div>` : ''}
|
||||
${bodyInput}
|
||||
<div class="api-test-buttons">
|
||||
<button class="api-test-btn primary" onclick="testAPI('${method}', '${escapeHtml(path)}', '${endpoint.operationId || ''}')">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polygon points="5 3 19 12 5 21 5 3"/>
|
||||
</svg>
|
||||
发送请求
|
||||
${sendRequestLabel}
|
||||
</button>
|
||||
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="复制curl命令">
|
||||
<button class="api-test-btn copy-curl" onclick="copyCurlCommand(event, '${method}', '${escapeHtml(path)}')" title="${copyCurlTitle}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
复制curl
|
||||
${copyCurlLabel}
|
||||
</button>
|
||||
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="清除测试结果">
|
||||
<button class="api-test-btn clear-result" onclick="clearTestResult('${escapeId(path)}-${method}')" title="${clearResultTitle}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
</svg>
|
||||
清除结果
|
||||
${clearResultLabel}
|
||||
</button>
|
||||
</div>
|
||||
<div id="test-result-${escapeId(path)}-${method}" class="api-test-result" style="display: none;"></div>
|
||||
@@ -548,7 +633,7 @@ async function testAPI(method, path, operationId) {
|
||||
|
||||
resultDiv.style.display = 'block';
|
||||
resultDiv.className = 'api-test-result loading';
|
||||
resultDiv.textContent = '发送请求中...';
|
||||
resultDiv.textContent = _t('apiDocs.sendingRequest');
|
||||
|
||||
try {
|
||||
// 替换路径参数
|
||||
@@ -561,7 +646,7 @@ async function testAPI(method, path, operationId) {
|
||||
if (input && input.value) {
|
||||
actualPath = actualPath.replace(param, encodeURIComponent(input.value));
|
||||
} else {
|
||||
throw new Error(`路径参数 ${paramName} 不能为空`);
|
||||
throw new Error(_t('apiDocs.errorPathParamRequired', { name: paramName }));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -580,7 +665,7 @@ async function testAPI(method, path, operationId) {
|
||||
if (input && input.value !== '' && input.value !== null && input.value !== undefined) {
|
||||
queryParams.push(`${encodeURIComponent(param.name)}=${encodeURIComponent(input.value)}`);
|
||||
} else if (param.required) {
|
||||
throw new Error(`查询参数 ${param.name} 不能为空`);
|
||||
throw new Error(_t('apiDocs.errorQueryParamRequired', { name: param.name }));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -602,8 +687,7 @@ async function testAPI(method, path, operationId) {
|
||||
if (currentToken) {
|
||||
options.headers['Authorization'] = 'Bearer ' + currentToken;
|
||||
} else {
|
||||
// 如果没有token,提示用户
|
||||
throw new Error('未检测到 Token。请先在前端页面登录,然后刷新此页面。或者手动在请求头中添加 Authorization: Bearer your_token');
|
||||
throw new Error(_t('apiDocs.errorTokenRequired'));
|
||||
}
|
||||
|
||||
// 添加请求体
|
||||
@@ -614,7 +698,7 @@ async function testAPI(method, path, operationId) {
|
||||
try {
|
||||
options.body = JSON.stringify(JSON.parse(bodyInput.value.trim()));
|
||||
} catch (e) {
|
||||
throw new Error('请求体JSON格式错误: ' + e.message);
|
||||
throw new Error(_t('apiDocs.errorJsonInvalid') + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -636,7 +720,7 @@ async function testAPI(method, path, operationId) {
|
||||
|
||||
} catch (error) {
|
||||
resultDiv.className = 'api-test-result error';
|
||||
resultDiv.textContent = '请求失败: ' + error.message;
|
||||
resultDiv.textContent = _t('apiDocs.requestFailed') + error.message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -727,17 +811,17 @@ function copyCurlCommand(event, method, path) {
|
||||
// 复制到剪贴板
|
||||
const button = event ? event.target.closest('button') : null;
|
||||
navigator.clipboard.writeText(curlCommand).then(() => {
|
||||
// 显示成功提示
|
||||
if (button) {
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
|
||||
const copiedLabel = escapeHtml(_t('apiDocs.copied'));
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>' + copiedLabel;
|
||||
button.style.color = 'var(--success-color)';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.style.color = '';
|
||||
}, 2000);
|
||||
} else {
|
||||
alert('curl命令已复制到剪贴板!');
|
||||
alert(_t('apiDocs.curlCopied'));
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
@@ -752,24 +836,25 @@ function copyCurlCommand(event, method, path) {
|
||||
document.execCommand('copy');
|
||||
if (button) {
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>已复制';
|
||||
const copiedLabel = escapeHtml(_t('apiDocs.copied'));
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"/></svg>' + copiedLabel;
|
||||
button.style.color = 'var(--success-color)';
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalText;
|
||||
button.style.color = '';
|
||||
}, 2000);
|
||||
} else {
|
||||
alert('curl命令已复制到剪贴板!');
|
||||
alert(_t('apiDocs.curlCopied'));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('复制失败,请手动复制:\n\n' + curlCommand);
|
||||
alert(_t('apiDocs.copyFailedManual') + curlCommand);
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('生成curl命令失败:', error);
|
||||
alert('生成curl命令失败: ' + error.message);
|
||||
alert(_t('apiDocs.curlGenFailed') + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -935,10 +1020,10 @@ function toggleDescription(button) {
|
||||
if (detail.style.display === 'none') {
|
||||
detail.style.display = 'block';
|
||||
icon.style.transform = 'rotate(180deg)';
|
||||
span.textContent = '隐藏详细说明';
|
||||
span.textContent = typeof window.t === 'function' ? window.t('apiDocs.hideDetailDesc') : '隐藏详细说明';
|
||||
} else {
|
||||
detail.style.display = 'none';
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
span.textContent = '查看详细说明';
|
||||
span.textContent = typeof window.t === 'function' ? window.t('apiDocs.viewDetailDesc') : '查看详细说明';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+30
-31
@@ -755,7 +755,7 @@ function renderMentionSuggestions({ showLoading = false } = {}) {
|
||||
const disabledClass = toolEnabled ? '' : 'disabled';
|
||||
const badge = tool.isExternal ? '<span class="mention-item-badge">外部</span>' : '<span class="mention-item-badge internal">内置</span>';
|
||||
const nameHtml = escapeHtml(tool.name);
|
||||
const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : '暂无描述';
|
||||
const description = tool.description && tool.description.length > 0 ? escapeHtml(tool.description) : (typeof window.t === 'function' ? window.t('chat.noDescription') : '暂无描述');
|
||||
const descHtml = `<div class="mention-item-desc">${description}</div>`;
|
||||
// 根据工具在当前角色中的启用状态显示状态标签
|
||||
const statusLabel = toolEnabled ? '可用' : (tool.roleEnabled !== undefined ? '已禁用(当前角色)' : '已禁用');
|
||||
@@ -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 = `<span>调用 #${index + 1}</span>`;
|
||||
detailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.callNumber', { n: index + 1 }) : '调用 #' + (index + 1)) + '</span>';
|
||||
detailBtn.onclick = () => showMCPDetail(execId);
|
||||
buttonsContainer.appendChild(detailBtn);
|
||||
// 异步获取工具名称并更新按钮文本
|
||||
@@ -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 = '<div class="loading-spinner">加载中...</div>';
|
||||
container.innerHTML = '<div class="loading-spinner">' + (typeof window.t === 'function' ? window.t('chat.loading') : '加载中...') + '</div>';
|
||||
}
|
||||
|
||||
// 隐藏详情面板
|
||||
@@ -2355,7 +2354,7 @@ async function loadAttackChain(conversationId) {
|
||||
console.error('加载攻击链失败:', error);
|
||||
const container = document.getElementById('attack-chain-container');
|
||||
if (container) {
|
||||
container.innerHTML = `<div class="error-message">加载失败: ${error.message}</div>`;
|
||||
container.innerHTML = '<div class="error-message">' + (typeof window.t === 'function' ? window.t('chat.loadFailed', { message: error.message }) : '加载失败: ' + error.message) + '</div>';
|
||||
}
|
||||
// 错误时也重置加载状态
|
||||
setAttackChainLoading(conversationId, false);
|
||||
@@ -2381,7 +2380,7 @@ function renderAttackChain(chainData) {
|
||||
container.innerHTML = '';
|
||||
|
||||
if (!chainData.nodes || chainData.nodes.length === 0) {
|
||||
container.innerHTML = '<div class="empty-message">暂无攻击链数据</div>';
|
||||
container.innerHTML = '<div class="empty-message">' + (typeof window.t === 'function' ? window.t('chat.noAttackChainData') : '暂无攻击链数据') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5544,9 +5543,9 @@ async function loadGroupConversations(groupId, searchQuery = '') {
|
||||
|
||||
// 显示加载状态
|
||||
if (searchQuery) {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">搜索中...</div>';
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.searching') : '搜索中...') + '</div>';
|
||||
} else {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载中...</div>';
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.loading') : '加载中...') + '</div>';
|
||||
}
|
||||
|
||||
// 构建URL,如果有搜索关键词则添加search参数
|
||||
@@ -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 = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">加载失败,请重试</div>';
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.loadFailedRetry') : '加载失败,请重试') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5572,7 +5571,7 @@ async function loadGroupConversations(groupId, searchQuery = '') {
|
||||
// 验证返回的数据类型
|
||||
if (!Array.isArray(groupConvs)) {
|
||||
console.error(`Invalid response for group ${groupId}:`, groupConvs);
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">数据格式错误</div>';
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (typeof window.t === 'function' ? window.t('chat.dataFormatError') : '数据格式错误') + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+108
-80
@@ -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 = `
|
||||
<div class="progress-header">
|
||||
<span class="progress-title">🔍 渗透测试进行中...</span>
|
||||
<span class="progress-title">🔍 ${progressTitleText}</span>
|
||||
<div class="progress-actions">
|
||||
<button class="progress-stop" id="${id}-stop-btn" onclick="cancelProgressTask('${id}')">停止任务</button>
|
||||
<button class="progress-toggle" onclick="toggleProgressDetails('${id}')">收起详情</button>
|
||||
<button class="progress-stop" id="${id}-stop-btn" onclick="cancelProgressTask('${id}')">${stopTaskText}</button>
|
||||
<button class="progress-toggle" onclick="toggleProgressDetails('${id}')">${collapseDetailText}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-timeline expanded" id="${id}-timeline"></div>
|
||||
@@ -123,10 +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 = '<span>展开详情</span>';
|
||||
btn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,7 +274,7 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) {
|
||||
// 设置详情内容(如果有错误,默认折叠;否则默认折叠)
|
||||
detailsContainer.innerHTML = `
|
||||
<div class="process-details-content">
|
||||
${hasContent ? `<div class="progress-timeline" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">暂无过程详情</div>'}
|
||||
${hasContent ? `<div class="progress-timeline" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>'}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -258,10 +286,9 @@ function integrateProgressToMCPSection(progressId, assistantMessageId) {
|
||||
timeline.classList.remove('expanded');
|
||||
}
|
||||
|
||||
// 更新按钮文本为"展开详情"(因为默认折叠)
|
||||
const processDetailBtn = buttonsContainer.querySelector('.process-detail-btn');
|
||||
if (processDetailBtn) {
|
||||
processDetailBtn.innerHTML = '<span>展开详情</span>';
|
||||
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,22 +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 = '<span>展开详情</span>';
|
||||
if (btn) btn.innerHTML = '<span>' + expandT + '</span>';
|
||||
} else {
|
||||
timeline.classList.add('expanded');
|
||||
if (btn) btn.innerHTML = '<span>收起详情</span>';
|
||||
if (btn) btn.innerHTML = '<span>' + collapseT + '</span>';
|
||||
}
|
||||
} else if (timeline) {
|
||||
// 如果只有timeline,直接切换
|
||||
if (timeline.classList.contains('expanded')) {
|
||||
timeline.classList.remove('expanded');
|
||||
if (btn) btn.innerHTML = '<span>展开详情</span>';
|
||||
if (btn) btn.innerHTML = '<span>' + expandT + '</span>';
|
||||
} else {
|
||||
timeline.classList.add('expanded');
|
||||
if (btn) btn.innerHTML = '<span>收起详情</span>';
|
||||
if (btn) btn.innerHTML = '<span>' + collapseT + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = `
|
||||
<div class="progress-header">
|
||||
<span class="progress-title">📋 渗透测试详情</span>
|
||||
<span class="progress-title">📋 ${penetrationDetailText}</span>
|
||||
${hasContent ? `<button class="progress-toggle" onclick="toggleProgressDetails('${detailsId}')">${toggleText}</button>` : ''}
|
||||
</div>
|
||||
${hasContent ? `<div class="progress-timeline ${expandedClass}" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">暂无过程详情(可能执行过快或未触发详细事件)</div>'}
|
||||
${hasContent ? `<div class="progress-timeline ${expandedClass}" id="${detailsId}-timeline">${timelineHTML}</div>` : '<div class="progress-timeline-empty">' + noProcessDetailText + '</div>'}
|
||||
`;
|
||||
|
||||
contentWrapper.appendChild(bubble);
|
||||
@@ -466,41 +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 = ' <span class="tool-status-badge tool-status-running">执行中...</span>';
|
||||
statusText = ' <span class="tool-status-badge tool-status-running">' + escapeHtml(runningLabel) + '</span>';
|
||||
} else if (status === 'completed') {
|
||||
item.classList.add('tool-call-completed');
|
||||
statusText = ' <span class="tool-status-badge tool-status-completed">✅ 已完成</span>';
|
||||
statusText = ' <span class="tool-status-badge tool-status-completed">✅ ' + escapeHtml(completedLabel) + '</span>';
|
||||
} else if (status === 'failed') {
|
||||
item.classList.add('tool-call-failed');
|
||||
statusText = ' <span class="tool-status-badge tool-status-failed">❌ 执行失败</span>';
|
||||
statusText = ' <span class="tool-status-badge tool-status-failed">❌ ' + escapeHtml(failedLabel) + '</span>';
|
||||
}
|
||||
|
||||
// 更新标题(保留原有文本,追加状态)
|
||||
@@ -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 += `
|
||||
<div class="timeline-item-content">
|
||||
<div class="tool-details">
|
||||
<div class="tool-arg-section">
|
||||
<strong>参数:</strong>
|
||||
<strong>${escapeHtml(paramsLabel)}</strong>
|
||||
<pre class="tool-args">${escapeHtml(JSON.stringify(args, null, 2))}</pre>
|
||||
</div>
|
||||
</div>
|
||||
@@ -882,22 +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 += `
|
||||
<div class="timeline-item-content">
|
||||
<div class="tool-result-section ${isError ? 'error' : 'success'}">
|
||||
<strong>执行结果:</strong>
|
||||
<strong>${escapeHtml(execResultLabel)}</strong>
|
||||
<pre class="tool-result">${escapeHtml(resultStr)}</pre>
|
||||
${data.executionId ? `<div class="tool-execution-id">执行ID: <code>${escapeHtml(data.executionId)}</code></div>` : ''}
|
||||
${data.executionId ? `<div class="tool-execution-id">${escapeHtml(execIdLabel)} <code>${escapeHtml(data.executionId)}</code></div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
} else if (type === 'cancelled') {
|
||||
const taskCancelledLabel = typeof window.t === 'function' ? window.t('chat.taskCancelled') : '任务已取消';
|
||||
content += `
|
||||
<div class="timeline-item-content">
|
||||
${escapeHtml(options.message || '任务已取消')}
|
||||
${escapeHtml(options.message || taskCancelledLabel)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -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 = `
|
||||
<div class="active-task-info">
|
||||
<span class="active-task-status">${statusText}</span>
|
||||
<span class="active-task-message">${escapeHtml(task.message || '未命名任务')}</span>
|
||||
<span class="active-task-message">${escapeHtml(task.message || unnamedTaskText)}</span>
|
||||
</div>
|
||||
<div class="active-task-actions">
|
||||
${timeText ? `<span class="active-task-time">${timeText}</span>` : ''}
|
||||
${!isFinalStatus ? '<button class="active-task-cancel">停止任务</button>' : ''}
|
||||
${!isFinalStatus ? '<button class="active-task-cancel">' + stopTaskBtnText + '</button>' : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -994,7 +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();
|
||||
});
|
||||
|
||||
@@ -1433,6 +1433,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
updateRoleSelectorDisplay();
|
||||
});
|
||||
|
||||
// 语言切换后刷新角色选择器显示(默认/自定义角色名)
|
||||
document.addEventListener('languagechange', () => {
|
||||
updateRoleSelectorDisplay();
|
||||
});
|
||||
|
||||
// 获取当前选中的角色(供chat.js使用)
|
||||
function getCurrentRole() {
|
||||
return currentRole || '';
|
||||
|
||||
+30
-15
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API 文档 - CyberStrikeAI</title>
|
||||
<title data-i18n="apiDocs.pageTitle">API 文档 - CyberStrikeAI</title>
|
||||
<link rel="icon" type="image/png" href="/static/logo.png">
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
<style>
|
||||
@@ -22,6 +22,7 @@
|
||||
}
|
||||
|
||||
.api-docs-header {
|
||||
position: relative;
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
@@ -833,9 +834,21 @@
|
||||
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
API 文档
|
||||
<span data-i18n="apiDocs.title">API 文档</span>
|
||||
</h1>
|
||||
<p>CyberStrikeAI 平台 API 接口文档,支持在线测试</p>
|
||||
<p data-i18n="apiDocs.subtitle">CyberStrikeAI 平台 API 接口文档,支持在线测试</p>
|
||||
<div class="api-docs-lang-switcher" style="position: absolute; top: 24px; right: 24px;">
|
||||
<div class="lang-switcher">
|
||||
<button type="button" class="btn-secondary lang-switcher-btn" onclick="typeof toggleLangDropdown === 'function' && toggleLangDropdown()" title="界面语言">
|
||||
<span class="lang-switcher-icon">🌐</span>
|
||||
<span id="current-lang-label">中文</span>
|
||||
</button>
|
||||
<div id="lang-dropdown" class="lang-dropdown" style="display: none;">
|
||||
<div class="lang-option" data-lang="zh-CN" onclick="typeof onLanguageSelect === 'function' && onLanguageSelect('zh-CN')">中文</div>
|
||||
<div class="lang-option" data-lang="en-US" onclick="typeof onLanguageSelect === 'function' && onLanguageSelect('en-US')">English</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="auth-info-section" class="auth-info-section" style="display: none;">
|
||||
@@ -846,17 +859,17 @@
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>
|
||||
<h3 style="margin: 0; font-size: 1rem; font-weight: 600; color: var(--text-primary);">API 认证说明</h3>
|
||||
<h3 style="margin: 0; font-size: 1rem; font-weight: 600; color: var(--text-primary);" data-i18n="apiDocs.authTitle">API 认证说明</h3>
|
||||
</div>
|
||||
<svg id="auth-info-arrow" class="auth-info-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="transition: transform 0.2s ease;">
|
||||
<polyline points="6 9 12 15 18 9"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div id="auth-info-body" class="auth-info-body" style="display: none; color: var(--text-secondary); font-size: 0.875rem; line-height: 1.6; margin-top: 16px;">
|
||||
<p style="margin: 0 0 12px 0;"><strong>所有 API 接口都需要 Token 认证。</strong></p>
|
||||
<p style="margin: 0 0 12px 0;"><strong data-i18n="apiDocs.authAllNeedToken">所有 API 接口都需要 Token 认证。</strong></p>
|
||||
<div style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin-bottom: 12px;">
|
||||
<p style="margin: 0 0 8px 0; font-weight: 500;">1. 获取 Token:</p>
|
||||
<p style="margin: 0 0 8px 0;">在前端页面登录后,Token 会自动保存。您也可以通过以下方式获取:</p>
|
||||
<p style="margin: 0 0 8px 0; font-weight: 500;" data-i18n="apiDocs.authGetToken">1. 获取 Token:</p>
|
||||
<p style="margin: 0 0 8px 0;" data-i18n="apiDocs.authGetTokenDesc">在前端页面登录后,Token 会自动保存。您也可以通过以下方式获取:</p>
|
||||
<pre style="background: var(--bg-primary); padding: 8px; border-radius: 4px; margin: 8px 0; overflow-x: auto; font-size: 0.8125rem;"><code>POST /api/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
@@ -871,13 +884,13 @@ Content-Type: application/json
|
||||
}</code></pre>
|
||||
</div>
|
||||
<div style="background: var(--bg-secondary); padding: 12px; border-radius: 6px; margin-bottom: 12px;">
|
||||
<p style="margin: 0 0 8px 0; font-weight: 500;">2. 使用 Token:</p>
|
||||
<p style="margin: 0 0 8px 0;">在请求头中添加 Authorization 字段:</p>
|
||||
<p style="margin: 0 0 8px 0; font-weight: 500;" data-i18n="apiDocs.authUseToken">2. 使用 Token:</p>
|
||||
<p style="margin: 0 0 8px 0;" data-i18n="apiDocs.authUseTokenDesc">在请求头中添加 Authorization 字段:</p>
|
||||
<pre style="background: var(--bg-primary); padding: 8px; border-radius: 4px; margin: 8px 0; overflow-x: auto; font-size: 0.8125rem;"><code>Authorization: Bearer your_token_here</code></pre>
|
||||
<p style="margin: 8px 0 0 0; font-size: 0.8125rem; color: var(--text-muted);">💡 提示:本页面会自动使用您已登录的 Token,无需手动填写。</p>
|
||||
<p style="margin: 8px 0 0 0; font-size: 0.8125rem; color: var(--text-muted);" data-i18n="apiDocs.authTip">💡 提示:本页面会自动使用您已登录的 Token,无需手动填写。</p>
|
||||
</div>
|
||||
<div id="token-status" style="display: none; background: rgba(0, 102, 255, 0.1); padding: 8px 12px; border-radius: 6px; border-left: 3px solid var(--accent-color);">
|
||||
<p style="margin: 0; font-size: 0.8125rem; color: var(--accent-color);">
|
||||
<p style="margin: 0; font-size: 0.8125rem; color: var(--accent-color);" data-i18n="apiDocs.tokenDetected">
|
||||
<strong>✓ 已检测到 Token</strong> - 您可以直接测试 API 接口
|
||||
</p>
|
||||
</div>
|
||||
@@ -899,10 +912,10 @@ Content-Type: application/json
|
||||
|
||||
<div class="api-docs-content">
|
||||
<div class="api-docs-sidebar">
|
||||
<h3>API 分组</h3>
|
||||
<h3 data-i18n="apiDocs.sidebarGroupTitle">API 分组</h3>
|
||||
<ul class="api-group-list" id="api-group-list">
|
||||
<li class="api-group-item">
|
||||
<a href="#" class="api-group-link active" data-group="all">全部接口</a>
|
||||
<a href="#" class="api-group-link active" data-group="all" data-i18n="apiDocs.allApis">全部接口</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -914,13 +927,15 @@ Content-Type: application/json
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>
|
||||
<h3>加载中...</h3>
|
||||
<p>正在加载 API 文档</p>
|
||||
<h3 data-i18n="apiDocs.loading">加载中...</h3>
|
||||
<p data-i18n="apiDocs.loadingDesc">正在加载 API 文档</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/i18next@23.11.5/i18next.min.js"></script>
|
||||
<script src="/static/js/i18n.js"></script>
|
||||
<script src="/static/js/api-docs.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user