Add files via upload

This commit is contained in:
公明
2026-03-09 21:30:32 +08:00
committed by GitHub
parent b7e724407b
commit 317461e259
7 changed files with 535 additions and 249 deletions
+135 -11
View File
@@ -14,7 +14,8 @@
"confirm": "Confirm", "confirm": "Confirm",
"copy": "Copy", "copy": "Copy",
"copied": "Copied", "copied": "Copied",
"copyFailed": "Copy failed" "copyFailed": "Copy failed",
"view": "View"
}, },
"header": { "header": {
"title": "CyberStrikeAI", "title": "CyberStrikeAI",
@@ -340,6 +341,7 @@
"configStdioNeedCommand": "Config error: \"{{name}}\" stdio mode needs command", "configStdioNeedCommand": "Config error: \"{{name}}\" stdio mode needs command",
"configHttpNeedUrl": "Config error: \"{{name}}\" http mode needs url", "configHttpNeedUrl": "Config error: \"{{name}}\" http mode needs url",
"configSseNeedUrl": "Config error: \"{{name}}\" sse mode needs url", "configSseNeedUrl": "Config error: \"{{name}}\" sse mode needs url",
"configEditMustContainName": "Config error: In edit mode, JSON must contain config name \"{{name}}\"",
"saveSuccess": "Saved", "saveSuccess": "Saved",
"deleteSuccess": "Deleted", "deleteSuccess": "Deleted",
"deleteExternalConfirm": "Delete external MCP \"{{name}}\"?", "deleteExternalConfirm": "Delete external MCP \"{{name}}\"?",
@@ -364,15 +366,36 @@
"description": "Configure WeCom, DingTalk and Lark bots so you can chat with CyberStrikeAI on your phone without opening the web UI.", "description": "Configure WeCom, DingTalk and Lark bots so you can chat with CyberStrikeAI on your phone without opening the web UI.",
"wecom": { "wecom": {
"title": "WeCom", "title": "WeCom",
"enabled": "Enable WeCom bot" "enabled": "Enable WeCom bot",
"token": "Token",
"tokenPlaceholder": "Token",
"encodingAesKey": "EncodingAESKey",
"encodingAesKeyPlaceholder": "EncodingAESKey (leave empty for plain mode)",
"corpId": "CorpID",
"corpIdPlaceholder": "Corp ID",
"secret": "Secret",
"secretPlaceholder": "App Secret",
"agentId": "AgentID",
"agentIdPlaceholder": "App AgentId"
}, },
"dingtalk": { "dingtalk": {
"title": "DingTalk", "title": "DingTalk",
"enabled": "Enable DingTalk bot" "enabled": "Enable DingTalk bot",
"clientIdLabel": "Client ID (AppKey)",
"clientIdPlaceholder": "DingTalk App Key",
"clientSecretLabel": "Client Secret",
"clientSecretPlaceholder": "DingTalk App Secret",
"streamHint": "Enable bot capability and configure streaming access in the open platform."
}, },
"lark": { "lark": {
"title": "Lark", "title": "Lark",
"enabled": "Enable Lark bot" "enabled": "Enable Lark bot",
"appIdLabel": "App ID",
"appIdPlaceholder": "Lark/Feishu App ID",
"appSecretLabel": "App Secret",
"appSecretPlaceholder": "Lark/Feishu App Secret",
"verifyTokenLabel": "Verify Token (Optional)",
"verifyTokenPlaceholder": "Event subscription Verification Token"
} }
}, },
"apply": { "apply": {
@@ -414,7 +437,15 @@
"title": "Role management", "title": "Role management",
"createRole": "Create role", "createRole": "Create role",
"searchPlaceholder": "Search roles...", "searchPlaceholder": "Search roles...",
"deleteConfirm": "Delete this role?" "deleteConfirm": "Delete this role?",
"loadFailed": "Failed to load roles",
"noDescription": "No description",
"defaultRoleDescription": "Default role, no extra user prompt, uses default MCP",
"noMatchingRoles": "No matching roles",
"noRoles": "No roles",
"enabled": "Enabled",
"disabled": "Disabled",
"noDescriptionShort": "No description"
}, },
"skills": { "skills": {
"title": "Skills management", "title": "Skills management",
@@ -436,7 +467,36 @@
"loadStatsFailed": "Failed to load skills monitor data", "loadStatsFailed": "Failed to load skills monitor data",
"clearStatsConfirm": "Clear all Skills statistics? This cannot be undone.", "clearStatsConfirm": "Clear all Skills statistics? This cannot be undone.",
"statsCleared": "Skills statistics cleared", "statsCleared": "Skills statistics cleared",
"clearStatsFailed": "Failed to clear statistics" "clearStatsFailed": "Failed to clear statistics",
"noDescription": "No description",
"viewSkillTitle": "View Skill: {{name}}",
"descriptionLabel": "Description:",
"pathLabel": "Path:",
"modTimeLabel": "Modified:",
"contentLabel": "Content:",
"nameRequired": "Skill name is required",
"contentRequired": "Skill content is required",
"nameInvalid": "Skill name can only contain letters, numbers, hyphens and underscores",
"saveSuccess": "Skill updated",
"createdSuccess": "Skill created",
"deleteConfirm": "Are you sure you want to delete skill \"{{name}}\"? This cannot be undone.",
"deleteConfirmWithRoles": "Are you sure you want to delete skill \"{{name}}\"?\n\n⚠️ This skill is currently bound to {{count}} role(s):\n{{roles}}\n\nAfter deletion, the system will automatically remove this skill from those roles.\n\nThis cannot be undone. Continue?",
"deleteSuccess": "Skill deleted",
"deleteSuccessWithRoles": "Skill deleted and automatically removed from {{count}} role(s): {{roles}}",
"loadFailedShort": "Load failed",
"totalSkillsCount": "Total Skills",
"totalCallsCount": "Total Call Count",
"successfulCalls": "Successful Calls",
"failedCalls": "Failed Calls",
"successRate": "Success Rate",
"skillName": "Skill Name",
"totalCalls": "Total Calls",
"success": "Success",
"failure": "Failure",
"lastCallTime": "Last Call Time",
"noCallRecords": "No Skills call records yet",
"loadStatsErrorShort": "Failed to load statistics",
"loadCallStatsError": "Failed to load call statistics"
}, },
"apiDocs": { "apiDocs": {
"curlCopied": "curl command copied to clipboard!" "curlCopied": "curl command copied to clipboard!"
@@ -580,6 +640,15 @@
"presetLogin": "Login page + China", "presetLogin": "Login page + China",
"presetDomain": "By domain", "presetDomain": "By domain",
"presetIp": "By IP", "presetIp": "By IP",
"queryPresetsAria": "FOFA query presets",
"fieldsPresetsAria": "FOFA field presets",
"resultsToolbarAria": "Results toolbar",
"fillExample": "Fill example",
"parseBtnTitle": "Parse natural language to FOFA query",
"minFieldsTitle": "For quick export",
"webCommonTitle": "For browsing and filtering",
"intelEnhancedTitle": "More fingerprint/intel",
"fullLabel": "full",
"nlPlaceholder": "e.g. Apache sites in Missouri, US, title contains Home", "nlPlaceholder": "e.g. Apache sites in Missouri, US, title contains Home",
"showHideColumns": "Show/hide columns", "showHideColumns": "Show/hide columns",
"exportCsvTitle": "Export results as CSV (UTF-8)", "exportCsvTitle": "Export results as CSV (UTF-8)",
@@ -617,7 +686,14 @@
"clearStatsTitle": "Clear all statistics", "clearStatsTitle": "Clear all statistics",
"skillsCallStats": "Skills call stats", "skillsCallStats": "Skills call stats",
"searchPlaceholder": "Search Skills...", "searchPlaceholder": "Search Skills...",
"loading": "Loading..." "loading": "Loading...",
"paginationShow": "Show {{start}}-{{end}} of {{total}}",
"perPageLabel": "Per page",
"firstPage": "First",
"prevPage": "Previous",
"pageOf": "Page {{current}} / {{total}}",
"nextPage": "Next",
"lastPage": "Last"
}, },
"settingsBasic": { "settingsBasic": {
"basicTitle": "Basic settings", "basicTitle": "Basic settings",
@@ -702,8 +778,20 @@
"changePasswordBtn": "Change password" "changePasswordBtn": "Change password"
}, },
"settingsRobotsExtra": { "settingsRobotsExtra": {
"botCommandsTitle": "Bot commands", "botCommandsTitle": "Bot command instructions",
"botCommandsDesc": "You can send these commands in chat (Chinese and English supported):" "botCommandsDesc": "You can send the following commands in chat (Chinese and English supported):",
"botCmdHelp": "Show this help",
"botCmdList": "List conversations",
"botCmdSwitch": "Switch to conversation",
"botCmdNew": "Start new conversation",
"botCmdClear": "Clear context",
"botCmdCurrent": "Show current conversation",
"botCmdStop": "Stop running task",
"botCmdRoles": "List roles",
"botCmdRole": "Switch role",
"botCmdDelete": "Delete conversation",
"botCmdVersion": "Show version",
"botCommandsFooter": "Otherwise, send any text for AI penetration testing / security analysis."
}, },
"mcpDetailModal": { "mcpDetailModal": {
"title": "Tool call details", "title": "Tool call details",
@@ -752,6 +840,15 @@
"configJson": "Config JSON", "configJson": "Config JSON",
"formatLabel": "Format:", "formatLabel": "Format:",
"formatDesc": "JSON object; key = config name, value = config. Use Start/Stop buttons to control state.", "formatDesc": "JSON object; key = config name, value = config. Use Start/Stop buttons to control state.",
"configExample": "Configuration example:",
"stdioMode": "stdio mode:",
"httpMode": "HTTP mode:",
"sseMode": "SSE mode:",
"placeholder": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\"],\n \"description\": \"Description\",\n \"timeout\": 300\n }\n}",
"exampleStdio": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\", \"--server\", \"http://example.com\"],\n \"description\": \"Description\",\n \"timeout\": 300\n }\n}",
"exampleHttp": "{\n \"cyberstrike-ai-http\": {\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:8081/mcp\"\n }\n}",
"exampleSse": "{\n \"cyberstrike-ai-sse\": {\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:8081/mcp/sse\"\n }\n}",
"exampleDescription": "Example description",
"formatJson": "Format JSON", "formatJson": "Format JSON",
"loadExample": "Load example" "loadExample": "Load example"
}, },
@@ -765,7 +862,7 @@
"descriptionPlaceholder": "Short description", "descriptionPlaceholder": "Short description",
"contentLabel": "Content (Markdown)", "contentLabel": "Content (Markdown)",
"contentPlaceholder": "Enter skill content in Markdown...", "contentPlaceholder": "Enter skill content in Markdown...",
"contentHint": "YAML front matter supported (optional)" "contentHint": "YAML front matter supported (optional), e.g.:"
}, },
"knowledgeItemModal": { "knowledgeItemModal": {
"addKnowledge": "Add knowledge", "addKnowledge": "Add knowledge",
@@ -920,6 +1017,33 @@
"searchSkillsPlaceholder": "Search skill...", "searchSkillsPlaceholder": "Search skill...",
"loadingSkills": "Loading skills...", "loadingSkills": "Loading skills...",
"relatedSkillsHint": "Selected skills are injected into system prompt before task execution.", "relatedSkillsHint": "Selected skills are injected into system prompt before task execution.",
"enableRole": "Enable this role" "enableRole": "Enable this role",
"selectAll": "Select All",
"deselectAll": "Deselect All",
"roleNameRequired": "Role name is required",
"roleNotFound": "Role not found",
"firstRoleNoToolsHint": "First role with no tools selected will use all tools by default.",
"currentPageSelected": "Current page: {{current}} / {{total}}",
"totalSelected": "Total selected: {{current}} / {{total}}",
"usingAllEnabledTools": "(Using all enabled tools)",
"currentPageSelectedTitle": "Selected on current page (enabled tools only)",
"totalSelectedTitle": "Total tools linked to this role",
"skillsSelectedCount": "Selected {{count}} / {{total}}",
"loadToolsFailed": "Failed to load tools",
"loadSkillsFailed": "Failed to load skills",
"cannotDeleteDefaultRole": "Cannot delete default role",
"noMatchingSkills": "No matching skills",
"noSkillsAvailable": "No skills available",
"usingAllTools": "Use all tools",
"andNMore": " and {{count}} more",
"toolsLabel": "Tools:",
"noTools": "No tools",
"paginationShow": "{{start}}-{{end}} of {{total}} tools",
"paginationSearch": " (search: \"{{keyword}}\")",
"firstPage": "First",
"prevPage": "Previous",
"pageOf": "Page {{page}} / {{total}}",
"nextPage": "Next",
"lastPage": "Last"
} }
} }
+134 -10
View File
@@ -14,7 +14,8 @@
"confirm": "确认", "confirm": "确认",
"copy": "复制", "copy": "复制",
"copied": "已复制", "copied": "已复制",
"copyFailed": "复制失败" "copyFailed": "复制失败",
"view": "查看"
}, },
"header": { "header": {
"title": "CyberStrikeAI", "title": "CyberStrikeAI",
@@ -340,6 +341,7 @@
"configStdioNeedCommand": "配置错误: \"{{name}}\" stdio模式需要command字段", "configStdioNeedCommand": "配置错误: \"{{name}}\" stdio模式需要command字段",
"configHttpNeedUrl": "配置错误: \"{{name}}\" http模式需要url字段", "configHttpNeedUrl": "配置错误: \"{{name}}\" http模式需要url字段",
"configSseNeedUrl": "配置错误: \"{{name}}\" sse模式需要url字段", "configSseNeedUrl": "配置错误: \"{{name}}\" sse模式需要url字段",
"configEditMustContainName": "配置错误: 编辑模式下,JSON必须包含配置名称 \"{{name}}\"",
"saveSuccess": "保存成功", "saveSuccess": "保存成功",
"deleteSuccess": "删除成功", "deleteSuccess": "删除成功",
"deleteExternalConfirm": "确定要删除外部MCP \"{{name}}\" 吗?", "deleteExternalConfirm": "确定要删除外部MCP \"{{name}}\" 吗?",
@@ -364,15 +366,36 @@
"description": "配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。", "description": "配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。",
"wecom": { "wecom": {
"title": "企业微信", "title": "企业微信",
"enabled": "启用企业微信机器人" "enabled": "启用企业微信机器人",
"token": "Token",
"tokenPlaceholder": "Token",
"encodingAesKey": "EncodingAESKey",
"encodingAesKeyPlaceholder": "EncodingAESKey(明文模式可留空)",
"corpId": "CorpID",
"corpIdPlaceholder": "企业 ID",
"secret": "Secret",
"secretPlaceholder": "应用 Secret",
"agentId": "AgentID",
"agentIdPlaceholder": "应用 AgentId"
}, },
"dingtalk": { "dingtalk": {
"title": "钉钉", "title": "钉钉",
"enabled": "启用钉钉机器人" "enabled": "启用钉钉机器人",
"clientIdLabel": "Client ID (AppKey)",
"clientIdPlaceholder": "钉钉应用 AppKey",
"clientSecretLabel": "Client Secret",
"clientSecretPlaceholder": "钉钉应用 Secret",
"streamHint": "需开启机器人能力并配置流式接入"
}, },
"lark": { "lark": {
"title": "飞书 (Lark)", "title": "飞书 (Lark)",
"enabled": "启用飞书机器人" "enabled": "启用飞书机器人",
"appIdLabel": "App ID",
"appIdPlaceholder": "飞书应用 App ID",
"appSecretLabel": "App Secret",
"appSecretPlaceholder": "飞书应用 App Secret",
"verifyTokenLabel": "Verify Token(可选)",
"verifyTokenPlaceholder": "事件订阅 Verification Token"
} }
}, },
"apply": { "apply": {
@@ -414,7 +437,15 @@
"title": "角色管理", "title": "角色管理",
"createRole": "创建角色", "createRole": "创建角色",
"searchPlaceholder": "搜索角色...", "searchPlaceholder": "搜索角色...",
"deleteConfirm": "确定要删除角色..." "deleteConfirm": "确定要删除角色...",
"loadFailed": "加载角色失败",
"noDescription": "暂无描述",
"defaultRoleDescription": "默认角色,不额外携带用户提示词,使用默认MCP",
"noMatchingRoles": "没有找到匹配的角色",
"noRoles": "暂无角色",
"enabled": "已启用",
"disabled": "已禁用",
"noDescriptionShort": "无描述"
}, },
"skills": { "skills": {
"title": "Skills管理", "title": "Skills管理",
@@ -436,7 +467,36 @@
"loadStatsFailed": "加载skills监控数据失败", "loadStatsFailed": "加载skills监控数据失败",
"clearStatsConfirm": "确定要清空所有Skills统计数据吗?此操作不可恢复。", "clearStatsConfirm": "确定要清空所有Skills统计数据吗?此操作不可恢复。",
"statsCleared": "已清空所有Skills统计数据", "statsCleared": "已清空所有Skills统计数据",
"clearStatsFailed": "清空统计数据失败" "clearStatsFailed": "清空统计数据失败",
"noDescription": "无描述",
"viewSkillTitle": "查看Skill: {{name}}",
"descriptionLabel": "描述:",
"pathLabel": "路径:",
"modTimeLabel": "修改时间:",
"contentLabel": "内容:",
"nameRequired": "skill名称不能为空",
"contentRequired": "skill内容不能为空",
"nameInvalid": "skill名称只能包含字母、数字、连字符和下划线",
"saveSuccess": "skill已更新",
"createdSuccess": "skill已创建",
"deleteConfirm": "确定要删除skill \"{{name}}\" 吗?此操作不可恢复。",
"deleteConfirmWithRoles": "确定要删除skill \"{{name}}\" 吗?\n\n⚠️ 该skill当前已被以下 {{count}} 个角色绑定:\n{{roles}}\n\n删除后,系统将自动从这些角色中移除该skill的绑定。\n\n此操作不可恢复,是否继续?",
"deleteSuccess": "skill已删除",
"deleteSuccessWithRoles": "skill已删除,已自动从 {{count}} 个角色中移除绑定:{{roles}}",
"loadFailedShort": "加载失败",
"totalSkillsCount": "总Skills数",
"totalCallsCount": "总调用次数",
"successfulCalls": "成功调用",
"failedCalls": "失败调用",
"successRate": "成功率",
"skillName": "Skill名称",
"totalCalls": "总调用",
"success": "成功",
"failure": "失败",
"lastCallTime": "最后调用时间",
"noCallRecords": "暂无Skills调用记录",
"loadStatsErrorShort": "无法加载统计信息",
"loadCallStatsError": "无法加载调用统计"
}, },
"apiDocs": { "apiDocs": {
"curlCopied": "curl命令已复制到剪贴板!" "curlCopied": "curl命令已复制到剪贴板!"
@@ -580,6 +640,15 @@
"presetLogin": "登录页 + 中国", "presetLogin": "登录页 + 中国",
"presetDomain": "指定域名", "presetDomain": "指定域名",
"presetIp": "指定 IP", "presetIp": "指定 IP",
"queryPresetsAria": "FOFA 查询示例",
"fieldsPresetsAria": "FOFA 字段模板",
"resultsToolbarAria": "结果工具条",
"fillExample": "填入示例",
"parseBtnTitle": "将自然语言解析为 FOFA 查询语法",
"minFieldsTitle": "适合快速导出目标",
"webCommonTitle": "适合浏览和筛选",
"intelEnhancedTitle": "更偏指纹/情报",
"fullLabel": "full",
"nlPlaceholder": "例如:找美国 Missouri 的 Apache 站点,标题包含 Home", "nlPlaceholder": "例如:找美国 Missouri 的 Apache 站点,标题包含 Home",
"showHideColumns": "显示/隐藏字段", "showHideColumns": "显示/隐藏字段",
"exportCsvTitle": "导出当前结果为 CSVUTF-8,兼容中文)", "exportCsvTitle": "导出当前结果为 CSVUTF-8,兼容中文)",
@@ -617,7 +686,14 @@
"clearStatsTitle": "清空所有统计数据", "clearStatsTitle": "清空所有统计数据",
"skillsCallStats": "Skills调用统计", "skillsCallStats": "Skills调用统计",
"searchPlaceholder": "搜索Skills...", "searchPlaceholder": "搜索Skills...",
"loading": "加载中..." "loading": "加载中...",
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 条",
"perPageLabel": "每页显示",
"firstPage": "首页",
"prevPage": "上一页",
"pageOf": "第 {{current}} / {{total}} 页",
"nextPage": "下一页",
"lastPage": "尾页"
}, },
"settingsBasic": { "settingsBasic": {
"basicTitle": "基本设置", "basicTitle": "基本设置",
@@ -703,7 +779,19 @@
}, },
"settingsRobotsExtra": { "settingsRobotsExtra": {
"botCommandsTitle": "机器人命令说明", "botCommandsTitle": "机器人命令说明",
"botCommandsDesc": "在对话中可发送以下命令(支持中英文):" "botCommandsDesc": "在对话中可发送以下命令(支持中英文):",
"botCmdHelp": "显示本帮助 | Show this help",
"botCmdList": "列出所有对话标题与 ID | List conversations",
"botCmdSwitch": "指定对话继续 | Switch to conversation",
"botCmdNew": "开启新对话 | Start new conversation",
"botCmdClear": "清空当前上下文 | Clear context",
"botCmdCurrent": "显示当前对话 ID 与标题 | Show current conversation",
"botCmdStop": "中断当前任务 | Stop running task",
"botCmdRoles": "列出所有可用角色 | List roles",
"botCmdRole": "切换当前角色 | Switch role",
"botCmdDelete": "删除指定对话 | Delete conversation",
"botCmdVersion": "显示当前版本号 | Show version",
"botCommandsFooter": "除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis."
}, },
"mcpDetailModal": { "mcpDetailModal": {
"title": "工具调用详情", "title": "工具调用详情",
@@ -752,6 +840,15 @@
"configJson": "配置JSON", "configJson": "配置JSON",
"formatLabel": "配置格式:", "formatLabel": "配置格式:",
"formatDesc": "JSON对象,key为配置名称,value为配置内容。状态通过\"启动/停止\"按钮控制,无需在JSON中配置。", "formatDesc": "JSON对象,key为配置名称,value为配置内容。状态通过\"启动/停止\"按钮控制,无需在JSON中配置。",
"configExample": "配置示例:",
"stdioMode": "stdio模式:",
"httpMode": "HTTP模式:",
"sseMode": "SSE模式:",
"placeholder": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\"],\n \"description\": \"描述\",\n \"timeout\": 300\n }\n}",
"exampleStdio": "{\n \"hexstrike-ai\": {\n \"command\": \"python3\",\n \"args\": [\"/path/to/script.py\", \"--server\", \"http://example.com\"],\n \"description\": \"描述\",\n \"timeout\": 300\n }\n}",
"exampleHttp": "{\n \"cyberstrike-ai-http\": {\n \"transport\": \"http\",\n \"url\": \"http://127.0.0.1:8081/mcp\"\n }\n}",
"exampleSse": "{\n \"cyberstrike-ai-sse\": {\n \"transport\": \"sse\",\n \"url\": \"http://127.0.0.1:8081/mcp/sse\"\n }\n}",
"exampleDescription": "示例描述",
"formatJson": "格式化JSON", "formatJson": "格式化JSON",
"loadExample": "加载示例" "loadExample": "加载示例"
}, },
@@ -765,7 +862,7 @@
"descriptionPlaceholder": "Skill的简短描述", "descriptionPlaceholder": "Skill的简短描述",
"contentLabel": "内容(Markdown格式)", "contentLabel": "内容(Markdown格式)",
"contentPlaceholder": "输入skill内容,支持Markdown格式...", "contentPlaceholder": "输入skill内容,支持Markdown格式...",
"contentHint": "支持YAML front matter格式(可选)" "contentHint": "支持YAML front matter格式(可选),例如:"
}, },
"knowledgeItemModal": { "knowledgeItemModal": {
"addKnowledge": "添加知识", "addKnowledge": "添加知识",
@@ -920,6 +1017,33 @@
"searchSkillsPlaceholder": "搜索skill...", "searchSkillsPlaceholder": "搜索skill...",
"loadingSkills": "正在加载skills列表...", "loadingSkills": "正在加载skills列表...",
"relatedSkillsHint": "勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。", "relatedSkillsHint": "勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。",
"enableRole": "启用此角色" "enableRole": "启用此角色",
"selectAll": "全选",
"deselectAll": "全不选",
"roleNameRequired": "角色名称不能为空",
"roleNotFound": "角色不存在",
"firstRoleNoToolsHint": "检测到这是首次添加角色且未选择工具,将默认使用全部工具",
"currentPageSelected": "当前页已选中: {{current}} / {{total}}",
"totalSelected": "总计已选中: {{current}} / {{total}}",
"usingAllEnabledTools": "(使用所有已启用工具)",
"currentPageSelectedTitle": "当前页选中的工具数(只统计已启用的工具)",
"totalSelectedTitle": "角色已关联的工具总数(基于角色实际配置)",
"skillsSelectedCount": "已选择 {{count}} / {{total}}",
"loadToolsFailed": "加载工具列表失败",
"loadSkillsFailed": "加载skills列表失败",
"cannotDeleteDefaultRole": "不能删除默认角色",
"noMatchingSkills": "没有找到匹配的skills",
"noSkillsAvailable": "暂无可用skills",
"usingAllTools": "使用所有工具",
"andNMore": " 等 {{count}} 个",
"toolsLabel": "工具:",
"noTools": "暂无工具",
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 个工具",
"paginationSearch": " (搜索: \"{{keyword}}\")",
"firstPage": "首页",
"prevPage": "上一页",
"pageOf": "第 {{page}} / {{total}} 页",
"nextPage": "下一页",
"lastPage": "末页"
} }
} }
+11 -6
View File
@@ -61,18 +61,23 @@
const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA'); const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
const attrList = el.getAttribute('data-i18n-attr'); const attrList = el.getAttribute('data-i18n-attr');
const text = i18next.t(key); const text = i18next.t(key);
// 仅当元素无子元素(仅文本或空)时才替换文本,避免覆盖卡片内的数字、子节点等;input/textarea 永不设置 textContent
// 仅当未使用 data-i18n-attr 时才替换元素文本内容(否则会覆盖卡片内的数字、子节点等) const hasNoElementChildren = !el.querySelector('*');
// input/textarea:永不设置 textContent(会变成 value),只更新属性 if (!skipText && !isFormControl && hasNoElementChildren && text && typeof text === 'string') {
if (!attrList && !skipText && !isFormControl && text && typeof text === 'string') {
el.textContent = text; el.textContent = text;
} }
if (attrList) { if (attrList) {
const titleKey = el.getAttribute('data-i18n-title');
attrList.split(',').map(function (s) { return s.trim(); }).forEach(function (attr) { attrList.split(',').map(function (s) { return s.trim(); }).forEach(function (attr) {
if (!attr) return; if (!attr) return;
if (text && typeof text === 'string') { var val = text;
el.setAttribute(attr, text); if (attr === 'title' && titleKey) {
var titleText = i18next.t(titleKey);
if (titleText && typeof titleText === 'string') val = titleText;
}
if (val && typeof val === 'string') {
el.setAttribute(attr, val);
} }
}); });
} }
+39 -36
View File
@@ -1,4 +1,7 @@
// 角色管理相关功能 // 角色管理相关功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
let currentRole = localStorage.getItem('currentRole') || ''; let currentRole = localStorage.getItem('currentRole') || '';
let roles = []; let roles = [];
let rolesSearchKeyword = ''; // 角色搜索关键词 let rolesSearchKeyword = ''; // 角色搜索关键词
@@ -54,7 +57,7 @@ async function loadRoles() {
return roles; return roles;
} catch (error) { } catch (error) {
console.error('加载角色失败:', error); console.error('加载角色失败:', error);
showNotification('加载角色失败: ' + error.message, 'error'); showNotification(_t('roles.loadFailed') + ': ' + error.message, 'error');
return []; return [];
} }
} }
@@ -167,9 +170,9 @@ function renderRoleSelectionSidebar() {
const icon = getRoleIcon(role); const icon = getRoleIcon(role);
// 处理默认角色的描述 // 处理默认角色的描述
let description = role.description || '暂无描述'; let description = role.description || _t('roles.noDescription');
if (isDefaultRole && !role.description) { if (isDefaultRole && !role.description) {
description = '默认角色,不额外携带用户提示词,使用默认MCP'; description = _t('roles.defaultRoleDescription');
} }
roleItem.innerHTML = ` roleItem.innerHTML = `
@@ -282,7 +285,7 @@ function renderRolesList() {
if (filteredRoles.length === 0) { if (filteredRoles.length === 0) {
rolesList.innerHTML = '<div class="empty-state">' + rolesList.innerHTML = '<div class="empty-state">' +
(rolesSearchKeyword ? '没有找到匹配的角色' : '暂无角色') + (rolesSearchKeyword ? _t('roles.noMatchingRoles') : _t('roles.noRoles')) +
'</div>'; '</div>';
return; return;
} }
@@ -312,7 +315,7 @@ function renderRolesList() {
let toolsDisplay = ''; let toolsDisplay = '';
let toolsCount = 0; let toolsCount = 0;
if (role.name === '默认') { if (role.name === '默认') {
toolsDisplay = '使用所有工具'; toolsDisplay = _t('roleModal.usingAllTools');
} else if (role.tools && role.tools.length > 0) { } else if (role.tools && role.tools.length > 0) {
toolsCount = role.tools.length; toolsCount = role.tools.length;
// 显示前5个工具名称 // 显示前5个工具名称
@@ -324,13 +327,13 @@ function renderRolesList() {
if (toolsCount <= 5) { if (toolsCount <= 5) {
toolsDisplay = toolNames.join(', '); toolsDisplay = toolNames.join(', ');
} else { } else {
toolsDisplay = toolNames.join(', ') + `${toolsCount}`; toolsDisplay = toolNames.join(', ') + _t('roleModal.andNMore', { count: toolsCount });
} }
} else if (role.mcps && role.mcps.length > 0) { } else if (role.mcps && role.mcps.length > 0) {
toolsCount = role.mcps.length; toolsCount = role.mcps.length;
toolsDisplay = `${toolsCount}`; toolsDisplay = _t('roleModal.andNMore', { count: toolsCount });
} else { } else {
toolsDisplay = '使用所有工具'; toolsDisplay = _t('roleModal.usingAllTools');
} }
return ` return `
@@ -341,17 +344,17 @@ function renderRolesList() {
${escapeHtml(role.name)} ${escapeHtml(role.name)}
</h3> </h3>
<span class="role-card-badge ${role.enabled !== false ? 'enabled' : 'disabled'}"> <span class="role-card-badge ${role.enabled !== false ? 'enabled' : 'disabled'}">
${role.enabled !== false ? '已启用' : '已禁用'} ${role.enabled !== false ? _t('roles.enabled') : _t('roles.disabled')}
</span> </span>
</div> </div>
<div class="role-card-description">${escapeHtml(role.description || '无描述')}</div> <div class="role-card-description">${escapeHtml(role.description || _t('roles.noDescriptionShort'))}</div>
<div class="role-card-tools"> <div class="role-card-tools">
<span class="role-card-tools-label">工具:</span> <span class="role-card-tools-label">${_t('roleModal.toolsLabel')}</span>
<span class="role-card-tools-value">${toolsDisplay}</span> <span class="role-card-tools-value">${toolsDisplay}</span>
</div> </div>
<div class="role-card-actions"> <div class="role-card-actions">
<button class="btn-secondary btn-small" onclick="editRole('${escapeHtml(role.name)}')">编辑</button> <button class="btn-secondary btn-small" onclick="editRole('${escapeHtml(role.name)}')">${_t('common.edit')}</button>
${role.name !== '默认' ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteRole('${escapeHtml(role.name)}')">删除</button>` : ''} ${role.name !== '默认' ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteRole('${escapeHtml(role.name)}')">${_t('common.delete')}</button>` : ''}
</div> </div>
</div> </div>
`; `;
@@ -503,7 +506,7 @@ async function loadRoleTools(page = 1, searchKeyword = '') {
console.error('加载工具列表失败:', error); console.error('加载工具列表失败:', error);
const toolsList = document.getElementById('role-tools-list'); const toolsList = document.getElementById('role-tools-list');
if (toolsList) { if (toolsList) {
toolsList.innerHTML = `<div class="tools-error">加载工具列表失败: ${escapeHtml(error.message)}</div>`; toolsList.innerHTML = `<div class="tools-error">${_t('roleModal.loadToolsFailed')}: ${escapeHtml(error.message)}</div>`;
} }
} }
} }
@@ -521,7 +524,7 @@ function renderRoleToolsList() {
listContainer.innerHTML = ''; listContainer.innerHTML = '';
if (allRoleTools.length === 0) { if (allRoleTools.length === 0) {
listContainer.innerHTML = '<div class="tools-empty">暂无工具</div>'; listContainer.innerHTML = '<div class="tools-empty">' + _t('roleModal.noTools') + '</div>';
toolsList.appendChild(listContainer); toolsList.appendChild(listContainer);
return; return;
} }
@@ -594,16 +597,16 @@ function renderRoleToolsPagination() {
const startItem = (page - 1) * roleToolsPagination.pageSize + 1; const startItem = (page - 1) * roleToolsPagination.pageSize + 1;
const endItem = Math.min(page * roleToolsPagination.pageSize, total); const endItem = Math.min(page * roleToolsPagination.pageSize, total);
const paginationShowText = _t('roleModal.paginationShow', { start: startItem, end: endItem, total: total }) +
(roleToolsSearchKeyword ? _t('roleModal.paginationSearch', { keyword: roleToolsSearchKeyword }) : '');
pagination.innerHTML = ` pagination.innerHTML = `
<div class="pagination-info"> <div class="pagination-info">${paginationShowText}</div>
显示 ${startItem}-${endItem} / 共 ${total} 个工具${roleToolsSearchKeyword ? ` (搜索: "${escapeHtml(roleToolsSearchKeyword)}")` : ''}
</div>
<div class="pagination-controls"> <div class="pagination-controls">
<button class="btn-secondary" onclick="loadRoleTools(1, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>首页</button> <button class="btn-secondary" onclick="loadRoleTools(1, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${_t('roleModal.firstPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${page - 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>上一页</button> <button class="btn-secondary" onclick="loadRoleTools(${page - 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${_t('roleModal.prevPage')}</button>
<span class="pagination-page">${page} / ${totalPages}</span> <span class="pagination-page">${_t('roleModal.pageOf', { page: page, total: totalPages })}</span>
<button class="btn-secondary" onclick="loadRoleTools(${page + 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>下一页</button> <button class="btn-secondary" onclick="loadRoleTools(${page + 1}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${_t('roleModal.nextPage')}</button>
<button class="btn-secondary" onclick="loadRoleTools(${totalPages}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>末页</button> <button class="btn-secondary" onclick="loadRoleTools(${totalPages}, '${escapeHtml(roleToolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${_t('roleModal.lastPage')}</button>
</div> </div>
`; `;
@@ -727,8 +730,8 @@ function updateRoleToolsStats() {
// 总工具数(所有工具,包括已启用和未启用的) // 总工具数(所有工具,包括已启用和未启用的)
const totalTools = roleToolsPagination.total || 0; const totalTools = roleToolsPagination.total || 0;
statsEl.innerHTML = ` statsEl.innerHTML = `
<span title="当前页选中的工具数">✅ 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span> <span title="${_t('roleModal.currentPageSelectedTitle')}">✅ ${_t('roleModal.currentPageSelected', { current: currentPageEnabled, total: currentPageTotal })}</span>
<span title="所有已启用工具中选中的工具总数(基于MCP管理)">📊 总计已选中: <strong>${totalEnabled}</strong> / ${totalTools} <em>(使用所有已启用工具)</em></span> <span title="${_t('roleModal.totalSelectedTitle')}">📊 ${_t('roleModal.totalSelected', { current: totalEnabled, total: totalTools })} <em>${_t('roleModal.usingAllEnabledTools')}</em></span>
`; `;
return; return;
} }
@@ -779,8 +782,8 @@ function updateRoleToolsStats() {
const totalTools = roleToolsPagination.total || 0; const totalTools = roleToolsPagination.total || 0;
statsEl.innerHTML = ` statsEl.innerHTML = `
<span title="当前页选中的工具数(只统计已启用的工具)">✅ 当前页已选中: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span> <span title="${_t('roleModal.currentPageSelectedTitle')}">✅ ${_t('roleModal.currentPageSelected', { current: currentPageEnabled, total: currentPageTotal })}</span>
<span title="角色已关联的工具总数(基于角色实际配置)">📊 总计已选中: <strong>${totalSelected}</strong> / ${totalTools}</span> <span title="${_t('roleModal.totalSelectedTitle')}">📊 ${_t('roleModal.totalSelected', { current: totalSelected, total: totalTools })}</span>
`; `;
} }
@@ -838,7 +841,7 @@ async function showAddRoleModal() {
const modal = document.getElementById('role-modal'); const modal = document.getElementById('role-modal');
if (!modal) return; if (!modal) return;
document.getElementById('role-modal-title').textContent = '添加角色'; document.getElementById('role-modal-title').textContent = _t('roleModal.addRole');
document.getElementById('role-name').value = ''; document.getElementById('role-name').value = '';
document.getElementById('role-name').disabled = false; document.getElementById('role-name').disabled = false;
document.getElementById('role-description').value = ''; document.getElementById('role-description').value = '';
@@ -918,14 +921,14 @@ async function showAddRoleModal() {
async function editRole(roleName) { async function editRole(roleName) {
const role = roles.find(r => r.name === roleName); const role = roles.find(r => r.name === roleName);
if (!role) { if (!role) {
showNotification('角色不存在', 'error'); showNotification(_t('roleModal.roleNotFound'), 'error');
return; return;
} }
const modal = document.getElementById('role-modal'); const modal = document.getElementById('role-modal');
if (!modal) return; if (!modal) return;
document.getElementById('role-modal-title').textContent = '编辑角色'; document.getElementById('role-modal-title').textContent = _t('roleModal.editRole');
document.getElementById('role-name').value = role.name; document.getElementById('role-name').value = role.name;
document.getElementById('role-name').disabled = true; // 编辑时不允许修改名称 document.getElementById('role-name').disabled = true; // 编辑时不允许修改名称
document.getElementById('role-description').value = role.description || ''; document.getElementById('role-description').value = role.description || '';
@@ -1186,7 +1189,7 @@ async function loadAllToolsToStateMap() {
async function saveRole() { async function saveRole() {
const name = document.getElementById('role-name').value.trim(); const name = document.getElementById('role-name').value.trim();
if (!name) { if (!name) {
showNotification('角色名称不能为空', 'error'); showNotification(_t('roleModal.roleNameRequired'), 'error');
return; return;
} }
@@ -1227,7 +1230,7 @@ async function saveRole() {
// 如果是首次添加角色且没有选择工具,默认使用全部工具 // 如果是首次添加角色且没有选择工具,默认使用全部工具
if (isFirstUserRole && allSelectedTools.length === 0) { if (isFirstUserRole && allSelectedTools.length === 0) {
roleUsesAllTools = true; roleUsesAllTools = true;
showNotification('检测到这是首次添加角色且未选择工具,将默认使用全部工具', 'info'); showNotification(_t('roleModal.firstRoleNoToolsHint'), 'info');
} else if (roleUsesAllTools) { } else if (roleUsesAllTools) {
// 如果当前使用所有工具,需要检查用户是否取消了一些工具 // 如果当前使用所有工具,需要检查用户是否取消了一些工具
// 检查状态映射中是否有未选中的已启用工具 // 检查状态映射中是否有未选中的已启用工具
@@ -1358,7 +1361,7 @@ async function saveRole() {
// 删除角色 // 删除角色
async function deleteRole(roleName) { async function deleteRole(roleName) {
if (roleName === '默认') { if (roleName === '默认') {
showNotification('不能删除默认角色', 'error'); showNotification(_t('roleModal.cannotDeleteDefaultRole'), 'error');
return; return;
} }
@@ -1469,7 +1472,7 @@ async function loadRoleSkills() {
allRoleSkills = []; allRoleSkills = [];
const skillsList = document.getElementById('role-skills-list'); const skillsList = document.getElementById('role-skills-list');
if (skillsList) { if (skillsList) {
skillsList.innerHTML = '<div class="skills-error">加载skills列表失败: ' + error.message + '</div>'; skillsList.innerHTML = '<div class="skills-error">' + _t('roleModal.loadSkillsFailed') + ': ' + error.message + '</div>';
} }
} }
} }
@@ -1490,7 +1493,7 @@ function renderRoleSkills() {
if (filteredSkills.length === 0) { if (filteredSkills.length === 0) {
skillsList.innerHTML = '<div class="skills-empty">' + skillsList.innerHTML = '<div class="skills-empty">' +
(roleSkillsSearchKeyword ? '没有找到匹配的skills' : '暂无可用skills') + (roleSkillsSearchKeyword ? _t('roleModal.noMatchingSkills') : _t('roleModal.noSkillsAvailable')) +
'</div>'; '</div>';
updateRoleSkillsStats(); updateRoleSkillsStats();
return; return;
@@ -1596,7 +1599,7 @@ function updateRoleSkillsStats() {
filteredSkills.includes(skill) filteredSkills.includes(skill)
).length; ).length;
statsEl.textContent = `已选择 ${selectedCount} / ${filteredSkills.length}`; statsEl.textContent = _t('roleModal.skillsSelectedCount', { count: selectedCount, total: filteredSkills.length });
} }
// HTML转义函数 // HTML转义函数
+17 -15
View File
@@ -1367,7 +1367,7 @@ function formatExternalMCPJSON() {
try { try {
const jsonStr = jsonTextarea.value.trim(); const jsonStr = jsonTextarea.value.trim();
if (!jsonStr) { if (!jsonStr) {
errorDiv.textContent = 'JSON不能为空'; errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.jsonEmpty') : 'JSON不能为空');
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
@@ -1379,7 +1379,7 @@ function formatExternalMCPJSON() {
errorDiv.style.display = 'none'; errorDiv.style.display = 'none';
jsonTextarea.classList.remove('error'); jsonTextarea.classList.remove('error');
} catch (error) { } catch (error) {
errorDiv.textContent = 'JSON格式错误: ' + error.message; errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.jsonError') : 'JSON格式错误') + ': ' + error.message;
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
} }
@@ -1387,6 +1387,7 @@ function formatExternalMCPJSON() {
// 加载示例 // 加载示例
function loadExternalMCPExample() { function loadExternalMCPExample() {
const desc = (typeof window.t === 'function' ? window.t('externalMcpModal.exampleDescription') : '示例描述');
const example = { const example = {
"hexstrike-ai": { "hexstrike-ai": {
command: "python3", command: "python3",
@@ -1395,7 +1396,7 @@ function loadExternalMCPExample() {
"--server", "--server",
"http://example.com" "http://example.com"
], ],
description: "示例描述", description: desc,
timeout: 300 timeout: 300
}, },
"cyberstrike-ai-http": { "cyberstrike-ai-http": {
@@ -1420,7 +1421,7 @@ async function saveExternalMCP() {
const errorDiv = document.getElementById('external-mcp-json-error'); const errorDiv = document.getElementById('external-mcp-json-error');
if (!jsonStr) { if (!jsonStr) {
errorDiv.textContent = 'JSON配置不能为空'; errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.jsonEmpty') : 'JSON不能为空');
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
jsonTextarea.focus(); jsonTextarea.focus();
@@ -1431,16 +1432,17 @@ async function saveExternalMCP() {
try { try {
configObj = JSON.parse(jsonStr); configObj = JSON.parse(jsonStr);
} catch (error) { } catch (error) {
errorDiv.textContent = 'JSON格式错误: ' + error.message; errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.jsonError') : 'JSON格式错误') + ': ' + error.message;
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
jsonTextarea.focus(); jsonTextarea.focus();
return; return;
} }
const t = (typeof window.t === 'function' ? window.t : function (k, opts) { return k; });
// 验证必须是对象格式 // 验证必须是对象格式
if (typeof configObj !== 'object' || Array.isArray(configObj) || configObj === null) { if (typeof configObj !== 'object' || Array.isArray(configObj) || configObj === null) {
errorDiv.textContent = '配置错误: 必须是JSON对象格式,key为配置名称,value为配置内容'; errorDiv.textContent = t('mcp.configMustBeObject');
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
@@ -1449,7 +1451,7 @@ async function saveExternalMCP() {
// 获取所有配置名称 // 获取所有配置名称
const names = Object.keys(configObj); const names = Object.keys(configObj);
if (names.length === 0) { if (names.length === 0) {
errorDiv.textContent = '配置错误: 至少需要一个配置项'; errorDiv.textContent = t('mcp.configNeedOne');
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
@@ -1458,7 +1460,7 @@ async function saveExternalMCP() {
// 验证每个配置 // 验证每个配置
for (const name of names) { for (const name of names) {
if (!name || name.trim() === '') { if (!name || name.trim() === '') {
errorDiv.textContent = '配置错误: 配置名称不能为空'; errorDiv.textContent = t('mcp.configNameEmpty');
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
@@ -1466,7 +1468,7 @@ async function saveExternalMCP() {
const config = configObj[name]; const config = configObj[name];
if (typeof config !== 'object' || Array.isArray(config) || config === null) { if (typeof config !== 'object' || Array.isArray(config) || config === null) {
errorDiv.textContent = `配置错误: "${name}" 的配置必须是对象`; errorDiv.textContent = t('mcp.configMustBeObj', { name: name });
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
@@ -1478,28 +1480,28 @@ async function saveExternalMCP() {
// 验证配置内容 // 验证配置内容
const transport = config.transport || (config.command ? 'stdio' : config.url ? 'http' : ''); const transport = config.transport || (config.command ? 'stdio' : config.url ? 'http' : '');
if (!transport) { if (!transport) {
errorDiv.textContent = `配置错误: "${name}" 需要指定commandstdio模式)或urlhttp/sse模式)`; errorDiv.textContent = t('mcp.configNeedCommand', { name: name });
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
} }
if (transport === 'stdio' && !config.command) { if (transport === 'stdio' && !config.command) {
errorDiv.textContent = `配置错误: "${name}" stdio模式需要command字段`; errorDiv.textContent = t('mcp.configStdioNeedCommand', { name: name });
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
} }
if (transport === 'http' && !config.url) { if (transport === 'http' && !config.url) {
errorDiv.textContent = `配置错误: "${name}" http模式需要url字段`; errorDiv.textContent = t('mcp.configHttpNeedUrl', { name: name });
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
} }
if (transport === 'sse' && !config.url) { if (transport === 'sse' && !config.url) {
errorDiv.textContent = `配置错误: "${name}" sse模式需要url字段`; errorDiv.textContent = t('mcp.configSseNeedUrl', { name: name });
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
@@ -1514,7 +1516,7 @@ async function saveExternalMCP() {
// 如果是编辑模式,只更新当前编辑的配置 // 如果是编辑模式,只更新当前编辑的配置
if (currentEditingMCPName) { if (currentEditingMCPName) {
if (!configObj[currentEditingMCPName]) { if (!configObj[currentEditingMCPName]) {
errorDiv.textContent = `配置错误: 编辑模式下,JSON必须包含配置名称 "${currentEditingMCPName}"`; errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.configEditMustContainName', { name: currentEditingMCPName }) : '配置错误: 编辑模式下,JSON必须包含配置名称 "' + currentEditingMCPName + '"');
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
return; return;
@@ -1561,7 +1563,7 @@ async function saveExternalMCP() {
alert(typeof window.t === 'function' ? window.t('mcp.saveSuccess') : '保存成功'); alert(typeof window.t === 'function' ? window.t('mcp.saveSuccess') : '保存成功');
} catch (error) { } catch (error) {
console.error('保存外部MCP失败:', error); console.error('保存外部MCP失败:', error);
errorDiv.textContent = '保存失败: ' + error.message; errorDiv.textContent = (typeof window.t === 'function' ? window.t('mcp.operationFailed') : '保存失败') + ': ' + error.message;
errorDiv.style.display = 'block'; errorDiv.style.display = 'block';
jsonTextarea.classList.add('error'); jsonTextarea.classList.add('error');
} }
+94 -66
View File
@@ -1,4 +1,7 @@
// Skills管理相关功能 // Skills管理相关功能
function _t(key, opts) {
return typeof window.t === 'function' ? window.t(key, opts) : key;
}
let skillsList = []; let skillsList = [];
let currentEditingSkillName = null; let currentEditingSkillName = null;
let isSavingSkill = false; // 防止重复提交 let isSavingSkill = false; // 防止重复提交
@@ -65,7 +68,7 @@ async function loadSkills(page = 1, pageSize = null) {
const response = await apiFetch(url); const response = await apiFetch(url);
if (!response.ok) { if (!response.ok) {
throw new Error('获取skills列表失败'); throw new Error(_t('skills.loadListFailed'));
} }
const data = await response.json(); const data = await response.json();
skillsList = data.skills || []; skillsList = data.skills || [];
@@ -76,10 +79,10 @@ async function loadSkills(page = 1, pageSize = null) {
updateSkillsManagementStats(); updateSkillsManagementStats();
} catch (error) { } catch (error) {
console.error('加载skills列表失败:', error); console.error('加载skills列表失败:', error);
showNotification('加载skills列表失败: ' + error.message, 'error'); showNotification(_t('skills.loadListFailed') + ': ' + error.message, 'error');
const skillsListEl = document.getElementById('skills-list'); const skillsListEl = document.getElementById('skills-list');
if (skillsListEl) { if (skillsListEl) {
skillsListEl.innerHTML = '<div class="empty-state">加载失败: ' + error.message + '</div>'; skillsListEl.innerHTML = '<div class="empty-state">' + _t('skills.loadFailedShort') + ': ' + escapeHtml(error.message) + '</div>';
} }
} }
} }
@@ -94,7 +97,7 @@ function renderSkillsList() {
if (filteredSkills.length === 0) { if (filteredSkills.length === 0) {
skillsListEl.innerHTML = '<div class="empty-state">' + skillsListEl.innerHTML = '<div class="empty-state">' +
(skillsSearchKeyword ? '没有找到匹配的skills' : '暂无skills,点击"创建Skill"创建第一个skill') + (skillsSearchKeyword ? _t('skills.noMatch') : _t('skills.noSkills')) +
'</div>'; '</div>';
// 搜索时隐藏分页 // 搜索时隐藏分页
const paginationContainer = document.getElementById('skills-pagination'); const paginationContainer = document.getElementById('skills-pagination');
@@ -109,12 +112,12 @@ function renderSkillsList() {
<div class="skill-card"> <div class="skill-card">
<div class="skill-card-header"> <div class="skill-card-header">
<h3 class="skill-card-title">${escapeHtml(skill.name || '')}</h3> <h3 class="skill-card-title">${escapeHtml(skill.name || '')}</h3>
<div class="skill-card-description">${escapeHtml(skill.description || '无描述')}</div> <div class="skill-card-description">${escapeHtml(skill.description || _t('skills.noDescription'))}</div>
</div> </div>
<div class="skill-card-actions"> <div class="skill-card-actions">
<button class="btn-secondary btn-small" onclick="viewSkill('${escapeHtml(skill.name)}')">查看</button> <button class="btn-secondary btn-small" onclick="viewSkill('${escapeHtml(skill.name)}')">${_t('common.view')}</button>
<button class="btn-secondary btn-small" onclick="editSkill('${escapeHtml(skill.name)}')">编辑</button> <button class="btn-secondary btn-small" onclick="editSkill('${escapeHtml(skill.name)}')">${_t('common.edit')}</button>
<button class="btn-secondary btn-small btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')">删除</button> <button class="btn-secondary btn-small btn-danger" onclick="deleteSkill('${escapeHtml(skill.name)}')">${_t('common.delete')}</button>
</div> </div>
</div> </div>
`; `;
@@ -154,12 +157,19 @@ function renderSkillsPagination() {
let paginationHTML = '<div class="pagination">'; let paginationHTML = '<div class="pagination">';
const paginationShowText = _t('skillsPage.paginationShow', { start, end, total });
const perPageLabelText = _t('skillsPage.perPageLabel');
const firstPageText = _t('skillsPage.firstPage');
const prevPageText = _t('skillsPage.prevPage');
const pageOfText = _t('skillsPage.pageOf', { current: currentPage, total: totalPages || 1 });
const nextPageText = _t('skillsPage.nextPage');
const lastPageText = _t('skillsPage.lastPage');
// 左侧:显示范围信息和每页数量选择器(参考MCP样式) // 左侧:显示范围信息和每页数量选择器(参考MCP样式)
paginationHTML += ` paginationHTML += `
<div class="pagination-info"> <div class="pagination-info">
<span>显示 ${start}-${end} / 共 ${total}</span> <span>${escapeHtml(paginationShowText)}</span>
<label class="pagination-page-size"> <label class="pagination-page-size">
每页显示 ${escapeHtml(perPageLabelText)}
<select id="skills-page-size-pagination" onchange="changeSkillsPageSize()"> <select id="skills-page-size-pagination" onchange="changeSkillsPageSize()">
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option> <option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option> <option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
@@ -173,11 +183,11 @@ function renderSkillsPagination() {
// 右侧:分页按钮(参考MCP样式:首页、上一页、第X/Y页、下一页、末页) // 右侧:分页按钮(参考MCP样式:首页、上一页、第X/Y页、下一页、末页)
paginationHTML += ` paginationHTML += `
<div class="pagination-controls"> <div class="pagination-controls">
<button class="btn-secondary" onclick="loadSkills(1, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button> <button class="btn-secondary" onclick="loadSkills(1, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(firstPageText)}</button>
<button class="btn-secondary" onclick="loadSkills(${currentPage - 1}, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button> <button class="btn-secondary" onclick="loadSkills(${currentPage - 1}, ${pageSize})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(prevPageText)}</button>
<span class="pagination-page">${currentPage} / ${totalPages || 1}</span> <span class="pagination-page">${escapeHtml(pageOfText)}</span>
<button class="btn-secondary" onclick="loadSkills(${currentPage + 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button> <button class="btn-secondary" onclick="loadSkills(${currentPage + 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(nextPageText)}</button>
<button class="btn-secondary" onclick="loadSkills(${totalPages || 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button> <button class="btn-secondary" onclick="loadSkills(${totalPages || 1}, ${pageSize})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(lastPageText)}</button>
</div> </div>
`; `;
@@ -291,7 +301,7 @@ async function searchSkills() {
try { try {
const response = await apiFetch(`/api/skills?search=${encodeURIComponent(skillsSearchKeyword)}&limit=10000&offset=0`); const response = await apiFetch(`/api/skills?search=${encodeURIComponent(skillsSearchKeyword)}&limit=10000&offset=0`);
if (!response.ok) { if (!response.ok) {
throw new Error('获取skills列表失败'); throw new Error(_t('skills.loadListFailed'));
} }
const data = await response.json(); const data = await response.json();
skillsList = data.skills || []; skillsList = data.skills || [];
@@ -306,7 +316,7 @@ async function searchSkills() {
updateSkillsManagementStats(); updateSkillsManagementStats();
} catch (error) { } catch (error) {
console.error('搜索skills失败:', error); console.error('搜索skills失败:', error);
showNotification('搜索失败: ' + error.message, 'error'); showNotification(_t('skills.searchFailed') + ': ' + error.message, 'error');
} }
} else { } else {
// 没有搜索关键词时,恢复分页加载 // 没有搜索关键词时,恢复分页加载
@@ -332,7 +342,7 @@ function clearSkillsSearch() {
// 刷新skills // 刷新skills
async function refreshSkills() { async function refreshSkills() {
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize); await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
showNotification('已刷新', 'success'); showNotification(_t('skills.refreshed'), 'success');
} }
// 显示添加skill模态框 // 显示添加skill模态框
@@ -340,7 +350,7 @@ function showAddSkillModal() {
const modal = document.getElementById('skill-modal'); const modal = document.getElementById('skill-modal');
if (!modal) return; if (!modal) return;
document.getElementById('skill-modal-title').textContent = '添加Skill'; document.getElementById('skill-modal-title').textContent = _t('skills.addSkill');
document.getElementById('skill-name').value = ''; document.getElementById('skill-name').value = '';
document.getElementById('skill-name').disabled = false; document.getElementById('skill-name').disabled = false;
document.getElementById('skill-description').value = ''; document.getElementById('skill-description').value = '';
@@ -354,7 +364,7 @@ async function editSkill(skillName) {
try { try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`); const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) { if (!response.ok) {
throw new Error('获取skill详情失败'); throw new Error(_t('skills.loadDetailFailed'));
} }
const data = await response.json(); const data = await response.json();
const skill = data.skill; const skill = data.skill;
@@ -362,7 +372,7 @@ async function editSkill(skillName) {
const modal = document.getElementById('skill-modal'); const modal = document.getElementById('skill-modal');
if (!modal) return; if (!modal) return;
document.getElementById('skill-modal-title').textContent = '编辑Skill'; document.getElementById('skill-modal-title').textContent = _t('skills.editSkill');
document.getElementById('skill-name').value = skill.name; document.getElementById('skill-name').value = skill.name;
document.getElementById('skill-name').disabled = true; // 编辑时不允许修改名称 document.getElementById('skill-name').disabled = true; // 编辑时不允许修改名称
document.getElementById('skill-description').value = skill.description || ''; document.getElementById('skill-description').value = skill.description || '';
@@ -372,7 +382,7 @@ async function editSkill(skillName) {
modal.style.display = 'flex'; modal.style.display = 'flex';
} catch (error) { } catch (error) {
console.error('加载skill详情失败:', error); console.error('加载skill详情失败:', error);
showNotification('加载skill详情失败: ' + error.message, 'error'); showNotification(_t('skills.loadDetailFailed') + ': ' + error.message, 'error');
} }
} }
@@ -381,7 +391,7 @@ async function viewSkill(skillName) {
try { try {
const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`); const response = await apiFetch(`/api/skills/${encodeURIComponent(skillName)}`);
if (!response.ok) { if (!response.ok) {
throw new Error('获取skill详情失败'); throw new Error(_t('skills.loadDetailFailed'));
} }
const data = await response.json(); const data = await response.json();
const skill = data.skill; const skill = data.skill;
@@ -390,22 +400,29 @@ async function viewSkill(skillName) {
const modal = document.createElement('div'); const modal = document.createElement('div');
modal.className = 'modal'; modal.className = 'modal';
modal.id = 'skill-view-modal'; modal.id = 'skill-view-modal';
const viewTitle = _t('skills.viewSkillTitle', { name: skill.name });
const descLabel = _t('skills.descriptionLabel');
const pathLabel = _t('skills.pathLabel');
const modTimeLabel = _t('skills.modTimeLabel');
const contentLabel = _t('skills.contentLabel');
const closeBtn = _t('common.close');
const editBtn = _t('common.edit');
modal.innerHTML = ` modal.innerHTML = `
<div class="modal-content" style="max-width: 900px; max-height: 90vh;"> <div class="modal-content" style="max-width: 900px; max-height: 90vh;">
<div class="modal-header"> <div class="modal-header">
<h2>查看Skill: ${escapeHtml(skill.name)}</h2> <h2>${escapeHtml(viewTitle)}</h2>
<span class="modal-close" onclick="closeSkillViewModal()">&times;</span> <span class="modal-close" onclick="closeSkillViewModal()">&times;</span>
</div> </div>
<div class="modal-body" style="overflow-y: auto; max-height: calc(90vh - 120px);"> <div class="modal-body" style="overflow-y: auto; max-height: calc(90vh - 120px);">
${skill.description ? `<div style="margin-bottom: 16px;"><strong>描述:</strong> ${escapeHtml(skill.description)}</div>` : ''} ${skill.description ? `<div style="margin-bottom: 16px;"><strong>${escapeHtml(descLabel)}</strong> ${escapeHtml(skill.description)}</div>` : ''}
<div style="margin-bottom: 8px;"><strong>路径:</strong> ${escapeHtml(skill.path || '')}</div> <div style="margin-bottom: 8px;"><strong>${escapeHtml(pathLabel)}</strong> ${escapeHtml(skill.path || '')}</div>
<div style="margin-bottom: 16px;"><strong>修改时间:</strong> ${escapeHtml(skill.mod_time || '')}</div> <div style="margin-bottom: 16px;"><strong>${escapeHtml(modTimeLabel)}</strong> ${escapeHtml(skill.mod_time || '')}</div>
<div style="margin-bottom: 8px;"><strong>内容:</strong></div> <div style="margin-bottom: 8px;"><strong>${escapeHtml(contentLabel)}</strong></div>
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(skill.content || '')}</pre> <pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; overflow-x: auto; white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(skill.content || '')}</pre>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillViewModal()">关闭</button> <button class="btn-secondary" onclick="closeSkillViewModal()">${escapeHtml(closeBtn)}</button>
<button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">编辑</button> <button class="btn-primary" onclick="editSkill('${escapeHtml(skill.name)}'); closeSkillViewModal();">${escapeHtml(editBtn)}</button>
</div> </div>
</div> </div>
`; `;
@@ -413,7 +430,7 @@ async function viewSkill(skillName) {
modal.style.display = 'flex'; modal.style.display = 'flex';
} catch (error) { } catch (error) {
console.error('查看skill失败:', error); console.error('查看skill失败:', error);
showNotification('查看skill失败: ' + error.message, 'error'); showNotification(_t('skills.viewFailed') + ': ' + error.message, 'error');
} }
} }
@@ -443,18 +460,18 @@ async function saveSkill() {
const content = document.getElementById('skill-content').value.trim(); const content = document.getElementById('skill-content').value.trim();
if (!name) { if (!name) {
showNotification('skill名称不能为空', 'error'); showNotification(_t('skills.nameRequired'), 'error');
return; return;
} }
if (!content) { if (!content) {
showNotification('skill内容不能为空', 'error'); showNotification(_t('skills.contentRequired'), 'error');
return; return;
} }
// 验证skill名称 // 验证skill名称
if (!/^[a-zA-Z0-9_-]+$/.test(name)) { if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
showNotification('skill名称只能包含字母、数字、连字符和下划线', 'error'); showNotification(_t('skills.nameInvalid'), 'error');
return; return;
} }
@@ -462,7 +479,7 @@ async function saveSkill() {
const saveBtn = document.querySelector('#skill-modal .btn-primary'); const saveBtn = document.querySelector('#skill-modal .btn-primary');
if (saveBtn) { if (saveBtn) {
saveBtn.disabled = true; saveBtn.disabled = true;
saveBtn.textContent = '保存中...'; saveBtn.textContent = _t('skills.saving');
} }
try { try {
@@ -484,20 +501,20 @@ async function saveSkill() {
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
throw new Error(error.error || '保存skill失败'); throw new Error(error.error || _t('skills.saveFailed'));
} }
showNotification(isEdit ? 'skill已更新' : 'skill已创建', 'success'); showNotification(isEdit ? _t('skills.saveSuccess') : _t('skills.createdSuccess'), 'success');
closeSkillModal(); closeSkillModal();
await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize); await loadSkills(skillsPagination.currentPage, skillsPagination.pageSize);
} catch (error) { } catch (error) {
console.error('保存skill失败:', error); console.error('保存skill失败:', error);
showNotification('保存skill失败: ' + error.message, 'error'); showNotification(_t('skills.saveFailed') + ': ' + error.message, 'error');
} finally { } finally {
isSavingSkill = false; isSavingSkill = false;
if (saveBtn) { if (saveBtn) {
saveBtn.disabled = false; saveBtn.disabled = false;
saveBtn.textContent = '保存'; saveBtn.textContent = _t('common.save');
} }
} }
} }
@@ -518,10 +535,10 @@ async function deleteSkill(skillName) {
} }
// 构建确认消息 // 构建确认消息
let confirmMessage = `确定要删除skill "${skillName}" 吗?此操作不可恢复。`; let confirmMessage = _t('skills.deleteConfirm', { name: skillName });
if (boundRoles.length > 0) { if (boundRoles.length > 0) {
const rolesList = boundRoles.join('、'); const rolesList = boundRoles.join('、');
confirmMessage = `确定要删除skill "${skillName}" 吗?\n\n⚠️ 该skill当前已被以下 ${boundRoles.length} 个角色绑定:\n${rolesList}\n\n删除后,系统将自动从这些角色中移除该skill的绑定。\n\n此操作不可恢复,是否继续?`; confirmMessage = _t('skills.deleteConfirmWithRoles', { name: skillName, count: boundRoles.length, roles: rolesList });
} }
if (!confirm(confirmMessage)) { if (!confirm(confirmMessage)) {
@@ -535,14 +552,14 @@ async function deleteSkill(skillName) {
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
throw new Error(error.error || '删除skill失败'); throw new Error(error.error || _t('skills.deleteFailed'));
} }
const data = await response.json(); const data = await response.json();
let successMessage = 'skill已删除'; let successMessage = _t('skills.deleteSuccess');
if (data.affected_roles && data.affected_roles.length > 0) { if (data.affected_roles && data.affected_roles.length > 0) {
const rolesList = data.affected_roles.join('、'); const rolesList = data.affected_roles.join('、');
successMessage = `skill已删除,已自动从 ${data.affected_roles.length} 个角色中移除绑定:${rolesList}`; successMessage = _t('skills.deleteSuccessWithRoles', { count: data.affected_roles.length, roles: rolesList });
} }
showNotification(successMessage, 'success'); showNotification(successMessage, 'success');
@@ -554,7 +571,7 @@ async function deleteSkill(skillName) {
await loadSkills(pageToLoad, skillsPagination.pageSize); await loadSkills(pageToLoad, skillsPagination.pageSize);
} catch (error) { } catch (error) {
console.error('删除skill失败:', error); console.error('删除skill失败:', error);
showNotification('删除skill失败: ' + error.message, 'error'); showNotification(_t('skills.deleteFailed') + ': ' + error.message, 'error');
} }
} }
@@ -565,7 +582,7 @@ async function loadSkillsMonitor() {
try { try {
const response = await apiFetch('/api/skills/stats'); const response = await apiFetch('/api/skills/stats');
if (!response.ok) { if (!response.ok) {
throw new Error('获取skills统计信息失败'); throw new Error(_t('skills.loadStatsFailed'));
} }
const data = await response.json(); const data = await response.json();
@@ -581,14 +598,14 @@ async function loadSkillsMonitor() {
renderSkillsMonitor(); renderSkillsMonitor();
} catch (error) { } catch (error) {
console.error('加载skills监控数据失败:', error); console.error('加载skills监控数据失败:', error);
showNotification('加载skills监控数据失败: ' + error.message, 'error'); showNotification(_t('skills.loadStatsFailed') + ': ' + error.message, 'error');
const statsEl = document.getElementById('skills-stats'); const statsEl = document.getElementById('skills-stats');
if (statsEl) { if (statsEl) {
statsEl.innerHTML = '<div class="monitor-error">无法加载统计信息:' + escapeHtml(error.message) + '</div>'; statsEl.innerHTML = '<div class="monitor-error">' + _t('skills.loadStatsErrorShort') + ': ' + escapeHtml(error.message) + '</div>';
} }
const monitorListEl = document.getElementById('skills-monitor-list'); const monitorListEl = document.getElementById('skills-monitor-list');
if (monitorListEl) { if (monitorListEl) {
monitorListEl.innerHTML = '<div class="monitor-error">无法加载调用统计:' + escapeHtml(error.message) + '</div>'; monitorListEl.innerHTML = '<div class="monitor-error">' + _t('skills.loadCallStatsError') + ': ' + escapeHtml(error.message) + '</div>';
} }
} }
} }
@@ -604,23 +621,23 @@ function renderSkillsMonitor() {
statsEl.innerHTML = ` statsEl.innerHTML = `
<div class="monitor-stat-card"> <div class="monitor-stat-card">
<div class="monitor-stat-label">总Skills数</div> <div class="monitor-stat-label">${_t('skills.totalSkillsCount')}</div>
<div class="monitor-stat-value">${skillsStats.total}</div> <div class="monitor-stat-value">${skillsStats.total}</div>
</div> </div>
<div class="monitor-stat-card"> <div class="monitor-stat-card">
<div class="monitor-stat-label">总调用次数</div> <div class="monitor-stat-label">${_t('skills.totalCallsCount')}</div>
<div class="monitor-stat-value">${skillsStats.totalCalls}</div> <div class="monitor-stat-value">${skillsStats.totalCalls}</div>
</div> </div>
<div class="monitor-stat-card"> <div class="monitor-stat-card">
<div class="monitor-stat-label">成功调用</div> <div class="monitor-stat-label">${_t('skills.successfulCalls')}</div>
<div class="monitor-stat-value" style="color: #28a745;">${skillsStats.totalSuccess}</div> <div class="monitor-stat-value" style="color: #28a745;">${skillsStats.totalSuccess}</div>
</div> </div>
<div class="monitor-stat-card"> <div class="monitor-stat-card">
<div class="monitor-stat-label">失败调用</div> <div class="monitor-stat-label">${_t('skills.failedCalls')}</div>
<div class="monitor-stat-value" style="color: #dc3545;">${skillsStats.totalFailed}</div> <div class="monitor-stat-value" style="color: #dc3545;">${skillsStats.totalFailed}</div>
</div> </div>
<div class="monitor-stat-card"> <div class="monitor-stat-card">
<div class="monitor-stat-label">成功率</div> <div class="monitor-stat-label">${_t('skills.successRate')}</div>
<div class="monitor-stat-value">${successRate}%</div> <div class="monitor-stat-value">${successRate}%</div>
</div> </div>
`; `;
@@ -634,7 +651,7 @@ function renderSkillsMonitor() {
// 如果没有统计数据,显示空状态 // 如果没有统计数据,显示空状态
if (stats.length === 0) { if (stats.length === 0) {
monitorListEl.innerHTML = '<div class="monitor-empty">暂无Skills调用记录</div>'; monitorListEl.innerHTML = '<div class="monitor-empty">' + _t('skills.noCallRecords') + '</div>';
return; return;
} }
@@ -652,12 +669,12 @@ function renderSkillsMonitor() {
<table class="monitor-table"> <table class="monitor-table">
<thead> <thead>
<tr> <tr>
<th style="text-align: left !important;">Skill名称</th> <th style="text-align: left !important;">${_t('skills.skillName')}</th>
<th style="text-align: center;">总调用</th> <th style="text-align: center;">${_t('skills.totalCalls')}</th>
<th style="text-align: center;">成功</th> <th style="text-align: center;">${_t('skills.success')}</th>
<th style="text-align: center;">失败</th> <th style="text-align: center;">${_t('skills.failure')}</th>
<th style="text-align: center;">成功率</th> <th style="text-align: center;">${_t('skills.successRate')}</th>
<th style="text-align: left;">最后调用时间</th> <th style="text-align: left;">${_t('skills.lastCallTime')}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -687,12 +704,12 @@ function renderSkillsMonitor() {
// 刷新skills监控 // 刷新skills监控
async function refreshSkillsMonitor() { async function refreshSkillsMonitor() {
await loadSkillsMonitor(); await loadSkillsMonitor();
showNotification('已刷新', 'success'); showNotification(_t('skills.refreshed'), 'success');
} }
// 清空skills统计数据 // 清空skills统计数据
async function clearSkillsStats() { async function clearSkillsStats() {
if (!confirm('确定要清空所有Skills统计数据吗?此操作不可恢复。')) { if (!confirm(_t('skills.clearStatsConfirm'))) {
return; return;
} }
@@ -703,15 +720,15 @@ async function clearSkillsStats() {
if (!response.ok) { if (!response.ok) {
const error = await response.json(); const error = await response.json();
throw new Error(error.error || '清空统计数据失败'); throw new Error(error.error || _t('skills.clearStatsFailed'));
} }
showNotification('已清空所有Skills统计数据', 'success'); showNotification(_t('skills.statsCleared'), 'success');
// 重新加载统计数据 // 重新加载统计数据
await loadSkillsMonitor(); await loadSkillsMonitor();
} catch (error) { } catch (error) {
console.error('清空统计数据失败:', error); console.error('清空统计数据失败:', error);
showNotification('清空统计数据失败: ' + error.message, 'error'); showNotification(_t('skills.clearStatsFailed') + ': ' + error.message, 'error');
} }
} }
@@ -722,3 +739,14 @@ function escapeHtml(text) {
div.textContent = text; div.textContent = text;
return div.innerHTML; return div.innerHTML;
} }
// 语言切换时重新渲染当前页(技能列表与分页使用 _t,需随语言更新)
document.addEventListener('languagechange', function () {
const page = document.getElementById('page-skills-management');
if (page && page.classList.contains('active')) {
renderSkillsList();
if (!skillsSearchKeyword) {
renderSkillsPagination();
}
}
});
+105 -105
View File
@@ -774,18 +774,18 @@
<label for="fofa-query" data-i18n="infoCollectPage.fofaQuerySyntax">FOFA 查询语法</label> <label for="fofa-query" data-i18n="infoCollectPage.fofaQuerySyntax">FOFA 查询语法</label>
<textarea id="fofa-query" class="info-collect-query-input" rows="1" data-i18n="infoCollect.queryPlaceholder" data-i18n-attr="placeholder" placeholder='例如:app="Apache" && country="CN"'></textarea> <textarea id="fofa-query" class="info-collect-query-input" rows="1" data-i18n="infoCollect.queryPlaceholder" data-i18n-attr="placeholder" placeholder='例如:app="Apache" && country="CN"'></textarea>
<small class="form-hint" data-i18n="infoCollectPage.formHint">查询语法参考 FOFA 文档,支持 && / || / () 等。</small> <small class="form-hint" data-i18n="infoCollectPage.formHint">查询语法参考 FOFA 文档,支持 && / || / () 等。</small>
<div class="info-collect-presets" aria-label="FOFA 查询示例"> <div class="info-collect-presets" aria-label="FOFA 查询示例" data-i18n="infoCollectPage.queryPresetsAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('app=&quot;Apache&quot; &amp;&amp; country=&quot;CN&quot;')" data-i18n="infoCollectPage.presetApache" data-i18n-attr="title" title="填入示例">Apache + 中国</button> <button class="preset-chip" type="button" onclick="applyFofaQueryPreset('app=&quot;Apache&quot; &amp;&amp; country=&quot;CN&quot;')" data-i18n="infoCollectPage.presetApache" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">Apache + 中国</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('title=&quot;登录&quot; &amp;&amp; country=&quot;CN&quot;')" data-i18n="infoCollectPage.presetLogin" data-i18n-attr="title" title="填入示例">登录页 + 中国</button> <button class="preset-chip" type="button" onclick="applyFofaQueryPreset('title=&quot;登录&quot; &amp;&amp; country=&quot;CN&quot;')" data-i18n="infoCollectPage.presetLogin" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">登录页 + 中国</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('domain=&quot;example.com&quot;')" data-i18n="infoCollectPage.presetDomain" data-i18n-attr="title" title="填入示例">指定域名</button> <button class="preset-chip" type="button" onclick="applyFofaQueryPreset('domain=&quot;example.com&quot;')" data-i18n="infoCollectPage.presetDomain" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">指定域名</button>
<button class="preset-chip" type="button" onclick="applyFofaQueryPreset('ip=&quot;1.1.1.1&quot;')" data-i18n="infoCollectPage.presetIp" data-i18n-attr="title" title="填入示例">指定 IP</button> <button class="preset-chip" type="button" onclick="applyFofaQueryPreset('ip=&quot;1.1.1.1&quot;')" data-i18n="infoCollectPage.presetIp" data-i18n-attr="title" data-i18n-title="infoCollectPage.fillExample" title="填入示例">指定 IP</button>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="fofa-nl" data-i18n="infoCollectPage.naturalLanguage">自然语言(AI 解析为 FOFA 语法)</label> <label for="fofa-nl" data-i18n="infoCollectPage.naturalLanguage">自然语言(AI 解析为 FOFA 语法)</label>
<div class="info-collect-nl-row"> <div class="info-collect-nl-row">
<textarea id="fofa-nl" class="info-collect-query-input" rows="1" data-i18n="infoCollectPage.nlPlaceholder" data-i18n-attr="placeholder" placeholder="例如:找美国 Missouri 的 Apache 站点,标题包含 Home"></textarea> <textarea id="fofa-nl" class="info-collect-query-input" rows="1" data-i18n="infoCollectPage.nlPlaceholder" data-i18n-attr="placeholder" placeholder="例如:找美国 Missouri 的 Apache 站点,标题包含 Home"></textarea>
<button id="fofa-nl-parse-btn" class="btn-secondary" type="button" onclick="parseFofaNaturalLanguage()" data-i18n="infoCollectPage.parseBtn" data-i18n-attr="title" title="将自然语言解析为 FOFA 查询语法">AI 解析</button> <button id="fofa-nl-parse-btn" class="btn-secondary" type="button" onclick="parseFofaNaturalLanguage()" data-i18n="infoCollectPage.parseBtn" data-i18n-attr="title" data-i18n-title="infoCollectPage.parseBtnTitle" title="将自然语言解析为 FOFA 查询语法">AI 解析</button>
</div> </div>
<div id="fofa-nl-status" class="fofa-nl-status muted" style="display: none;" aria-live="polite"></div> <div id="fofa-nl-status" class="fofa-nl-status muted" style="display: none;" aria-live="polite"></div>
<small class="form-hint" data-i18n="infoCollectPage.parseHint">解析后会弹窗展示 FOFA 语法(可编辑),确认无误后再填入查询框并执行查询。</small> <small class="form-hint" data-i18n="infoCollectPage.parseHint">解析后会弹窗展示 FOFA 语法(可编辑),确认无误后再填入查询框并执行查询。</small>
@@ -803,17 +803,17 @@
<label class="checkbox-label" style="margin-top: 24px;"> <label class="checkbox-label" style="margin-top: 24px;">
<input type="checkbox" id="fofa-full" class="modern-checkbox" /> <input type="checkbox" id="fofa-full" class="modern-checkbox" />
<span class="checkbox-custom"></span> <span class="checkbox-custom"></span>
<span class="checkbox-text">full</span> <span class="checkbox-text" data-i18n="infoCollectPage.fullLabel">full</span>
</label> </label>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="fofa-fields" data-i18n="infoCollectPage.returnFields">返回字段名(逗号分隔)</label> <label for="fofa-fields" data-i18n="infoCollectPage.returnFields">返回字段名(逗号分隔)</label>
<input type="text" id="fofa-fields" value="host,ip,port,domain,title,protocol,country,province,city,server" /> <input type="text" id="fofa-fields" value="host,ip,port,domain,title,protocol,country,province,city,server" />
<div class="info-collect-presets" aria-label="FOFA 字段模板"> <div class="info-collect-presets" aria-label="FOFA 字段模板" data-i18n="infoCollectPage.fieldsPresetsAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain')" data-i18n="infoCollectPage.minFields" data-i18n-attr="title" title="适合快速导出目标">最小字段</button> <button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain')" data-i18n="infoCollectPage.minFields" data-i18n-attr="title" data-i18n-title="infoCollectPage.minFieldsTitle" title="适合快速导出目标">最小字段</button>
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,title,ip,port,domain,protocol,server,icp,country,province,city')" data-i18n="infoCollectPage.webCommon" data-i18n-attr="title" title="适合浏览和筛选">Web 常用</button> <button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,title,ip,port,domain,protocol,server,icp,country,province,city')" data-i18n="infoCollectPage.webCommon" data-i18n-attr="title" data-i18n-title="infoCollectPage.webCommonTitle" title="适合浏览和筛选">Web 常用</button>
<button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain,title,protocol,country,province,city,server,as_number,as_organization,icp,header,banner')" data-i18n="infoCollectPage.intelEnhanced" data-i18n-attr="title" title="更偏指纹/情报">情报增强</button> <button class="preset-chip" type="button" onclick="applyFofaFieldsPreset('host,ip,port,domain,title,protocol,country,province,city,server,as_number,as_organization,icp,header,banner')" data-i18n="infoCollectPage.intelEnhanced" data-i18n-attr="title" data-i18n-title="infoCollectPage.intelEnhancedTitle" title="更偏指纹/情报">情报增强</button>
</div> </div>
</div> </div>
</div> </div>
@@ -825,7 +825,7 @@
<div class="info-collect-results-title" data-i18n="infoCollectPage.queryResults">查询结果</div> <div class="info-collect-results-title" data-i18n="infoCollectPage.queryResults">查询结果</div>
<div class="info-collect-results-meta" id="fofa-results-meta">-</div> <div class="info-collect-results-meta" id="fofa-results-meta">-</div>
</div> </div>
<div class="info-collect-results-toolbar" aria-label="结果工具条"> <div class="info-collect-results-toolbar" aria-label="结果工具条" data-i18n="infoCollectPage.resultsToolbarAria" data-i18n-attr="aria-label" data-i18n-skip-text="true">
<div class="info-collect-selected" id="fofa-selected-meta" data-i18n="infoCollectPage.selectedRowsZero">已选择 0 条</div> <div class="info-collect-selected" id="fofa-selected-meta" data-i18n="infoCollectPage.selectedRowsZero">已选择 0 条</div>
<button class="btn-secondary btn-small" type="button" onclick="toggleFofaColumnsPanel()" data-i18n="infoCollectPage.showHideColumns" data-i18n-attr="title" title="显示/隐藏字段"><span data-i18n="infoCollectPage.columns"></span></button> <button class="btn-secondary btn-small" type="button" onclick="toggleFofaColumnsPanel()" data-i18n="infoCollectPage.showHideColumns" data-i18n-attr="title" title="显示/隐藏字段"><span data-i18n="infoCollectPage.columns"></span></button>
<button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('csv')" data-i18n="infoCollectPage.exportCsvTitle" data-i18n-attr="title" title="导出当前结果为 CSVUTF-8,兼容中文)"><span data-i18n="infoCollectPage.exportCsv">导出 CSV</span></button> <button class="btn-secondary btn-small" type="button" onclick="exportFofaResults('csv')" data-i18n="infoCollectPage.exportCsvTitle" data-i18n-attr="title" title="导出当前结果为 CSVUTF-8,兼容中文)"><span data-i18n="infoCollectPage.exportCsv">导出 CSV</span></button>
@@ -1281,24 +1281,24 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-wecom-token">Token</label> <label for="robot-wecom-token" data-i18n="settings.robots.wecom.token">Token</label>
<input type="text" id="robot-wecom-token" placeholder="Token" autocomplete="off" /> <input type="text" id="robot-wecom-token" data-i18n="settings.robots.wecom.tokenPlaceholder" data-i18n-attr="placeholder" placeholder="Token" autocomplete="off" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-wecom-encoding-aes-key">EncodingAESKey</label> <label for="robot-wecom-encoding-aes-key" data-i18n="settings.robots.wecom.encodingAesKey">EncodingAESKey</label>
<input type="text" id="robot-wecom-encoding-aes-key" placeholder="EncodingAESKey(明文模式可留空)" autocomplete="off" /> <input type="text" id="robot-wecom-encoding-aes-key" data-i18n="settings.robots.wecom.encodingAesKeyPlaceholder" data-i18n-attr="placeholder" placeholder="EncodingAESKey(明文模式可留空)" autocomplete="off" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-wecom-corp-id">CorpID</label> <label for="robot-wecom-corp-id" data-i18n="settings.robots.wecom.corpId">CorpID</label>
<input type="text" id="robot-wecom-corp-id" placeholder="企业 ID" autocomplete="off" /> <input type="text" id="robot-wecom-corp-id" data-i18n="settings.robots.wecom.corpIdPlaceholder" data-i18n-attr="placeholder" placeholder="企业 ID" autocomplete="off" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-wecom-secret">Secret</label> <label for="robot-wecom-secret" data-i18n="settings.robots.wecom.secret">Secret</label>
<input type="password" id="robot-wecom-secret" placeholder="应用 Secret" autocomplete="off" /> <input type="password" id="robot-wecom-secret" data-i18n="settings.robots.wecom.secretPlaceholder" data-i18n-attr="placeholder" placeholder="应用 Secret" autocomplete="off" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-wecom-agent-id">AgentID</label> <label for="robot-wecom-agent-id" data-i18n="settings.robots.wecom.agentId">AgentID</label>
<input type="number" id="robot-wecom-agent-id" placeholder="应用 AgentId" /> <input type="number" id="robot-wecom-agent-id" data-i18n="settings.robots.wecom.agentIdPlaceholder" data-i18n-attr="placeholder" placeholder="应用 AgentId" />
</div> </div>
</div> </div>
</div> </div>
@@ -1315,13 +1315,13 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-dingtalk-client-id">Client ID (AppKey)</label> <label for="robot-dingtalk-client-id" data-i18n="settings.robots.dingtalk.clientIdLabel">Client ID (AppKey)</label>
<input type="text" id="robot-dingtalk-client-id" placeholder="钉钉应用 AppKey" autocomplete="off" /> <input type="text" id="robot-dingtalk-client-id" data-i18n="settings.robots.dingtalk.clientIdPlaceholder" data-i18n-attr="placeholder" placeholder="钉钉应用 AppKey" autocomplete="off" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-dingtalk-client-secret">Client Secret</label> <label for="robot-dingtalk-client-secret" data-i18n="settings.robots.dingtalk.clientSecretLabel">Client Secret</label>
<input type="password" id="robot-dingtalk-client-secret" placeholder="钉钉应用 Secret" autocomplete="off" /> <input type="password" id="robot-dingtalk-client-secret" data-i18n="settings.robots.dingtalk.clientSecretPlaceholder" data-i18n-attr="placeholder" placeholder="钉钉应用 Secret" autocomplete="off" />
<small class="form-hint">需开启机器人能力并配置流式接入</small> <small class="form-hint" data-i18n="settings.robots.dingtalk.streamHint">需开启机器人能力并配置流式接入</small>
</div> </div>
</div> </div>
</div> </div>
@@ -1338,41 +1338,41 @@
</label> </label>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-lark-app-id">App ID</label> <label for="robot-lark-app-id" data-i18n="settings.robots.lark.appIdLabel">App ID</label>
<input type="text" id="robot-lark-app-id" placeholder="飞书应用 App ID" autocomplete="off" /> <input type="text" id="robot-lark-app-id" data-i18n="settings.robots.lark.appIdPlaceholder" data-i18n-attr="placeholder" placeholder="飞书应用 App ID" autocomplete="off" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-lark-app-secret">App Secret</label> <label for="robot-lark-app-secret" data-i18n="settings.robots.lark.appSecretLabel">App Secret</label>
<input type="password" id="robot-lark-app-secret" placeholder="飞书应用 App Secret" autocomplete="off" /> <input type="password" id="robot-lark-app-secret" data-i18n="settings.robots.lark.appSecretPlaceholder" data-i18n-attr="placeholder" placeholder="飞书应用 App Secret" autocomplete="off" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="robot-lark-verify-token">Verify Token(可选)</label> <label for="robot-lark-verify-token" data-i18n="settings.robots.lark.verifyTokenLabel">Verify Token(可选)</label>
<input type="text" id="robot-lark-verify-token" placeholder="事件订阅 Verification Token" autocomplete="off" /> <input type="text" id="robot-lark-verify-token" data-i18n="settings.robots.lark.verifyTokenPlaceholder" data-i18n-attr="placeholder" placeholder="事件订阅 Verification Token" autocomplete="off" />
</div> </div>
</div> </div>
</div> </div>
<div class="settings-subsection"> <div class="settings-subsection">
<h4>机器人命令说明</h4> <h4 data-i18n="settingsRobotsExtra.botCommandsTitle">机器人命令说明</h4>
<p class="settings-description">在对话中可发送以下命令(支持中英文):</p> <p class="settings-description" data-i18n="settingsRobotsExtra.botCommandsDesc">在对话中可发送以下命令(支持中英文):</p>
<ul style="color: var(--text-muted); font-size: 13px; line-height: 1.8; margin: 8px 0 0 16px;"> <ul style="color: var(--text-muted); font-size: 13px; line-height: 1.8; margin: 8px 0 0 16px;">
<li><code>帮助</code> <code>help</code> — 显示本帮助 | Show this help</li> <li><code>帮助</code> <code>help</code><span data-i18n="settingsRobotsExtra.botCmdHelp">显示本帮助 | Show this help</span></li>
<li><code>列表</code> <code>list</code> — 列出所有对话标题与 ID | List conversations</li> <li><code>列表</code> <code>list</code><span data-i18n="settingsRobotsExtra.botCmdList">列出所有对话标题与 ID | List conversations</span></li>
<li><code>切换 &lt;ID&gt;</code> <code>switch &lt;ID&gt;</code> — 指定对话继续 | Switch to conversation</li> <li><code>切换 &lt;ID&gt;</code> <code>switch &lt;ID&gt;</code><span data-i18n="settingsRobotsExtra.botCmdSwitch">指定对话继续 | Switch to conversation</span></li>
<li><code>新对话</code> <code>new</code> — 开启新对话 | Start new conversation</li> <li><code>新对话</code> <code>new</code><span data-i18n="settingsRobotsExtra.botCmdNew">开启新对话 | Start new conversation</span></li>
<li><code>清空</code> <code>clear</code> — 清空当前上下文 | Clear context</li> <li><code>清空</code> <code>clear</code><span data-i18n="settingsRobotsExtra.botCmdClear">清空当前上下文 | Clear context</span></li>
<li><code>当前</code> <code>current</code> — 显示当前对话 ID 与标题 | Show current conversation</li> <li><code>当前</code> <code>current</code><span data-i18n="settingsRobotsExtra.botCmdCurrent">显示当前对话 ID 与标题 | Show current conversation</span></li>
<li><code>停止</code> <code>stop</code> — 中断当前任务 | Stop running task</li> <li><code>停止</code> <code>stop</code><span data-i18n="settingsRobotsExtra.botCmdStop">中断当前任务 | Stop running task</span></li>
<li><code>角色</code> <code>roles</code> — 列出所有可用角色 | List roles</li> <li><code>角色</code> <code>roles</code><span data-i18n="settingsRobotsExtra.botCmdRoles">列出所有可用角色 | List roles</span></li>
<li><code>角色 &lt;&gt;</code> <code>role &lt;name&gt;</code> — 切换当前角色 | Switch role</li> <li><code>角色 &lt;&gt;</code> <code>role &lt;name&gt;</code><span data-i18n="settingsRobotsExtra.botCmdRole">切换当前角色 | Switch role</span></li>
<li><code>删除 &lt;ID&gt;</code> <code>delete &lt;ID&gt;</code> — 删除指定对话 | Delete conversation</li> <li><code>删除 &lt;ID&gt;</code> <code>delete &lt;ID&gt;</code><span data-i18n="settingsRobotsExtra.botCmdDelete">删除指定对话 | Delete conversation</span></li>
<li><code>版本</code> <code>version</code> — 显示当前版本号 | Show version</li> <li><code>版本</code> <code>version</code><span data-i18n="settingsRobotsExtra.botCmdVersion">显示当前版本号 | Show version</span></li>
</ul> </ul>
<p class="settings-description" style="margin-top: 8px;">除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis.</p> <p class="settings-description" style="margin-top: 8px;" data-i18n="settingsRobotsExtra.botCommandsFooter">除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。Otherwise, send any text for AI penetration testing / security analysis.</p>
</div> </div>
<div class="settings-actions"> <div class="settings-actions">
<button class="btn-primary" onclick="applySettings()">应用配置</button> <button class="btn-primary" onclick="applySettings()" data-i18n="settings.apply.button">应用配置</button>
</div> </div>
</div> </div>
@@ -1507,18 +1507,18 @@
<div id="external-mcp-modal" class="modal"> <div id="external-mcp-modal" class="modal">
<div class="modal-content" style="max-width: 900px;"> <div class="modal-content" style="max-width: 900px;">
<div class="modal-header"> <div class="modal-header">
<h2 id="external-mcp-modal-title">添加外部MCP</h2> <h2 id="external-mcp-modal-title" data-i18n="mcp.addExternalMCP">添加外部MCP</h2>
<span class="modal-close" onclick="closeExternalMCPModal()">&times;</span> <span class="modal-close" onclick="closeExternalMCPModal()">&times;</span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label for="external-mcp-json">配置JSON <span style="color: red;">*</span></label> <label for="external-mcp-json"><span data-i18n="externalMcpModal.configJson">配置JSON</span> <span style="color: red;">*</span></label>
<textarea id="external-mcp-json" rows="15" placeholder='{\n "hexstrike-ai": {\n "command": "python3",\n "args": ["/path/to/script.py"],\n "description": "描述",\n "timeout": 300\n }\n}' style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea> <textarea id="external-mcp-json" rows="15" data-i18n="externalMcpModal.placeholder" data-i18n-attr="placeholder" style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;"></textarea>
<div class="password-hint"> <div class="password-hint">
<strong>配置格式:</strong>JSON对象,key为配置名称,value为配置内容。状态通过"启动/停止"按钮控制,无需在JSON中配置。<br> <strong data-i18n="externalMcpModal.formatLabel">配置格式:</strong><span data-i18n="externalMcpModal.formatDesc">JSON对象,key为配置名称,value为配置内容。状态通过"启动/停止"按钮控制,无需在JSON中配置。</span><br>
<strong>配置示例:</strong><br> <strong data-i18n="externalMcpModal.configExample">配置示例:</strong><br>
<strong>stdio模式:</strong><br> <strong data-i18n="externalMcpModal.stdioMode">stdio模式:</strong><br>
<code style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{ <code data-i18n="externalMcpModal.exampleStdio" style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
"hexstrike-ai": { "hexstrike-ai": {
"command": "python3", "command": "python3",
"args": ["/path/to/script.py", "--server", "http://example.com"], "args": ["/path/to/script.py", "--server", "http://example.com"],
@@ -1526,15 +1526,15 @@
"timeout": 300 "timeout": 300
} }
}</code> }</code>
<strong>HTTP模式:</strong><br> <strong data-i18n="externalMcpModal.httpMode">HTTP模式:</strong><br>
<code style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{ <code data-i18n="externalMcpModal.exampleHttp" style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
"cyberstrike-ai-http": { "cyberstrike-ai-http": {
"transport": "http", "transport": "http",
"url": "http://127.0.0.1:8081/mcp" "url": "http://127.0.0.1:8081/mcp"
} }
}</code> }</code>
<strong>SSE模式:</strong><br> <strong data-i18n="externalMcpModal.sseMode">SSE模式:</strong><br>
<code style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{ <code data-i18n="externalMcpModal.exampleSse" style="display: block; margin: 8px 0; padding: 8px; background: var(--bg-secondary); border-radius: 4px; white-space: pre-wrap;">{
"cyberstrike-ai-sse": { "cyberstrike-ai-sse": {
"transport": "sse", "transport": "sse",
"url": "http://127.0.0.1:8081/mcp/sse" "url": "http://127.0.0.1:8081/mcp/sse"
@@ -1544,13 +1544,13 @@
<div id="external-mcp-json-error" class="error-message" style="display: none; margin-top: 8px; padding: 8px; background: rgba(220, 53, 69, 0.1); border: 1px solid rgba(220, 53, 69, 0.3); border-radius: 4px; color: var(--error-color); font-size: 0.875rem;"></div> <div id="external-mcp-json-error" class="error-message" style="display: none; margin-top: 8px; padding: 8px; background: rgba(220, 53, 69, 0.1); border: 1px solid rgba(220, 53, 69, 0.3); border-radius: 4px; color: var(--error-color); font-size: 0.875rem;"></div>
</div> </div>
<div class="form-group"> <div class="form-group">
<button type="button" class="btn-secondary" onclick="formatExternalMCPJSON()" style="margin-top: 8px;">格式化JSON</button> <button type="button" class="btn-secondary" onclick="formatExternalMCPJSON()" style="margin-top: 8px;" data-i18n="externalMcpModal.formatJson">格式化JSON</button>
<button type="button" class="btn-secondary" onclick="loadExternalMCPExample()" style="margin-top: 8px; margin-left: 8px;">加载示例</button> <button type="button" class="btn-secondary" onclick="loadExternalMCPExample()" style="margin-top: 8px; margin-left: 8px;" data-i18n="externalMcpModal.loadExample">加载示例</button>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-secondary" onclick="closeExternalMCPModal()">取消</button> <button class="btn-secondary" onclick="closeExternalMCPModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveExternalMCP()">保存</button> <button class="btn-primary" onclick="saveExternalMCP()" data-i18n="common.save">保存</button>
</div> </div>
</div> </div>
</div> </div>
@@ -1687,23 +1687,23 @@
<div id="skill-modal" class="modal"> <div id="skill-modal" class="modal">
<div class="modal-content" style="max-width: 900px;"> <div class="modal-content" style="max-width: 900px;">
<div class="modal-header"> <div class="modal-header">
<h2 id="skill-modal-title">添加Skill</h2> <h2 id="skill-modal-title" data-i18n="skillModal.addSkill">添加Skill</h2>
<span class="modal-close" onclick="closeSkillModal()">&times;</span> <span class="modal-close" onclick="closeSkillModal()">&times;</span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label for="skill-name">Skill名称 <span style="color: red;">*</span></label> <label for="skill-name"><span data-i18n="skillModal.skillName">Skill名称</span> <span style="color: red;">*</span></label>
<input type="text" id="skill-name" placeholder="例如: sql-injection-testing" required /> <input type="text" id="skill-name" data-i18n="skillModal.skillNamePlaceholder" data-i18n-attr="placeholder" placeholder="例如: sql-injection-testing" required />
<small class="form-hint">只能包含字母、数字、连字符和下划线</small> <small class="form-hint" data-i18n="skillModal.skillNameHint">只能包含字母、数字、连字符和下划线</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="skill-description">描述</label> <label for="skill-description" data-i18n="skillModal.description">描述</label>
<input type="text" id="skill-description" placeholder="Skill的简短描述" /> <input type="text" id="skill-description" data-i18n="skillModal.descriptionPlaceholder" data-i18n-attr="placeholder" placeholder="Skill的简短描述" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="skill-content">内容(Markdown格式) <span style="color: red;">*</span></label> <label for="skill-content"><span data-i18n="skillModal.contentLabel">内容(Markdown格式)</span> <span style="color: red;">*</span></label>
<textarea id="skill-content" rows="20" placeholder="输入skill内容,支持Markdown格式..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;" required></textarea> <textarea id="skill-content" rows="20" data-i18n="skillModal.contentPlaceholder" data-i18n-attr="placeholder" placeholder="输入skill内容,支持Markdown格式..." style="font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; font-size: 0.875rem; line-height: 1.5;" required></textarea>
<small class="form-hint">支持YAML front matter格式(可选),例如:<br> <small class="form-hint"><span data-i18n="skillModal.contentHint">支持YAML front matter格式(可选),例如:</span><br>
---<br> ---<br>
name: skill-name<br> name: skill-name<br>
description: Skill描述<br> description: Skill描述<br>
@@ -1714,8 +1714,8 @@ version: 1.0.0<br>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-secondary" onclick="closeSkillModal()">取消</button> <button class="btn-secondary" onclick="closeSkillModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveSkill()">保存</button> <button class="btn-primary" onclick="saveSkill()" data-i18n="common.save">保存</button>
</div> </div>
</div> </div>
</div> </div>
@@ -2094,7 +2094,7 @@ version: 1.0.0<br>
<div id="role-select-modal" class="modal"> <div id="role-select-modal" class="modal">
<div class="modal-content role-select-modal-content"> <div class="modal-content role-select-modal-content">
<div class="modal-header"> <div class="modal-header">
<h2>选择角色</h2> <h2 data-i18n="chatGroup.rolePanelTitle">选择角色</h2>
<span class="modal-close" onclick="closeRoleSelectModal()">&times;</span> <span class="modal-close" onclick="closeRoleSelectModal()">&times;</span>
</div> </div>
<div class="modal-body role-select-body"> <div class="modal-body role-select-body">
@@ -2107,49 +2107,49 @@ version: 1.0.0<br>
<div id="role-modal" class="modal"> <div id="role-modal" class="modal">
<div class="modal-content" style="max-width: 900px;"> <div class="modal-content" style="max-width: 900px;">
<div class="modal-header"> <div class="modal-header">
<h2 id="role-modal-title">添加角色</h2> <h2 id="role-modal-title" data-i18n="roleModal.addRole">添加角色</h2>
<span class="modal-close" onclick="closeRoleModal()">&times;</span> <span class="modal-close" onclick="closeRoleModal()">&times;</span>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="form-group"> <div class="form-group">
<label for="role-name">角色名称 <span style="color: red;">*</span></label> <label for="role-name"><span data-i18n="roleModal.roleName">角色名称</span> <span style="color: red;">*</span></label>
<input type="text" id="role-name" placeholder="输入角色名称" required /> <input type="text" id="role-name" data-i18n="roleModal.roleNamePlaceholder" data-i18n-attr="placeholder" placeholder="输入角色名称" required />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="role-description">角色描述</label> <label for="role-description" data-i18n="roleModal.roleDescription">角色描述</label>
<input type="text" id="role-description" placeholder="输入角色描述" /> <input type="text" id="role-description" data-i18n="roleModal.roleDescriptionPlaceholder" data-i18n-attr="placeholder" placeholder="输入角色描述" />
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="role-icon">角色图标</label> <label for="role-icon" data-i18n="roleModal.roleIcon">角色图标</label>
<input type="text" id="role-icon" placeholder="输入emoji图标,例如: 🏆" maxlength="10" /> <input type="text" id="role-icon" data-i18n="roleModal.roleIconPlaceholder" data-i18n-attr="placeholder" placeholder="输入emoji图标,例如: 🏆" maxlength="10" />
<small class="form-hint">输入一个emoji作为角色的图标,将显示在角色选择器中。</small> <small class="form-hint" data-i18n="roleModal.roleIconHint">输入一个emoji作为角色的图标,将显示在角色选择器中。</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="role-user-prompt">用户提示词</label> <label for="role-user-prompt" data-i18n="roleModal.userPrompt">用户提示词</label>
<textarea id="role-user-prompt" rows="10" placeholder="输入用户提示词,会在用户消息前追加此提示词..."></textarea> <textarea id="role-user-prompt" rows="10" data-i18n="roleModal.userPromptPlaceholder" data-i18n-attr="placeholder" placeholder="输入用户提示词,会在用户消息前追加此提示词..."></textarea>
<small class="form-hint">此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。</small> <small class="form-hint" data-i18n="roleModal.userPromptHint">此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。</small>
</div> </div>
<div class="form-group" id="role-tools-section"> <div class="form-group" id="role-tools-section">
<label>关联的工具(可选)</label> <label data-i18n="roleModal.relatedTools">关联的工具(可选)</label>
<div id="role-tools-default-hint" class="role-tools-default-hint" style="display: none;"> <div id="role-tools-default-hint" class="role-tools-default-hint" style="display: none;">
<div class="role-tools-default-info"> <div class="role-tools-default-info">
<span class="role-tools-default-icon"></span> <span class="role-tools-default-icon"></span>
<div class="role-tools-default-content"> <div class="role-tools-default-content">
<div class="role-tools-default-title">默认角色使用所有工具</div> <div class="role-tools-default-title" data-i18n="roleModal.defaultRoleToolsTitle">默认角色使用所有工具</div>
<div class="role-tools-default-desc">默认角色会自动使用MCP管理中启用的所有工具,无需单独配置。</div> <div class="role-tools-default-desc" data-i18n="roleModal.defaultRoleToolsDesc">默认角色会自动使用MCP管理中启用的所有工具,无需单独配置。</div>
</div> </div>
</div> </div>
</div> </div>
<div class="role-tools-controls"> <div class="role-tools-controls">
<div class="role-tools-actions"> <div class="role-tools-actions">
<button type="button" class="btn-secondary" onclick="selectAllRoleTools()">全选</button> <button type="button" class="btn-secondary" onclick="selectAllRoleTools()" data-i18n="roleModal.selectAll">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleTools()">全不选</button> <button type="button" class="btn-secondary" onclick="deselectAllRoleTools()" data-i18n="roleModal.deselectAll">全不选</button>
<div class="role-tools-search-box"> <div class="role-tools-search-box">
<input type="text" id="role-tools-search" placeholder="搜索工具..." <input type="text" id="role-tools-search" data-i18n="roleModal.searchToolsPlaceholder" data-i18n-attr="placeholder" placeholder="搜索工具..."
oninput="searchRoleTools(this.value)" oninput="searchRoleTools(this.value)"
onkeypress="if(event.key === 'Enter') searchRoleTools(this.value)" /> onkeypress="if(event.key === 'Enter') searchRoleTools(this.value)" />
<button class="role-tools-search-clear" id="role-tools-search-clear" <button class="role-tools-search-clear" id="role-tools-search-clear"
onclick="clearRoleToolsSearch()" style="display: none;" title="清除搜索"> onclick="clearRoleToolsSearch()" style="display: none;" data-i18n="common.clearSearch" data-i18n-attr="title" title="清除搜索">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/> <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -2160,22 +2160,22 @@ version: 1.0.0<br>
<div id="role-tools-stats" class="role-tools-stats"></div> <div id="role-tools-stats" class="role-tools-stats"></div>
</div> </div>
<div id="role-tools-list" class="role-tools-list"> <div id="role-tools-list" class="role-tools-list">
<div class="tools-loading">正在加载工具列表...</div> <div class="tools-loading" data-i18n="roleModal.loadingTools">正在加载工具列表...</div>
</div> </div>
<small class="form-hint">勾选要关联的工具,留空则使用MCP管理中的全部工具配置。</small> <small class="form-hint" data-i18n="roleModal.relatedToolsHint">勾选要关联的工具,留空则使用MCP管理中的全部工具配置。</small>
</div> </div>
<div class="form-group" id="role-skills-section"> <div class="form-group" id="role-skills-section">
<label>关联的Skills(可选)</label> <label data-i18n="roleModal.relatedSkills">关联的Skills(可选)</label>
<div class="role-skills-controls"> <div class="role-skills-controls">
<div class="role-skills-actions"> <div class="role-skills-actions">
<button type="button" class="btn-secondary" onclick="selectAllRoleSkills()">全选</button> <button type="button" class="btn-secondary" onclick="selectAllRoleSkills()" data-i18n="roleModal.selectAll">全选</button>
<button type="button" class="btn-secondary" onclick="deselectAllRoleSkills()">全不选</button> <button type="button" class="btn-secondary" onclick="deselectAllRoleSkills()" data-i18n="roleModal.deselectAll">全不选</button>
<div class="role-skills-search-box"> <div class="role-skills-search-box">
<input type="text" id="role-skills-search" placeholder="搜索skill..." <input type="text" id="role-skills-search" data-i18n="roleModal.searchSkillsPlaceholder" data-i18n-attr="placeholder" placeholder="搜索skill..."
oninput="searchRoleSkills(this.value)" oninput="searchRoleSkills(this.value)"
onkeypress="if(event.key === 'Enter') searchRoleSkills(this.value)" /> onkeypress="if(event.key === 'Enter') searchRoleSkills(this.value)" />
<button class="role-skills-search-clear" id="role-skills-search-clear" <button class="role-skills-search-clear" id="role-skills-search-clear"
onclick="clearRoleSkillsSearch()" style="display: none;" title="清除搜索"> onclick="clearRoleSkillsSearch()" style="display: none;" data-i18n="common.clearSearch" data-i18n-attr="title" title="清除搜索">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/> <circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
<path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> <path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
@@ -2186,21 +2186,21 @@ version: 1.0.0<br>
<div id="role-skills-stats" class="role-skills-stats"></div> <div id="role-skills-stats" class="role-skills-stats"></div>
</div> </div>
<div id="role-skills-list" class="role-skills-list"> <div id="role-skills-list" class="role-skills-list">
<div class="skills-loading">正在加载skills列表...</div> <div class="skills-loading" data-i18n="roleModal.loadingSkills">正在加载skills列表...</div>
</div> </div>
<small class="form-hint">勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。</small> <small class="form-hint" data-i18n="roleModal.relatedSkillsHint">勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。</small>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-label"> <label class="checkbox-label">
<input type="checkbox" id="role-enabled" class="modern-checkbox" checked /> <input type="checkbox" id="role-enabled" class="modern-checkbox" checked />
<span class="checkbox-custom"></span> <span class="checkbox-custom"></span>
<span class="checkbox-text">启用此角色</span> <span class="checkbox-text" data-i18n="roleModal.enableRole">启用此角色</span>
</label> </label>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button class="btn-secondary" onclick="closeRoleModal()">取消</button> <button class="btn-secondary" onclick="closeRoleModal()" data-i18n="common.cancel">取消</button>
<button class="btn-primary" onclick="saveRole()">保存</button> <button class="btn-primary" onclick="saveRole()" data-i18n="common.save">保存</button>
</div> </div>
</div> </div>
</div> </div>