mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-17 21:44:43 +02:00
Add files via upload
This commit is contained in:
@@ -529,6 +529,60 @@ header {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.lang-switcher {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.lang-switcher-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 10px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border-color);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 400;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.lang-switcher-btn:hover {
|
||||
background: var(--bg-tertiary);
|
||||
border-color: var(--accent-color);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.lang-switcher-icon {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.lang-dropdown {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: calc(100% + 6px);
|
||||
min-width: 120px;
|
||||
background: var(--bg-primary);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.16);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 4px 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.lang-option {
|
||||
padding: 6px 12px;
|
||||
font-size: 0.8125rem;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lang-option:hover {
|
||||
background: var(--bg-tertiary);
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.header-actions button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -1748,6 +1802,7 @@ header {
|
||||
|
||||
.chat-input-container textarea::placeholder {
|
||||
color: var(--text-muted);
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.chat-input-container .send-btn {
|
||||
|
||||
@@ -0,0 +1,925 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "OK",
|
||||
"cancel": "Cancel",
|
||||
"refresh": "Refresh",
|
||||
"close": "Close",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"save": "Save",
|
||||
"loading": "Loading…",
|
||||
"search": "Search",
|
||||
"clearSearch": "Clear search",
|
||||
"noData": "No data",
|
||||
"confirm": "Confirm",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied",
|
||||
"copyFailed": "Copy failed"
|
||||
},
|
||||
"header": {
|
||||
"title": "CyberStrikeAI",
|
||||
"apiDocs": "API Docs",
|
||||
"logout": "Sign out",
|
||||
"language": "Interface language",
|
||||
"backToDashboard": "Back to dashboard",
|
||||
"userMenu": "User menu",
|
||||
"version": "Current version",
|
||||
"toggleSidebar": "Collapse/expand sidebar"
|
||||
},
|
||||
"login": {
|
||||
"title": "Sign in to CyberStrikeAI",
|
||||
"subtitle": "Enter the access password from config",
|
||||
"passwordLabel": "Password",
|
||||
"passwordPlaceholder": "Enter password",
|
||||
"submit": "Sign in"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "Dashboard",
|
||||
"chat": "Chat",
|
||||
"infoCollect": "Recon",
|
||||
"tasks": "Tasks",
|
||||
"vulnerabilities": "Vulnerabilities",
|
||||
"mcp": "MCP",
|
||||
"mcpMonitor": "MCP Monitor",
|
||||
"mcpManagement": "MCP Management",
|
||||
"knowledge": "Knowledge",
|
||||
"knowledgeRetrievalLogs": "Retrieval history",
|
||||
"knowledgeManagement": "Knowledge management",
|
||||
"skills": "Skills",
|
||||
"skillsMonitor": "Skills monitor",
|
||||
"skillsManagement": "Skills management",
|
||||
"roles": "Roles",
|
||||
"rolesManagement": "Roles management",
|
||||
"settings": "System settings"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Dashboard",
|
||||
"refresh": "Refresh",
|
||||
"refreshData": "Refresh data",
|
||||
"runningTasks": "Running tasks",
|
||||
"vulnTotal": "Total vulnerabilities",
|
||||
"toolCalls": "Tool invocations",
|
||||
"successRate": "Tool success rate",
|
||||
"clickToViewTasks": "Click to view tasks",
|
||||
"clickToViewVuln": "Click to view vulnerabilities",
|
||||
"clickToViewMCP": "Click to view MCP monitor",
|
||||
"severityDistribution": "Vulnerability severity distribution",
|
||||
"severityCritical": "Critical",
|
||||
"severityHigh": "High",
|
||||
"severityMedium": "Medium",
|
||||
"severityLow": "Low",
|
||||
"severityInfo": "Info",
|
||||
"runOverview": "Run overview",
|
||||
"batchQueues": "Batch task queues",
|
||||
"pending": "Pending",
|
||||
"executing": "Running",
|
||||
"completed": "Completed",
|
||||
"toolInvocations": "Tool invocations",
|
||||
"callsUnit": "calls",
|
||||
"toolsUnit": "tools",
|
||||
"knowledgeLabel": "Knowledge",
|
||||
"knowledgeItems": "items",
|
||||
"categoriesUnit": "categories",
|
||||
"skillsLabel": "Skills",
|
||||
"skillUnit": "Skills",
|
||||
"quickLinks": "Quick links",
|
||||
"toolsExecCount": "Tool execution count",
|
||||
"ctaTitle": "Start your security journey",
|
||||
"ctaSub": "Describe your target in chat, AI will assist with scanning and vulnerability analysis",
|
||||
"goToChat": "Go to chat",
|
||||
"noTasks": "No tasks",
|
||||
"totalCount": "{{count}} total",
|
||||
"notEnabled": "Disabled",
|
||||
"enabled": "Enabled",
|
||||
"toConfigure": "To configure",
|
||||
"toUse": "To use",
|
||||
"active": "Active",
|
||||
"highFreq": "High frequency",
|
||||
"noCallData": "No call data"
|
||||
},
|
||||
"chat": {
|
||||
"newChat": "New chat",
|
||||
"searchHistory": "Search history...",
|
||||
"conversationGroups": "Conversation groups",
|
||||
"addGroup": "New group",
|
||||
"recentConversations": "Recent conversations",
|
||||
"batchManage": "Batch manage",
|
||||
"attackChain": "Attack chain",
|
||||
"viewAttackChain": "View attack chain",
|
||||
"selectRole": "Select role",
|
||||
"defaultRole": "Default",
|
||||
"inputPlaceholder": "Enter target or command... (type @ to select tools | Shift+Enter newline, Enter send)",
|
||||
"selectFile": "Select file",
|
||||
"uploadFile": "Upload file (multi-select or drag & drop)",
|
||||
"send": "Send",
|
||||
"searchInGroup": "Search in group...",
|
||||
"loadingTools": "Loading tools...",
|
||||
"noMatchTools": "No matching tools",
|
||||
"penetrationTestDetail": "Penetration test details",
|
||||
"expandDetail": "Expand details",
|
||||
"noProcessDetail": "No process details (execution may be too fast or no detailed events)",
|
||||
"copyMessageTitle": "Copy message",
|
||||
"emptyGroupConversations": "This group has no conversations yet.",
|
||||
"noMatchingConversationsInGroup": "No matching conversations found.",
|
||||
"renameGroupPrompt": "Please enter new name:",
|
||||
"deleteGroupConfirm": "Are you sure you want to delete this group? Conversations in the group will not be deleted, but will be removed from the group.",
|
||||
"deleteConversationConfirm": "Are you sure you want to delete this conversation?",
|
||||
"renameFailed": "Rename failed",
|
||||
"viewAttackChainSelectConv": "Please select a conversation to view attack chain",
|
||||
"viewAttackChainCurrentConv": "View attack chain of current conversation",
|
||||
"executeFailed": "Execution failed",
|
||||
"callOpenAIFailed": "Call OpenAI failed",
|
||||
"systemReadyMessage": "System is ready. Please enter your test requirements, and the system will automatically perform the corresponding security tests.",
|
||||
"addNewGroup": "+ New group"
|
||||
},
|
||||
"tasks": {
|
||||
"title": "Task management",
|
||||
"newTask": "New task",
|
||||
"autoRefresh": "Auto refresh",
|
||||
"historyHint": "Tip: Completed task history available. Check \"Show history\" to view.",
|
||||
"statusRunning": "Running",
|
||||
"statusCancelling": "Cancelling",
|
||||
"statusFailed": "Failed",
|
||||
"statusTimeout": "Timeout",
|
||||
"statusCancelled": "Cancelled",
|
||||
"statusCompleted": "Completed",
|
||||
"historyBadge": "History",
|
||||
"duration": "Duration",
|
||||
"completedAt": "Completed at",
|
||||
"startedAt": "Started at",
|
||||
"clickToCopy": "Click to copy",
|
||||
"unnamedTask": "Unnamed task",
|
||||
"unknown": "Unknown",
|
||||
"unknownTime": "Unknown time",
|
||||
"clearHistoryConfirm": "Clear all task history?",
|
||||
"cancelTaskFailed": "Cancel task failed",
|
||||
"copiedToast": "Copied!",
|
||||
"cancelling": "Cancelling...",
|
||||
"enterTaskPrompt": "Enter at least one task",
|
||||
"noValidTask": "No valid tasks",
|
||||
"createBatchQueueFailed": "Failed to create batch task queue",
|
||||
"noBatchQueues": "Currently there are no batch task queues",
|
||||
"recentCompletedTasks": "Recently completed tasks (last 24 hours)",
|
||||
"clearHistory": "Clear history",
|
||||
"cancelTask": "Cancel task",
|
||||
"viewConversation": "View conversation",
|
||||
"conversationIdLabel": "Conversation ID",
|
||||
"statusPending": "Pending",
|
||||
"statusPaused": "Paused",
|
||||
"confirmCancelTasks": "Cancel {{n}} selected task(s)?",
|
||||
"batchCancelResultPartial": "Batch cancel: {{success}} succeeded, {{fail}} failed",
|
||||
"batchCancelResultSuccess": "Successfully cancelled {{n}} task(s)",
|
||||
"taskCount": "{{count}} task(s)",
|
||||
"queueIdLabel": "Queue ID",
|
||||
"createdTimeLabel": "Created at",
|
||||
"totalLabel": "Total",
|
||||
"pendingLabel": "Pending",
|
||||
"runningLabel": "Running",
|
||||
"completedLabel": "Completed",
|
||||
"failedLabel": "Failed",
|
||||
"cancelledLabel": "Cancelled",
|
||||
"loadingTasks": "Loading...",
|
||||
"loadFailedRetry": "Load failed",
|
||||
"loadTaskListFailed": "Failed to load task list",
|
||||
"getQueueDetailFailed": "Failed to load queue details",
|
||||
"startBatchQueueFailed": "Failed to start batch queue",
|
||||
"pauseQueueFailed": "Failed to pause queue",
|
||||
"pauseQueueConfirm": "Pause this batch queue? The current task will be stopped; remaining tasks will stay pending.",
|
||||
"deleteQueueConfirm": "Delete this batch queue? This cannot be undone.",
|
||||
"deleteQueueFailed": "Failed to delete batch queue",
|
||||
"batchQueueTitle": "Batch task queue",
|
||||
"resumeExecute": "Resume",
|
||||
"taskIncomplete": "Task information incomplete",
|
||||
"cannotGetTaskMessageInput": "Cannot get task message input",
|
||||
"taskMessageRequired": "Task message is required",
|
||||
"saveTaskFailed": "Failed to save task",
|
||||
"queueInfoMissing": "Queue information not found",
|
||||
"addTaskFailed": "Failed to add task",
|
||||
"confirmDeleteTask": "Delete this task?\n\nTask: {{message}}\n\nThis cannot be undone.",
|
||||
"deleteTaskFailed": "Failed to delete task",
|
||||
"paginationShow": "{{start}}-{{end}} of {{total}}",
|
||||
"paginationPerPage": "Per page",
|
||||
"paginationFirst": "First",
|
||||
"paginationPrev": "Previous",
|
||||
"paginationNext": "Next",
|
||||
"paginationLast": "Last",
|
||||
"paginationPage": "Page {{current}} / {{total}}",
|
||||
"deleteQueue": "Delete queue",
|
||||
"retry": "Retry",
|
||||
"noMatchingTasks": "No matching tasks",
|
||||
"updateTaskFailed": "Failed to update task",
|
||||
"durationSeconds": "s",
|
||||
"durationMinutes": "m",
|
||||
"durationHours": "h"
|
||||
},
|
||||
"infoCollect": {
|
||||
"enterFofaQuery": "Enter FOFA query syntax",
|
||||
"querying": "Querying...",
|
||||
"queryFailed": "Query failed",
|
||||
"enterNaturalLanguage": "Enter natural language description",
|
||||
"cancelParse": "Cancel parse",
|
||||
"clickToCancelParse": "Click to cancel AI parse",
|
||||
"parseToFofa": "Parse natural language to FOFA query",
|
||||
"parseResultEmpty": "Parse result empty: Please add/modify FOFA query in popup",
|
||||
"queryPlaceholder": "e.g. app=\"Apache\" && country=\"CN\"",
|
||||
"selectAll": "Select all/none",
|
||||
"selectRow": "Select row",
|
||||
"copyTarget": "Copy target",
|
||||
"sendToChat": "Send to chat (editable; Ctrl/Cmd+click to send directly)",
|
||||
"noTargetToCopy": "No target to copy",
|
||||
"targetCopied": "Target copied",
|
||||
"manualCopyHint": "Copy failed, please copy manually: ",
|
||||
"cannotInferTarget": "Cannot infer scan target from row (include host/ip/port/domain in fields)",
|
||||
"noSendMessage": "sendMessage() not found, please refresh and retry",
|
||||
"filledToInput": "Filled to chat input, edit and send",
|
||||
"noExportResult": "No results to export",
|
||||
"xlsxNotLoaded": "XLSX library not loaded, please refresh and retry",
|
||||
"noResults": "No results",
|
||||
"selectRowsFirst": "Select rows to scan first",
|
||||
"noScanTarget": "No scan targets inferred from selection (include host/ip/port/domain in fields)",
|
||||
"batchScanFailed": "Batch scan failed",
|
||||
"batchQueueCreated": "Batch scan queue created",
|
||||
"field": "Field",
|
||||
"parsePending": "AI parsing...",
|
||||
"parsePendingClickCancel": "AI parsing... (click button to cancel)",
|
||||
"parseSlow": "AI parse is taking a while, still processing…",
|
||||
"parseDone": "AI parse complete",
|
||||
"parseCancelled": "AI parse cancelled",
|
||||
"parseFailed": "AI parse failed: ",
|
||||
"parseResultTitle": "AI parse result",
|
||||
"naturalLanguageLabel": "Natural language",
|
||||
"fofaQueryEditable": "FOFA query (editable)",
|
||||
"confirmBeforeQuery": "Confirm syntax and scope before running the query.",
|
||||
"reminder": "Reminder",
|
||||
"explanation": "Explanation",
|
||||
"actions": "Actions",
|
||||
"batchScanTitle": "FOFA batch scan",
|
||||
"queueCreatedSkipped": "Queue created ({{n}} rows skipped, no target)",
|
||||
"createQueueFailed": "Failed to create batch queue",
|
||||
"loading": "Loading...",
|
||||
"none": "None",
|
||||
"truncated": "truncated",
|
||||
"resultsMeta": "Total {{total}} · This page {{count}} · page={{page}} · size={{size}}",
|
||||
"parseModalCancel": "Cancel",
|
||||
"parseModalApply": "Fill into query",
|
||||
"parseModalApplyRun": "Fill and query"
|
||||
},
|
||||
"vulnerability": {
|
||||
"title": "Vulnerability management",
|
||||
"addVuln": "Add vulnerability",
|
||||
"editVuln": "Edit vulnerability",
|
||||
"loadFailed": "Failed to load vulnerabilities",
|
||||
"deleteConfirm": "Delete this vulnerability?"
|
||||
},
|
||||
"mcp": {
|
||||
"monitorTitle": "MCP Status Monitor",
|
||||
"execStats": "Execution stats",
|
||||
"latestExecutions": "Latest executions",
|
||||
"toolSearch": "Tool search",
|
||||
"toolSearchPlaceholder": "Enter tool name...",
|
||||
"statusFilter": "Status filter",
|
||||
"filterAll": "All",
|
||||
"selectedCount": "{{count}} selected",
|
||||
"selectAll": "Select all",
|
||||
"deselectAll": "Deselect all",
|
||||
"deleteSelected": "Batch delete",
|
||||
"deleteExecConfirm": "Delete this execution record?",
|
||||
"batchDeleteFailed": "Batch delete failed",
|
||||
"managementTitle": "MCP Management",
|
||||
"addExternal": "Add external MCP",
|
||||
"toolConfig": "MCP tool config",
|
||||
"saveToolConfig": "Save tool config",
|
||||
"externalConfig": "External MCP config",
|
||||
"loadingTools": "Loading tools...",
|
||||
"loadToolsTimeout": "Tools load timeout. External MCP may be slow. Click Refresh to retry or check connection.",
|
||||
"loadToolsFailed": "Failed to load tools",
|
||||
"noTools": "No tools",
|
||||
"externalBadge": "External",
|
||||
"externalFrom": "External ({{name}})",
|
||||
"externalToolFrom": "External MCP - Source: {{name}}",
|
||||
"noDescription": "No description",
|
||||
"paginationInfo": "{{start}}-{{end}} of {{total}} tools",
|
||||
"perPage": "Per page:",
|
||||
"firstPage": "First",
|
||||
"prevPage": "Previous",
|
||||
"nextPage": "Next",
|
||||
"lastPage": "Last",
|
||||
"pageInfo": "Page {{page}} of {{total}}",
|
||||
"currentPageEnabled": "Enabled on current page",
|
||||
"totalEnabled": "Total enabled",
|
||||
"toolsConfigSaved": "Tool configuration saved!",
|
||||
"saveToolsConfigFailed": "Failed to save tool config",
|
||||
"getConfigFailed": "Failed to get config",
|
||||
"noExternalMCP": "No external MCP configured",
|
||||
"clickToAddExternal": "Click \"Add external MCP\" to configure",
|
||||
"connected": "Connected",
|
||||
"connecting": "Connecting...",
|
||||
"connectionFailed": "Connection failed",
|
||||
"disabled": "Disabled",
|
||||
"disconnected": "Disconnected",
|
||||
"stopConnection": "Stop connection",
|
||||
"startConnection": "Start connection",
|
||||
"stop": "Stop",
|
||||
"start": "Start",
|
||||
"editConfig": "Edit config",
|
||||
"deleteConfig": "Delete config",
|
||||
"transportMode": "Transport",
|
||||
"toolCount": "Tool count",
|
||||
"description": "Description",
|
||||
"timeout": "Timeout",
|
||||
"command": "Command",
|
||||
"addExternalMCP": "Add external MCP",
|
||||
"editExternalMCP": "Edit external MCP",
|
||||
"jsonEmpty": "JSON cannot be empty",
|
||||
"jsonError": "JSON format error",
|
||||
"configMustBeObject": "Config error: Must be JSON object with name as key",
|
||||
"configNeedOne": "Config error: At least one config item required",
|
||||
"configNameEmpty": "Config error: Name cannot be empty",
|
||||
"configMustBeObj": "Config error: \"{{name}}\" must be object",
|
||||
"configNeedCommand": "Config error: \"{{name}}\" needs command (stdio) or url (http/sse)",
|
||||
"configStdioNeedCommand": "Config error: \"{{name}}\" stdio mode needs command",
|
||||
"configHttpNeedUrl": "Config error: \"{{name}}\" http mode needs url",
|
||||
"configSseNeedUrl": "Config error: \"{{name}}\" sse mode needs url",
|
||||
"saveSuccess": "Saved",
|
||||
"deleteSuccess": "Deleted",
|
||||
"deleteExternalConfirm": "Delete external MCP \"{{name}}\"?",
|
||||
"operationFailed": "Operation failed",
|
||||
"connectionFailedCheck": "Connection failed. Check config and network.",
|
||||
"connectionTimeout": "Connection timeout. Check config and network.",
|
||||
"totalCount": "Total",
|
||||
"enabledCount": "Enabled",
|
||||
"disabledCount": "Disabled",
|
||||
"connectedCount": "Connected"
|
||||
},
|
||||
"settings": {
|
||||
"title": "System settings",
|
||||
"nav": {
|
||||
"basic": "Basic",
|
||||
"robots": "Bots",
|
||||
"terminal": "Terminal",
|
||||
"security": "Security"
|
||||
},
|
||||
"robots": {
|
||||
"title": "Bot settings",
|
||||
"description": "Configure WeCom, DingTalk and Lark bots so you can chat with CyberStrikeAI on your phone without opening the web UI.",
|
||||
"wecom": {
|
||||
"title": "WeCom",
|
||||
"enabled": "Enable WeCom bot"
|
||||
},
|
||||
"dingtalk": {
|
||||
"title": "DingTalk",
|
||||
"enabled": "Enable DingTalk bot"
|
||||
},
|
||||
"lark": {
|
||||
"title": "Lark",
|
||||
"enabled": "Enable Lark bot"
|
||||
}
|
||||
},
|
||||
"apply": {
|
||||
"button": "Apply configuration",
|
||||
"loadFailed": "Failed to load configuration",
|
||||
"fillRequired": "Please fill in all required fields (marked with *)",
|
||||
"applyFailed": "Failed to apply configuration",
|
||||
"applySuccess": "Configuration applied successfully!"
|
||||
},
|
||||
"security": {
|
||||
"changePassword": "Change password",
|
||||
"fillPasswordHint": "Fill current and new password correctly. New password at least 8 characters, must match twice.",
|
||||
"changePasswordFailed": "Failed to change password",
|
||||
"passwordUpdated": "Password updated. Please sign in again with new password."
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"sessionExpired": "Session expired, please sign in again",
|
||||
"unauthorized": "Unauthorized",
|
||||
"enterPassword": "Please enter password",
|
||||
"loginFailedCheck": "Sign-in failed, please check the password",
|
||||
"loginFailedRetry": "Sign-in failed, please try again later",
|
||||
"loggedOut": "Signed out"
|
||||
},
|
||||
"knowledge": {
|
||||
"title": "Knowledge management",
|
||||
"retrievalLogs": "Retrieval history",
|
||||
"totalItems": "Total items",
|
||||
"categories": "Categories",
|
||||
"addKnowledge": "Add knowledge",
|
||||
"rebuildIndex": "Rebuild index",
|
||||
"rebuildIndexConfirm": "Rebuild index?",
|
||||
"deleteItemConfirm": "Delete this knowledge item?",
|
||||
"notEnabledTitle": "Knowledge base function not enabled",
|
||||
"notEnabledHint": "Please go to system settings to enable knowledge retrieval.",
|
||||
"goToSettings": "Go to settings"
|
||||
},
|
||||
"roles": {
|
||||
"title": "Role management",
|
||||
"createRole": "Create role",
|
||||
"searchPlaceholder": "Search roles...",
|
||||
"deleteConfirm": "Delete this role?"
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills management",
|
||||
"monitorTitle": "Skills monitor",
|
||||
"createSkill": "Create Skill",
|
||||
"callStats": "Call stats",
|
||||
"addSkill": "Add Skill",
|
||||
"editSkill": "Edit Skill",
|
||||
"loadListFailed": "Failed to load skills list",
|
||||
"noSkills": "No skills. Click \"Create Skill\" to add first.",
|
||||
"noMatch": "No matching skills",
|
||||
"searchFailed": "Search failed",
|
||||
"refreshed": "Refreshed",
|
||||
"loadDetailFailed": "Failed to load skill details",
|
||||
"viewFailed": "Failed to view skill",
|
||||
"saving": "Saving...",
|
||||
"saveFailed": "Failed to save skill",
|
||||
"deleteFailed": "Failed to delete skill",
|
||||
"loadStatsFailed": "Failed to load skills monitor data",
|
||||
"clearStatsConfirm": "Clear all Skills statistics? This cannot be undone.",
|
||||
"statsCleared": "Skills statistics cleared",
|
||||
"clearStatsFailed": "Failed to clear statistics"
|
||||
},
|
||||
"apiDocs": {
|
||||
"curlCopied": "curl command copied to clipboard!"
|
||||
},
|
||||
"chatGroup": {
|
||||
"search": "Search",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"clearSearch": "Clear search",
|
||||
"searchInGroupPlaceholder": "Search in group...",
|
||||
"attackChain": "Attack chain",
|
||||
"viewAttackChain": "View attack chain",
|
||||
"selectRole": "Select role",
|
||||
"close": "Close",
|
||||
"selectFile": "Select file",
|
||||
"uploadFile": "Upload file (multi-select or drag & drop)",
|
||||
"send": "Send",
|
||||
"rolePanelTitle": "Select role",
|
||||
"copyMessage": "Copy message",
|
||||
"remove": "Remove"
|
||||
},
|
||||
"mcpMonitor": {
|
||||
"deselectAll": "Deselect all",
|
||||
"statusPending": "Pending",
|
||||
"statusCompleted": "Completed",
|
||||
"statusRunning": "Running",
|
||||
"statusFailed": "Failed",
|
||||
"loading": "Loading...",
|
||||
"noStatsData": "No statistical data",
|
||||
"noExecutions": "No execution records",
|
||||
"noRecordsWithFilter": "No records with current filter",
|
||||
"paginationInfo": "Show {{start}}-{{end}} of {{total}} records",
|
||||
"perPageLabel": "Per page",
|
||||
"loadStatsError": "Failed to load statistics",
|
||||
"loadExecutionsError": "Failed to load execution records",
|
||||
"totalCalls": "Total calls",
|
||||
"successFailed": "Success {{success}} / Failed {{failed}}",
|
||||
"successRate": "Success rate",
|
||||
"statsFromAllTools": "From all tool calls",
|
||||
"lastCall": "Last call",
|
||||
"lastRefreshTime": "Last refresh",
|
||||
"noCallsYet": "No calls yet",
|
||||
"unknownTool": "Unknown tool",
|
||||
"successFailedRate": "Success {{success}} / Failed {{failed}} · {{rate}}% success rate",
|
||||
"columnTool": "Tool",
|
||||
"columnStatus": "Status",
|
||||
"columnStartTime": "Start time",
|
||||
"columnDuration": "Duration",
|
||||
"columnActions": "Actions",
|
||||
"viewDetail": "View details",
|
||||
"delete": "Delete",
|
||||
"deleteExecTitle": "Delete this execution record",
|
||||
"deleteExecConfirmSingle": "Are you sure you want to delete this execution record? This cannot be undone.",
|
||||
"deleteExecFailed": "Failed to delete execution record",
|
||||
"execDeleted": "Execution record deleted",
|
||||
"selectExecFirst": "Please select execution record(s) to delete first",
|
||||
"batchDeleteConfirm": "Are you sure you want to delete the selected {{count}} execution record(s)? This cannot be undone.",
|
||||
"batchDeleteSuccess": "Successfully deleted {{count}} execution record(s)",
|
||||
"unknown": "Unknown",
|
||||
"durationSeconds": "{{n}} sec",
|
||||
"durationMinutes": "{{minutes}} min {{seconds}} sec",
|
||||
"durationMinutesOnly": "{{minutes}} min",
|
||||
"durationHours": "{{hours}} hr {{minutes}} min",
|
||||
"durationHoursOnly": "{{hours}} hr"
|
||||
},
|
||||
"knowledgePage": {
|
||||
"totalContent": "Total content",
|
||||
"categoryFilter": "Category filter",
|
||||
"all": "All",
|
||||
"searchPlaceholder": "Search knowledge...",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"retrievalLogs": {
|
||||
"totalRetrievals": "Total retrievals",
|
||||
"successRetrievals": "Success",
|
||||
"successRate": "Success rate",
|
||||
"retrievedItems": "Items retrieved",
|
||||
"conversationId": "Conversation ID",
|
||||
"messageId": "Message ID",
|
||||
"filter": "Filter",
|
||||
"optionalConversation": "Optional: filter by conversation",
|
||||
"optionalMessage": "Optional: filter by message",
|
||||
"loading": "Loading...",
|
||||
"noRecords": "No retrieval records yet",
|
||||
"noQuery": "No query content",
|
||||
"itemsUnit": "items",
|
||||
"hasResults": "Has results",
|
||||
"noResults": "No results",
|
||||
"clickToCopy": "Click to copy",
|
||||
"retrievalResult": "Retrieval result",
|
||||
"foundCount": "Found {{count}} related knowledge item(s)",
|
||||
"foundUnknown": "Found related knowledge (count unknown)",
|
||||
"noMatch": "No matching knowledge items",
|
||||
"retrievedItemsLabel": "Retrieved knowledge items:",
|
||||
"viewDetails": "View details",
|
||||
"loadError": "Failed to load retrieval logs",
|
||||
"detailError": "Unable to get retrieval details",
|
||||
"deleteError": "Failed to delete retrieval log",
|
||||
"detailsTitle": "Retrieval details",
|
||||
"queryInfo": "Query info",
|
||||
"queryContent": "Query content:",
|
||||
"retrievalInfo": "Retrieval info",
|
||||
"riskType": "Risk type",
|
||||
"retrievalTime": "Retrieval time",
|
||||
"noItemDetails": "No knowledge item details found",
|
||||
"noContentPreview": "No content preview",
|
||||
"untitled": "Untitled",
|
||||
"uncategorized": "Uncategorized",
|
||||
"relatedInfo": "Related info",
|
||||
"itemsCount": "{{count}} knowledge item(s)",
|
||||
"deleteConfirm": "Delete this retrieval record?"
|
||||
},
|
||||
"infoCollectPage": {
|
||||
"title": "Recon",
|
||||
"reset": "Reset",
|
||||
"confirm": "OK",
|
||||
"fofaQuerySyntax": "FOFA query syntax",
|
||||
"naturalLanguage": "Natural language (AI parses to FOFA)",
|
||||
"returnCount": "Return count",
|
||||
"pageNum": "Page",
|
||||
"returnFields": "Return fields (comma-separated)",
|
||||
"queryResults": "Query results",
|
||||
"selectedRows": "{{count}} selected",
|
||||
"selectedRowsZero": "0 selected",
|
||||
"columns": "Columns",
|
||||
"exportCsv": "Export CSV",
|
||||
"exportJson": "Export JSON",
|
||||
"exportXlsx": "Export XLSX",
|
||||
"batchScan": "Batch scan",
|
||||
"showColumns": "Show columns",
|
||||
"columnsPanelAll": "Select all",
|
||||
"columnsPanelNone": "Deselect all",
|
||||
"columnsPanelClose": "Close",
|
||||
"formHint": "See FOFA docs for query syntax; supports && / || / ().",
|
||||
"parseBtn": "AI parse",
|
||||
"parseHint": "Result will open in a popup for editing before running the query.",
|
||||
"minFields": "Min fields",
|
||||
"webCommon": "Web common",
|
||||
"intelEnhanced": "Intel enhanced",
|
||||
"presetApache": "Apache + China",
|
||||
"presetLogin": "Login page + China",
|
||||
"presetDomain": "By domain",
|
||||
"presetIp": "By IP",
|
||||
"nlPlaceholder": "e.g. Apache sites in Missouri, US, title contains Home",
|
||||
"showHideColumns": "Show/hide columns",
|
||||
"exportCsvTitle": "Export results as CSV (UTF-8)",
|
||||
"exportJsonTitle": "Export results as JSON",
|
||||
"exportXlsxTitle": "Export results as Excel",
|
||||
"batchScanTitle": "Create batch task queue from selected rows"
|
||||
},
|
||||
"vulnerabilityPage": {
|
||||
"statTotal": "Total",
|
||||
"filter": "Filter",
|
||||
"clear": "Clear",
|
||||
"vulnId": "Vuln ID",
|
||||
"conversationId": "Conversation ID",
|
||||
"severity": "Severity",
|
||||
"status": "Status",
|
||||
"statusOpen": "Open",
|
||||
"statusConfirmed": "Confirmed",
|
||||
"statusFixed": "Fixed",
|
||||
"statusFalsePositive": "False positive",
|
||||
"searchVulnId": "Search vuln ID",
|
||||
"filterConversation": "Filter by conversation",
|
||||
"loading": "Loading...",
|
||||
"noRecords": "No vulnerability records"
|
||||
},
|
||||
"tasksPage": {
|
||||
"statusFilter": "Status filter",
|
||||
"statusPending": "Pending",
|
||||
"statusPaused": "Paused",
|
||||
"statusCancelled": "Cancelled",
|
||||
"searchQueuePlaceholder": "Search queue ID, title or created time",
|
||||
"searchKeywordPlaceholder": "Enter keyword..."
|
||||
},
|
||||
"skillsPage": {
|
||||
"clearStats": "Clear stats",
|
||||
"clearStatsTitle": "Clear all statistics",
|
||||
"skillsCallStats": "Skills call stats",
|
||||
"searchPlaceholder": "Search Skills...",
|
||||
"loading": "Loading..."
|
||||
},
|
||||
"settingsBasic": {
|
||||
"basicTitle": "Basic settings",
|
||||
"openaiConfig": "OpenAI config",
|
||||
"fofaConfig": "FOFA config",
|
||||
"agentConfig": "Agent config",
|
||||
"knowledgeConfig": "Knowledge base config",
|
||||
"baseUrl": "Base URL",
|
||||
"apiKey": "API Key",
|
||||
"model": "Model",
|
||||
"openaiBaseUrlPlaceholder": "https://api.openai.com/v1",
|
||||
"openaiApiKeyPlaceholder": "Enter OpenAI API Key",
|
||||
"modelPlaceholder": "gpt-4",
|
||||
"fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all (optional)",
|
||||
"fofaBaseUrlHint": "Leave empty for default.",
|
||||
"email": "Email",
|
||||
"fofaEmailPlaceholder": "Enter FOFA email",
|
||||
"fofaApiKeyPlaceholder": "Enter FOFA API Key",
|
||||
"fofaApiKeyHint": "Stored in server config (config.yaml) only.",
|
||||
"maxIterations": "Max iterations",
|
||||
"iterationsPlaceholder": "30",
|
||||
"enableKnowledge": "Enable knowledge retrieval",
|
||||
"knowledgeBasePath": "Knowledge base path",
|
||||
"knowledgeBasePathPlaceholder": "knowledge_base",
|
||||
"knowledgeBasePathHint": "Relative to config file directory",
|
||||
"embeddingConfig": "Embedding config",
|
||||
"provider": "Provider",
|
||||
"embeddingBaseUrlPlaceholder": "Leave empty to use OpenAI base_url",
|
||||
"embeddingApiKeyPlaceholder": "Leave empty to use OpenAI api_key",
|
||||
"modelName": "Model name",
|
||||
"embeddingModelPlaceholder": "text-embedding-v4",
|
||||
"retrievalConfig": "Retrieval config",
|
||||
"topK": "Top-K results",
|
||||
"topKPlaceholder": "5",
|
||||
"topKHint": "Number of top-K results to return",
|
||||
"similarityThreshold": "Similarity threshold",
|
||||
"similarityPlaceholder": "0.7",
|
||||
"similarityHint": "Results below this value are filtered (0-1)",
|
||||
"hybridWeight": "Hybrid weight",
|
||||
"hybridPlaceholder": "0.7",
|
||||
"hybridHint": "Vector weight (0-1); 1.0 = vector only, 0.0 = keyword only",
|
||||
"indexConfig": "Index config",
|
||||
"chunkSize": "Chunk size",
|
||||
"chunkSizePlaceholder": "512",
|
||||
"chunkSizeHint": "Max tokens per chunk (default 512)",
|
||||
"chunkOverlap": "Chunk overlap",
|
||||
"chunkOverlapPlaceholder": "50",
|
||||
"chunkOverlapHint": "Overlap tokens between chunks (default 50)",
|
||||
"maxChunksPerItem": "Max chunks per item",
|
||||
"maxChunksPlaceholder": "0",
|
||||
"maxChunksHint": "Max chunks per knowledge item (0 = no limit)",
|
||||
"maxRpm": "Max RPM",
|
||||
"maxRpmPlaceholder": "0",
|
||||
"maxRpmHint": "Max requests per minute (0 = no limit)",
|
||||
"rateLimitDelay": "Rate limit delay (ms)",
|
||||
"rateLimitPlaceholder": "300",
|
||||
"rateLimitHint": "Delay between requests (ms); 0 = no limit",
|
||||
"maxRetries": "Max retries",
|
||||
"maxRetriesPlaceholder": "3",
|
||||
"maxRetriesHint": "Retries on rate limit or server error",
|
||||
"retryDelay": "Retry delay (ms)",
|
||||
"retryDelayPlaceholder": "1000",
|
||||
"retryDelayHint": "Delay between retries (ms)"
|
||||
},
|
||||
"settingsTerminal": {
|
||||
"title": "Terminal",
|
||||
"description": "Run commands on the server for ops and debugging. Commands run on the server; avoid sensitive or destructive operations.",
|
||||
"terminalTab": "Terminal {{n}}",
|
||||
"close": "Close",
|
||||
"newTerminal": "New terminal"
|
||||
},
|
||||
"settingsSecurity": {
|
||||
"changePasswordTitle": "Change password",
|
||||
"changePasswordDesc": "After changing password, sign in again with the new password.",
|
||||
"currentPassword": "Current password",
|
||||
"currentPasswordPlaceholder": "Enter current password",
|
||||
"newPassword": "New password",
|
||||
"newPasswordPlaceholder": "New password (at least 8 characters)",
|
||||
"confirmPassword": "Confirm new password",
|
||||
"confirmPasswordPlaceholder": "Enter new password again",
|
||||
"clear": "Clear",
|
||||
"changePasswordBtn": "Change password"
|
||||
},
|
||||
"settingsRobotsExtra": {
|
||||
"botCommandsTitle": "Bot commands",
|
||||
"botCommandsDesc": "You can send these commands in chat (Chinese and English supported):"
|
||||
},
|
||||
"mcpDetailModal": {
|
||||
"title": "Tool call details",
|
||||
"execInfo": "Execution info",
|
||||
"tool": "Tool",
|
||||
"status": "Status",
|
||||
"time": "Time",
|
||||
"executionId": "Execution ID",
|
||||
"requestParams": "Request params",
|
||||
"copyJson": "Copy JSON",
|
||||
"responseResult": "Response",
|
||||
"copyContent": "Copy content",
|
||||
"correctInfo": "Correct info",
|
||||
"errorInfo": "Error info",
|
||||
"copyError": "Copy error"
|
||||
},
|
||||
"attackChainModal": {
|
||||
"title": "Attack chain",
|
||||
"regenerate": "Regenerate",
|
||||
"regenerateTitle": "Regenerate attack chain (include latest conversation)",
|
||||
"exportPng": "Export PNG",
|
||||
"exportSvg": "Export SVG",
|
||||
"refreshTitle": "Refresh current attack chain",
|
||||
"nodesEdges": "Nodes: {{nodes}} | Edges: {{edges}}",
|
||||
"searchPlaceholder": "Search nodes...",
|
||||
"allTypes": "All types",
|
||||
"target": "Target",
|
||||
"action": "Action",
|
||||
"vulnerability": "Vulnerability",
|
||||
"allRisks": "All risks",
|
||||
"highRisk": "High (80-100)",
|
||||
"mediumHighRisk": "Medium-high (60-79)",
|
||||
"mediumRisk": "Medium (40-59)",
|
||||
"lowRisk": "Low (0-39)",
|
||||
"resetFilter": "Reset filter",
|
||||
"loading": "Loading...",
|
||||
"riskLevel": "Risk level",
|
||||
"lineMeaning": "Line meaning",
|
||||
"blueLine": "Blue: action finds vulnerability",
|
||||
"redLine": "Red: enables/contributes",
|
||||
"grayLine": "Gray: logical order",
|
||||
"nodeDetails": "Node details",
|
||||
"closeDetails": "Close details"
|
||||
},
|
||||
"externalMcpModal": {
|
||||
"configJson": "Config JSON",
|
||||
"formatLabel": "Format:",
|
||||
"formatDesc": "JSON object; key = config name, value = config. Use Start/Stop buttons to control state.",
|
||||
"formatJson": "Format JSON",
|
||||
"loadExample": "Load example"
|
||||
},
|
||||
"skillModal": {
|
||||
"addSkill": "Add Skill",
|
||||
"editSkill": "Edit Skill",
|
||||
"skillName": "Skill name",
|
||||
"skillNamePlaceholder": "e.g. sql-injection-testing",
|
||||
"skillNameHint": "Letters, numbers, hyphens and underscores only",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Short description",
|
||||
"contentLabel": "Content (Markdown)",
|
||||
"contentPlaceholder": "Enter skill content in Markdown...",
|
||||
"contentHint": "YAML front matter supported (optional)"
|
||||
},
|
||||
"knowledgeItemModal": {
|
||||
"addKnowledge": "Add knowledge",
|
||||
"editKnowledge": "Edit knowledge",
|
||||
"category": "Category (risk type)",
|
||||
"categoryPlaceholder": "e.g. SQL injection",
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "Knowledge item title",
|
||||
"contentLabel": "Content (Markdown)",
|
||||
"contentPlaceholder": "Enter content in Markdown..."
|
||||
},
|
||||
"batchManageModal": {
|
||||
"title": "Manage conversations · {{count}} total",
|
||||
"searchPlaceholder": "Search history",
|
||||
"conversationName": "Conversation name",
|
||||
"lastTime": "Last activity",
|
||||
"action": "Action",
|
||||
"selectAll": "Select all",
|
||||
"deleteSelected": "Delete selected",
|
||||
"confirmDeleteNone": "Please select at least one conversation to delete",
|
||||
"confirmDeleteN": "Delete {{count}} selected conversation(s)?",
|
||||
"deleteFailed": "Delete failed",
|
||||
"unnamedConversation": "Unnamed conversation"
|
||||
},
|
||||
"createGroupModal": {
|
||||
"title": "Create group",
|
||||
"description": "Group conversations for easier management.",
|
||||
"selectIcon": "Click to choose icon",
|
||||
"groupNamePlaceholder": "Enter group name",
|
||||
"pickIcon": "Pick icon",
|
||||
"customIcon": "Custom",
|
||||
"confirmIcon": "OK",
|
||||
"create": "Create",
|
||||
"cancel": "Cancel",
|
||||
"suggestionPenetrationTest": "Penetration Testing",
|
||||
"suggestionCtf": "CTF",
|
||||
"suggestionRedTeam": "Red Team",
|
||||
"suggestionVulnerabilityMining": "Vulnerability Mining",
|
||||
"nameExists": "Group name already exists, please use another name.",
|
||||
"createFailed": "Create failed",
|
||||
"unknownError": "Unknown error"
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "View attack chain",
|
||||
"rename": "Rename",
|
||||
"pinConversation": "Pin conversation",
|
||||
"unpinConversation": "Unpin",
|
||||
"batchManage": "Batch manage",
|
||||
"moveToGroup": "Move to group",
|
||||
"deleteConversation": "Delete conversation",
|
||||
"pinGroup": "Pin group",
|
||||
"unpinGroup": "Unpin",
|
||||
"deleteGroup": "Delete group"
|
||||
},
|
||||
"batchImportModal": {
|
||||
"title": "New task",
|
||||
"queueTitle": "Queue title",
|
||||
"queueTitlePlaceholder": "Enter queue title (optional, for identification and filtering)",
|
||||
"queueTitleHint": "Set a title for the batch task queue to make it easier to find and manage later.",
|
||||
"role": "Role",
|
||||
"defaultRole": "Default",
|
||||
"roleHint": "Select a role; all tasks will be executed using that role's configuration (prompt and tools).",
|
||||
"tasksList": "Task list (one task per line)",
|
||||
"tasksListPlaceholder": "Enter task list, one per line",
|
||||
"tasksListPlaceholderExample": "Enter task list, one per line, for example:\nScan open ports of 192.168.1.1\nCheck if https://example.com has SQL injection\nEnumerate subdomains of example.com",
|
||||
"tasksListHint": "Enter one task command per line; the system will execute them in order. Empty lines are ignored.",
|
||||
"tasksListHintFull": "Hint: Enter one task command per line; the system will execute these tasks in order. Empty lines are ignored.",
|
||||
"createQueue": "Create queue"
|
||||
},
|
||||
"batchQueueDetailModal": {
|
||||
"title": "Batch queue details",
|
||||
"addTask": "Add task",
|
||||
"startExecute": "Start",
|
||||
"pauseQueue": "Pause queue",
|
||||
"deleteQueue": "Delete queue",
|
||||
"queueTitle": "Task title",
|
||||
"role": "Role",
|
||||
"defaultRole": "Default",
|
||||
"queueId": "Queue ID",
|
||||
"status": "Status",
|
||||
"createdAt": "Created at",
|
||||
"startedAt": "Started at",
|
||||
"completedAt": "Completed at",
|
||||
"taskTotal": "Total tasks",
|
||||
"taskList": "Task list",
|
||||
"startLabel": "Start",
|
||||
"completeLabel": "Complete",
|
||||
"errorLabel": "Error",
|
||||
"resultLabel": "Result"
|
||||
},
|
||||
"editBatchTaskModal": {
|
||||
"title": "Edit task",
|
||||
"taskMessage": "Task message",
|
||||
"taskMessagePlaceholder": "Enter task message"
|
||||
},
|
||||
"addBatchTaskModal": {
|
||||
"title": "Add task",
|
||||
"taskMessage": "Task message",
|
||||
"taskMessagePlaceholder": "Enter task message",
|
||||
"add": "Add"
|
||||
},
|
||||
"vulnerabilityModal": {
|
||||
"conversationId": "Conversation ID",
|
||||
"conversationIdPlaceholder": "Enter conversation ID",
|
||||
"title": "Title",
|
||||
"titlePlaceholder": "Vulnerability title",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Detailed description",
|
||||
"severity": "Severity",
|
||||
"pleaseSelect": "Please select",
|
||||
"severityCritical": "Critical",
|
||||
"severityHigh": "High",
|
||||
"severityMedium": "Medium",
|
||||
"severityLow": "Low",
|
||||
"severityInfo": "Info",
|
||||
"status": "Status",
|
||||
"statusOpen": "Open",
|
||||
"statusConfirmed": "Confirmed",
|
||||
"statusFixed": "Fixed",
|
||||
"statusFalsePositive": "False positive",
|
||||
"type": "Vulnerability type",
|
||||
"typePlaceholder": "e.g. SQL injection, XSS, CSRF",
|
||||
"target": "Target",
|
||||
"targetPlaceholder": "Affected target (URL, IP, etc.)",
|
||||
"proof": "Proof (POC)",
|
||||
"proofPlaceholder": "Proof: request/response, screenshots, etc.",
|
||||
"impact": "Impact",
|
||||
"impactPlaceholder": "Impact description",
|
||||
"recommendation": "Recommendation",
|
||||
"recommendationPlaceholder": "Remediation"
|
||||
},
|
||||
"roleModal": {
|
||||
"addRole": "Add role",
|
||||
"editRole": "Edit role",
|
||||
"roleName": "Role name",
|
||||
"roleNamePlaceholder": "Enter role name",
|
||||
"roleDescription": "Role description",
|
||||
"roleDescriptionPlaceholder": "Enter role description",
|
||||
"roleIcon": "Role icon",
|
||||
"roleIconPlaceholder": "Enter emoji, e.g. 🏆",
|
||||
"roleIconHint": "Emoji shown in role selector.",
|
||||
"userPrompt": "User prompt",
|
||||
"userPromptPlaceholder": "Appended before user message...",
|
||||
"userPromptHint": "This prompt is appended before user message to guide AI. It does not change system prompt.",
|
||||
"relatedTools": "Related tools (optional)",
|
||||
"defaultRoleToolsTitle": "Default role uses all tools",
|
||||
"defaultRoleToolsDesc": "Default role uses all tools enabled in MCP management.",
|
||||
"searchToolsPlaceholder": "Search tools...",
|
||||
"loadingTools": "Loading tools...",
|
||||
"relatedToolsHint": "Select tools to link; empty = use all from MCP management.",
|
||||
"relatedSkills": "Related Skills (optional)",
|
||||
"searchSkillsPlaceholder": "Search skill...",
|
||||
"loadingSkills": "Loading skills...",
|
||||
"relatedSkillsHint": "Selected skills are injected into system prompt before task execution.",
|
||||
"enableRole": "Enable this role"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,925 @@
|
||||
{
|
||||
"common": {
|
||||
"ok": "确定",
|
||||
"cancel": "取消",
|
||||
"refresh": "刷新",
|
||||
"close": "关闭",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"save": "保存",
|
||||
"loading": "加载中…",
|
||||
"search": "搜索",
|
||||
"clearSearch": "清除搜索",
|
||||
"noData": "暂无数据",
|
||||
"confirm": "确认",
|
||||
"copy": "复制",
|
||||
"copied": "已复制",
|
||||
"copyFailed": "复制失败"
|
||||
},
|
||||
"header": {
|
||||
"title": "CyberStrikeAI",
|
||||
"apiDocs": "API 文档",
|
||||
"logout": "退出登录",
|
||||
"language": "界面语言",
|
||||
"backToDashboard": "返回仪表盘",
|
||||
"userMenu": "用户菜单",
|
||||
"version": "当前版本",
|
||||
"toggleSidebar": "折叠/展开侧边栏"
|
||||
},
|
||||
"login": {
|
||||
"title": "登录 CyberStrikeAI",
|
||||
"subtitle": "请输入配置中的访问密码",
|
||||
"passwordLabel": "密码",
|
||||
"passwordPlaceholder": "输入登录密码",
|
||||
"submit": "登录"
|
||||
},
|
||||
"nav": {
|
||||
"dashboard": "仪表盘",
|
||||
"chat": "对话",
|
||||
"infoCollect": "信息收集",
|
||||
"tasks": "任务管理",
|
||||
"vulnerabilities": "漏洞管理",
|
||||
"mcp": "MCP",
|
||||
"mcpMonitor": "MCP状态监控",
|
||||
"mcpManagement": "MCP管理",
|
||||
"knowledge": "知识",
|
||||
"knowledgeRetrievalLogs": "检索历史",
|
||||
"knowledgeManagement": "知识管理",
|
||||
"skills": "Skills",
|
||||
"skillsMonitor": "Skills状态监控",
|
||||
"skillsManagement": "Skills管理",
|
||||
"roles": "角色",
|
||||
"rolesManagement": "角色管理",
|
||||
"settings": "系统设置"
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "仪表盘",
|
||||
"refresh": "刷新",
|
||||
"refreshData": "刷新数据",
|
||||
"runningTasks": "运行中任务",
|
||||
"vulnTotal": "漏洞总数",
|
||||
"toolCalls": "工具调用次数",
|
||||
"successRate": "工具执行成功率",
|
||||
"clickToViewTasks": "点击查看任务管理",
|
||||
"clickToViewVuln": "点击查看漏洞管理",
|
||||
"clickToViewMCP": "点击查看 MCP 监控",
|
||||
"severityDistribution": "漏洞严重程度分布",
|
||||
"severityCritical": "严重",
|
||||
"severityHigh": "高危",
|
||||
"severityMedium": "中危",
|
||||
"severityLow": "低危",
|
||||
"severityInfo": "信息",
|
||||
"runOverview": "运行概览",
|
||||
"batchQueues": "批量任务队列",
|
||||
"pending": "待执行",
|
||||
"executing": "执行中",
|
||||
"completed": "已完成",
|
||||
"toolInvocations": "工具调用",
|
||||
"callsUnit": "次调用",
|
||||
"toolsUnit": "个工具",
|
||||
"knowledgeLabel": "知识",
|
||||
"knowledgeItems": "项知识",
|
||||
"categoriesUnit": "个分类",
|
||||
"skillsLabel": "Skills",
|
||||
"skillUnit": "个 Skill",
|
||||
"quickLinks": "快捷入口",
|
||||
"toolsExecCount": "工具执行次数",
|
||||
"ctaTitle": "开始你的安全之旅",
|
||||
"ctaSub": "在对话中描述目标,AI 将协助执行扫描与漏洞分析",
|
||||
"goToChat": "前往对话",
|
||||
"noTasks": "暂无任务",
|
||||
"totalCount": "共 {{count}} 个",
|
||||
"notEnabled": "未启用",
|
||||
"enabled": "已启用",
|
||||
"toConfigure": "待配置",
|
||||
"toUse": "待使用",
|
||||
"active": "活跃",
|
||||
"highFreq": "高频",
|
||||
"noCallData": "暂无调用数据"
|
||||
},
|
||||
"chat": {
|
||||
"newChat": "新对话",
|
||||
"searchHistory": "搜索历史记录...",
|
||||
"conversationGroups": "对话分组",
|
||||
"addGroup": "新建分组",
|
||||
"recentConversations": "最近对话",
|
||||
"batchManage": "批量管理",
|
||||
"attackChain": "攻击链",
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"selectRole": "选择角色",
|
||||
"defaultRole": "默认",
|
||||
"inputPlaceholder": "输入测试目标或命令... (输入 @ 选择工具 | Shift+Enter 换行,Enter 发送)",
|
||||
"selectFile": "选择文件",
|
||||
"uploadFile": "上传文件(可多选或拖拽到此处)",
|
||||
"send": "发送",
|
||||
"searchInGroup": "搜索分组中的对话...",
|
||||
"loadingTools": "正在加载工具...",
|
||||
"noMatchTools": "没有匹配的工具",
|
||||
"penetrationTestDetail": "渗透测试详情",
|
||||
"expandDetail": "展开详情",
|
||||
"noProcessDetail": "暂无过程详情(可能执行过快或未触发详细事件)",
|
||||
"copyMessageTitle": "复制消息内容",
|
||||
"emptyGroupConversations": "该分组暂无对话",
|
||||
"noMatchingConversationsInGroup": "未找到匹配的对话",
|
||||
"renameGroupPrompt": "请输入新名称:",
|
||||
"deleteGroupConfirm": "确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。",
|
||||
"deleteConversationConfirm": "确定要删除此对话吗?",
|
||||
"renameFailed": "重命名失败",
|
||||
"viewAttackChainSelectConv": "请选择一个对话以查看攻击链",
|
||||
"viewAttackChainCurrentConv": "查看当前对话的攻击链",
|
||||
"executeFailed": "执行失败",
|
||||
"callOpenAIFailed": "调用OpenAI失败",
|
||||
"systemReadyMessage": "系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。",
|
||||
"addNewGroup": "+ 新增分组"
|
||||
},
|
||||
"tasks": {
|
||||
"title": "任务管理",
|
||||
"newTask": "新建任务",
|
||||
"autoRefresh": "自动刷新",
|
||||
"historyHint": "提示:有已完成的任务历史,请勾选\"显示历史记录\"查看",
|
||||
"statusRunning": "执行中",
|
||||
"statusCancelling": "取消中",
|
||||
"statusFailed": "执行失败",
|
||||
"statusTimeout": "执行超时",
|
||||
"statusCancelled": "已取消",
|
||||
"statusCompleted": "已完成",
|
||||
"historyBadge": "历史记录",
|
||||
"duration": "执行时长",
|
||||
"completedAt": "完成时间",
|
||||
"startedAt": "开始时间",
|
||||
"clickToCopy": "点击复制",
|
||||
"unnamedTask": "未命名任务",
|
||||
"unknown": "未知",
|
||||
"unknownTime": "未知时间",
|
||||
"clearHistoryConfirm": "确定要清空所有任务历史记录吗?",
|
||||
"cancelTaskFailed": "取消任务失败",
|
||||
"copiedToast": "已复制!",
|
||||
"cancelling": "取消中...",
|
||||
"enterTaskPrompt": "请输入至少一个任务",
|
||||
"noValidTask": "没有有效的任务",
|
||||
"createBatchQueueFailed": "创建批量任务队列失败",
|
||||
"noBatchQueues": "当前没有批量任务队列",
|
||||
"recentCompletedTasks": "最近完成的任务(最近24小时)",
|
||||
"clearHistory": "清空历史",
|
||||
"cancelTask": "取消任务",
|
||||
"viewConversation": "查看对话",
|
||||
"conversationIdLabel": "对话ID",
|
||||
"statusPending": "待执行",
|
||||
"statusPaused": "已暂停",
|
||||
"confirmCancelTasks": "确定要取消 {{n}} 个任务吗?",
|
||||
"batchCancelResultPartial": "批量取消完成:成功 {{success}} 个,失败 {{fail}} 个",
|
||||
"batchCancelResultSuccess": "成功取消 {{n}} 个任务",
|
||||
"taskCount": "共 {{count}} 个任务",
|
||||
"queueIdLabel": "队列ID",
|
||||
"createdTimeLabel": "创建时间",
|
||||
"totalLabel": "总计",
|
||||
"pendingLabel": "待执行",
|
||||
"runningLabel": "执行中",
|
||||
"completedLabel": "已完成",
|
||||
"failedLabel": "失败",
|
||||
"cancelledLabel": "已取消",
|
||||
"loadingTasks": "加载中...",
|
||||
"loadFailedRetry": "加载失败",
|
||||
"loadTaskListFailed": "获取任务列表失败",
|
||||
"getQueueDetailFailed": "获取队列详情失败",
|
||||
"startBatchQueueFailed": "启动批量任务失败",
|
||||
"pauseQueueFailed": "暂停批量任务失败",
|
||||
"pauseQueueConfirm": "确定要暂停这个批量任务队列吗?当前正在执行的任务将被停止,后续任务将保留待执行状态。",
|
||||
"deleteQueueConfirm": "确定要删除这个批量任务队列吗?此操作不可恢复。",
|
||||
"deleteQueueFailed": "删除批量任务队列失败",
|
||||
"batchQueueTitle": "批量任务队列",
|
||||
"resumeExecute": "继续执行",
|
||||
"taskIncomplete": "任务信息不完整",
|
||||
"cannotGetTaskMessageInput": "无法获取任务消息输入框",
|
||||
"taskMessageRequired": "任务消息不能为空",
|
||||
"saveTaskFailed": "保存任务失败",
|
||||
"queueInfoMissing": "队列信息不存在",
|
||||
"addTaskFailed": "添加任务失败",
|
||||
"confirmDeleteTask": "确定要删除这个任务吗?\n\n任务内容: {{message}}\n\n此操作不可恢复。",
|
||||
"deleteTaskFailed": "删除任务失败",
|
||||
"paginationShow": "显示 {{start}}-{{end}} / 共 {{total}} 条",
|
||||
"paginationPerPage": "每页显示",
|
||||
"paginationFirst": "首页",
|
||||
"paginationPrev": "上一页",
|
||||
"paginationNext": "下一页",
|
||||
"paginationLast": "末页",
|
||||
"paginationPage": "第 {{current}} / {{total}} 页",
|
||||
"deleteQueue": "删除队列",
|
||||
"retry": "重试",
|
||||
"noMatchingTasks": "当前没有符合条件的任务",
|
||||
"updateTaskFailed": "更新任务失败",
|
||||
"durationSeconds": "秒",
|
||||
"durationMinutes": "分",
|
||||
"durationHours": "小时"
|
||||
},
|
||||
"infoCollect": {
|
||||
"enterFofaQuery": "请输入 FOFA 查询语法",
|
||||
"querying": "查询中...",
|
||||
"queryFailed": "查询失败",
|
||||
"enterNaturalLanguage": "请输入自然语言描述",
|
||||
"cancelParse": "取消解析",
|
||||
"clickToCancelParse": "点击取消 AI 解析",
|
||||
"parseToFofa": "将自然语言解析为 FOFA 查询语法",
|
||||
"parseResultEmpty": "解析结果为空:请在弹窗中补充/修改 FOFA 查询语法",
|
||||
"queryPlaceholder": "例如:app=\"Apache\" && country=\"CN\"",
|
||||
"selectAll": "全选/全不选",
|
||||
"selectRow": "选择该行",
|
||||
"copyTarget": "复制目标",
|
||||
"sendToChat": "发送到对话(可编辑;Ctrl/⌘+点击可直接发送)",
|
||||
"noTargetToCopy": "没有可复制的目标",
|
||||
"targetCopied": "已复制目标",
|
||||
"manualCopyHint": "复制失败,请手动复制:",
|
||||
"cannotInferTarget": "无法从该行推断扫描目标(建议在 fields 中包含 host/ip/port/domain)",
|
||||
"noSendMessage": "未找到 sendMessage(),请刷新页面后重试",
|
||||
"filledToInput": "已填入对话输入框,可编辑后发送",
|
||||
"noExportResult": "暂无可导出的结果",
|
||||
"xlsxNotLoaded": "未加载 XLSX 库,请刷新页面后重试",
|
||||
"noResults": "暂无结果",
|
||||
"selectRowsFirst": "请先勾选需要扫描的行",
|
||||
"noScanTarget": "未能从所选行推断任何可扫描目标(建议 fields 中包含 host/ip/port/domain)",
|
||||
"batchScanFailed": "批量扫描失败",
|
||||
"batchQueueCreated": "已创建批量扫描队列",
|
||||
"field": "字段",
|
||||
"parsePending": "AI 解析中...",
|
||||
"parsePendingClickCancel": "AI 解析中...(点击按钮可取消)",
|
||||
"parseSlow": "AI 解析耗时较长,仍在处理中…",
|
||||
"parseDone": "AI 解析完成",
|
||||
"parseCancelled": "已取消 AI 解析",
|
||||
"parseFailed": "AI 解析失败:",
|
||||
"parseResultTitle": "AI 解析结果",
|
||||
"naturalLanguageLabel": "自然语言",
|
||||
"fofaQueryEditable": "FOFA 查询语法(可编辑)",
|
||||
"confirmBeforeQuery": "请人工确认语法与范围无误后再执行查询。",
|
||||
"reminder": "提醒",
|
||||
"explanation": "解析说明",
|
||||
"actions": "操作",
|
||||
"batchScanTitle": "FOFA 批量扫描",
|
||||
"queueCreatedSkipped": "已创建队列(跳过 {{n}} 条无目标行)",
|
||||
"createQueueFailed": "创建批量队列失败",
|
||||
"loading": "加载中...",
|
||||
"none": "无",
|
||||
"truncated": "已截断",
|
||||
"resultsMeta": "共 {{total}} 条 · 本页 {{count}} 条 · page={{page}} · size={{size}}",
|
||||
"parseModalCancel": "取消",
|
||||
"parseModalApply": "填入查询框",
|
||||
"parseModalApplyRun": "填入并查询"
|
||||
},
|
||||
"vulnerability": {
|
||||
"title": "漏洞管理",
|
||||
"addVuln": "添加漏洞",
|
||||
"editVuln": "编辑漏洞",
|
||||
"loadFailed": "加载漏洞失败",
|
||||
"deleteConfirm": "确定要删除此漏洞吗?"
|
||||
},
|
||||
"mcp": {
|
||||
"monitorTitle": "MCP 状态监控",
|
||||
"execStats": "执行统计",
|
||||
"latestExecutions": "最新执行记录",
|
||||
"toolSearch": "工具搜索",
|
||||
"toolSearchPlaceholder": "输入工具名称...",
|
||||
"statusFilter": "状态筛选",
|
||||
"filterAll": "全部",
|
||||
"selectedCount": "已选择 {{count}} 项",
|
||||
"selectAll": "全选",
|
||||
"deselectAll": "全不选",
|
||||
"deleteSelected": "批量删除",
|
||||
"deleteExecConfirm": "确定要删除此执行记录吗?",
|
||||
"batchDeleteFailed": "批量删除执行记录失败",
|
||||
"managementTitle": "MCP 管理",
|
||||
"addExternal": "添加外部MCP",
|
||||
"toolConfig": "MCP 工具配置",
|
||||
"saveToolConfig": "保存工具配置",
|
||||
"externalConfig": "外部 MCP 配置",
|
||||
"loadingTools": "正在加载工具列表...",
|
||||
"loadToolsTimeout": "加载工具列表超时,可能是外部MCP连接较慢。请点击\"刷新\"按钮重试,或检查外部MCP连接状态。",
|
||||
"loadToolsFailed": "加载工具列表失败",
|
||||
"noTools": "暂无工具",
|
||||
"externalBadge": "外部",
|
||||
"externalFrom": "外部 ({{name}})",
|
||||
"externalToolFrom": "外部MCP工具 - 来源:{{name}}",
|
||||
"noDescription": "无描述",
|
||||
"paginationInfo": "显示 {{start}}-{{end}} / 共 {{total}} 个工具",
|
||||
"perPage": "每页:",
|
||||
"firstPage": "首页",
|
||||
"prevPage": "上一页",
|
||||
"nextPage": "下一页",
|
||||
"lastPage": "末页",
|
||||
"pageInfo": "第 {{page}} / {{total}} 页",
|
||||
"currentPageEnabled": "当前页已启用",
|
||||
"totalEnabled": "总计已启用",
|
||||
"toolsConfigSaved": "工具配置已成功保存!",
|
||||
"saveToolsConfigFailed": "保存工具配置失败",
|
||||
"getConfigFailed": "获取配置失败",
|
||||
"noExternalMCP": "暂无外部MCP配置",
|
||||
"clickToAddExternal": "点击\"添加外部MCP\"按钮开始配置",
|
||||
"connected": "已连接",
|
||||
"connecting": "连接中...",
|
||||
"connectionFailed": "连接失败",
|
||||
"disabled": "已禁用",
|
||||
"disconnected": "未连接",
|
||||
"stopConnection": "停止连接",
|
||||
"startConnection": "启动连接",
|
||||
"stop": "停止",
|
||||
"start": "启动",
|
||||
"editConfig": "编辑配置",
|
||||
"deleteConfig": "删除配置",
|
||||
"transportMode": "传输模式",
|
||||
"toolCount": "工具数量",
|
||||
"description": "描述",
|
||||
"timeout": "超时时间",
|
||||
"command": "命令",
|
||||
"addExternalMCP": "添加外部MCP",
|
||||
"editExternalMCP": "编辑外部MCP",
|
||||
"jsonEmpty": "JSON不能为空",
|
||||
"jsonError": "JSON格式错误",
|
||||
"configMustBeObject": "配置错误: 必须是JSON对象格式,key为配置名称,value为配置内容",
|
||||
"configNeedOne": "配置错误: 至少需要一个配置项",
|
||||
"configNameEmpty": "配置错误: 配置名称不能为空",
|
||||
"configMustBeObj": "配置错误: \"{{name}}\" 的配置必须是对象",
|
||||
"configNeedCommand": "配置错误: \"{{name}}\" 需要指定command(stdio模式)或url(http/sse模式)",
|
||||
"configStdioNeedCommand": "配置错误: \"{{name}}\" stdio模式需要command字段",
|
||||
"configHttpNeedUrl": "配置错误: \"{{name}}\" http模式需要url字段",
|
||||
"configSseNeedUrl": "配置错误: \"{{name}}\" sse模式需要url字段",
|
||||
"saveSuccess": "保存成功",
|
||||
"deleteSuccess": "删除成功",
|
||||
"deleteExternalConfirm": "确定要删除外部MCP \"{{name}}\" 吗?",
|
||||
"operationFailed": "操作失败",
|
||||
"connectionFailedCheck": "连接失败,请检查配置和网络连接",
|
||||
"connectionTimeout": "连接超时,请检查配置和网络连接",
|
||||
"totalCount": "总数",
|
||||
"enabledCount": "已启用",
|
||||
"disabledCount": "已停用",
|
||||
"connectedCount": "已连接"
|
||||
},
|
||||
"settings": {
|
||||
"title": "系统设置",
|
||||
"nav": {
|
||||
"basic": "基本设置",
|
||||
"robots": "机器人设置",
|
||||
"terminal": "终端",
|
||||
"security": "安全设置"
|
||||
},
|
||||
"robots": {
|
||||
"title": "机器人设置",
|
||||
"description": "配置企业微信、钉钉、飞书等机器人,在手机端直接与 CyberStrikeAI 对话,无需在服务器上打开网页。",
|
||||
"wecom": {
|
||||
"title": "企业微信",
|
||||
"enabled": "启用企业微信机器人"
|
||||
},
|
||||
"dingtalk": {
|
||||
"title": "钉钉",
|
||||
"enabled": "启用钉钉机器人"
|
||||
},
|
||||
"lark": {
|
||||
"title": "飞书 (Lark)",
|
||||
"enabled": "启用飞书机器人"
|
||||
}
|
||||
},
|
||||
"apply": {
|
||||
"button": "应用配置",
|
||||
"loadFailed": "加载配置失败",
|
||||
"fillRequired": "请填写所有必填字段(标记为 * 的字段)",
|
||||
"applyFailed": "应用配置失败",
|
||||
"applySuccess": "配置已成功应用!"
|
||||
},
|
||||
"security": {
|
||||
"changePassword": "修改密码",
|
||||
"fillPasswordHint": "请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。",
|
||||
"changePasswordFailed": "修改密码失败",
|
||||
"passwordUpdated": "密码已更新,请使用新密码重新登录。"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"sessionExpired": "认证已过期,请重新登录",
|
||||
"unauthorized": "未授权访问",
|
||||
"enterPassword": "请输入密码",
|
||||
"loginFailedCheck": "登录失败,请检查密码",
|
||||
"loginFailedRetry": "登录失败,请稍后重试",
|
||||
"loggedOut": "已退出登录"
|
||||
},
|
||||
"knowledge": {
|
||||
"title": "知识管理",
|
||||
"retrievalLogs": "检索历史",
|
||||
"totalItems": "总知识项",
|
||||
"categories": "分类数",
|
||||
"addKnowledge": "添加知识",
|
||||
"rebuildIndex": "重建索引",
|
||||
"rebuildIndexConfirm": "确定要重建索引吗?",
|
||||
"deleteItemConfirm": "确定要删除这个知识项吗?",
|
||||
"notEnabledTitle": "知识库功能未启用",
|
||||
"notEnabledHint": "请前往系统设置启用知识检索功能",
|
||||
"goToSettings": "前往设置"
|
||||
},
|
||||
"roles": {
|
||||
"title": "角色管理",
|
||||
"createRole": "创建角色",
|
||||
"searchPlaceholder": "搜索角色...",
|
||||
"deleteConfirm": "确定要删除角色..."
|
||||
},
|
||||
"skills": {
|
||||
"title": "Skills管理",
|
||||
"monitorTitle": "Skills状态监控",
|
||||
"createSkill": "创建Skill",
|
||||
"callStats": "调用统计",
|
||||
"addSkill": "添加Skill",
|
||||
"editSkill": "编辑Skill",
|
||||
"loadListFailed": "加载skills列表失败",
|
||||
"noSkills": "暂无skills,点击\"创建Skill\"创建第一个skill",
|
||||
"noMatch": "没有找到匹配的skills",
|
||||
"searchFailed": "搜索失败",
|
||||
"refreshed": "已刷新",
|
||||
"loadDetailFailed": "加载skill详情失败",
|
||||
"viewFailed": "查看skill失败",
|
||||
"saving": "保存中...",
|
||||
"saveFailed": "保存skill失败",
|
||||
"deleteFailed": "删除skill失败",
|
||||
"loadStatsFailed": "加载skills监控数据失败",
|
||||
"clearStatsConfirm": "确定要清空所有Skills统计数据吗?此操作不可恢复。",
|
||||
"statsCleared": "已清空所有Skills统计数据",
|
||||
"clearStatsFailed": "清空统计数据失败"
|
||||
},
|
||||
"apiDocs": {
|
||||
"curlCopied": "curl命令已复制到剪贴板!"
|
||||
},
|
||||
"chatGroup": {
|
||||
"search": "搜索",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"clearSearch": "清除搜索",
|
||||
"searchInGroupPlaceholder": "搜索分组中的对话...",
|
||||
"attackChain": "攻击链",
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"selectRole": "选择角色",
|
||||
"close": "关闭",
|
||||
"selectFile": "选择文件",
|
||||
"uploadFile": "上传文件(可多选或拖拽到此处)",
|
||||
"send": "发送",
|
||||
"rolePanelTitle": "选择角色",
|
||||
"copyMessage": "复制消息内容",
|
||||
"remove": "移除"
|
||||
},
|
||||
"mcpMonitor": {
|
||||
"deselectAll": "取消全选",
|
||||
"statusPending": "等待中",
|
||||
"statusCompleted": "已完成",
|
||||
"statusRunning": "执行中",
|
||||
"statusFailed": "失败",
|
||||
"loading": "加载中...",
|
||||
"noStatsData": "暂无统计数据",
|
||||
"noExecutions": "暂无执行记录",
|
||||
"noRecordsWithFilter": "当前筛选条件下暂无记录",
|
||||
"paginationInfo": "显示 {{start}}-{{end}} / 共 {{total}} 条记录",
|
||||
"perPageLabel": "每页显示",
|
||||
"loadStatsError": "无法加载统计信息",
|
||||
"loadExecutionsError": "无法加载执行记录",
|
||||
"totalCalls": "总调用次数",
|
||||
"successFailed": "成功 {{success}} / 失败 {{failed}}",
|
||||
"successRate": "成功率",
|
||||
"statsFromAllTools": "统计自全部工具调用",
|
||||
"lastCall": "最近一次调用",
|
||||
"lastRefreshTime": "最后刷新时间",
|
||||
"noCallsYet": "暂无调用",
|
||||
"unknownTool": "未知工具",
|
||||
"successFailedRate": "成功 {{success}} / 失败 {{failed}} · 成功率 {{rate}}%",
|
||||
"columnTool": "工具",
|
||||
"columnStatus": "状态",
|
||||
"columnStartTime": "开始时间",
|
||||
"columnDuration": "耗时",
|
||||
"columnActions": "操作",
|
||||
"viewDetail": "查看详情",
|
||||
"delete": "删除",
|
||||
"deleteExecTitle": "删除此执行记录",
|
||||
"deleteExecConfirmSingle": "确定要删除此执行记录吗?此操作不可恢复。",
|
||||
"deleteExecFailed": "删除执行记录失败",
|
||||
"execDeleted": "执行记录已删除",
|
||||
"selectExecFirst": "请先选择要删除的执行记录",
|
||||
"batchDeleteConfirm": "确定要删除选中的 {{count}} 条执行记录吗?此操作不可恢复。",
|
||||
"batchDeleteSuccess": "成功删除 {{count}} 条执行记录",
|
||||
"unknown": "未知",
|
||||
"durationSeconds": "{{n}} 秒",
|
||||
"durationMinutes": "{{minutes}} 分 {{seconds}} 秒",
|
||||
"durationMinutesOnly": "{{minutes}} 分",
|
||||
"durationHours": "{{hours}} 小时 {{minutes}} 分",
|
||||
"durationHoursOnly": "{{hours}} 小时"
|
||||
},
|
||||
"knowledgePage": {
|
||||
"totalContent": "总内容",
|
||||
"categoryFilter": "分类筛选",
|
||||
"all": "全部",
|
||||
"searchPlaceholder": "搜索知识...",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"retrievalLogs": {
|
||||
"totalRetrievals": "总检索次数",
|
||||
"successRetrievals": "成功检索",
|
||||
"successRate": "成功率",
|
||||
"retrievedItems": "检索到知识项",
|
||||
"conversationId": "对话ID",
|
||||
"messageId": "消息ID",
|
||||
"filter": "筛选",
|
||||
"optionalConversation": "可选:筛选特定对话",
|
||||
"optionalMessage": "可选:筛选特定消息",
|
||||
"loading": "加载中...",
|
||||
"noRecords": "暂无检索记录",
|
||||
"noQuery": "无查询内容",
|
||||
"itemsUnit": "项",
|
||||
"hasResults": "有结果",
|
||||
"noResults": "无结果",
|
||||
"clickToCopy": "点击复制",
|
||||
"retrievalResult": "检索结果",
|
||||
"foundCount": "找到 {{count}} 个相关知识项",
|
||||
"foundUnknown": "找到相关知识项(数量未知)",
|
||||
"noMatch": "未找到匹配的知识项",
|
||||
"retrievedItemsLabel": "检索到的知识项:",
|
||||
"viewDetails": "查看详情",
|
||||
"loadError": "加载检索日志失败",
|
||||
"detailError": "无法获取检索详情",
|
||||
"deleteError": "删除检索日志失败",
|
||||
"detailsTitle": "检索详情",
|
||||
"queryInfo": "查询信息",
|
||||
"queryContent": "查询内容:",
|
||||
"retrievalInfo": "检索信息",
|
||||
"riskType": "风险类型",
|
||||
"retrievalTime": "检索时间",
|
||||
"noItemDetails": "未找到知识项详情",
|
||||
"noContentPreview": "无内容预览",
|
||||
"untitled": "未命名",
|
||||
"uncategorized": "未分类",
|
||||
"relatedInfo": "关联信息",
|
||||
"itemsCount": "{{count}} 个知识项",
|
||||
"deleteConfirm": "确定要删除这条检索记录吗?"
|
||||
},
|
||||
"infoCollectPage": {
|
||||
"title": "信息收集",
|
||||
"reset": "重置",
|
||||
"confirm": "确定",
|
||||
"fofaQuerySyntax": "FOFA 查询语法",
|
||||
"naturalLanguage": "自然语言(AI 解析为 FOFA 语法)",
|
||||
"returnCount": "返回数量",
|
||||
"pageNum": "页码",
|
||||
"returnFields": "返回字段名(逗号分隔)",
|
||||
"queryResults": "查询结果",
|
||||
"selectedRows": "已选择 {{count}} 条",
|
||||
"selectedRowsZero": "已选择 0 条",
|
||||
"columns": "列",
|
||||
"exportCsv": "导出 CSV",
|
||||
"exportJson": "导出 JSON",
|
||||
"exportXlsx": "导出 XLSX",
|
||||
"batchScan": "批量扫描",
|
||||
"showColumns": "显示字段",
|
||||
"columnsPanelAll": "全选",
|
||||
"columnsPanelNone": "全不选",
|
||||
"columnsPanelClose": "关闭",
|
||||
"formHint": "查询语法参考 FOFA 文档,支持 && / || / () 等。",
|
||||
"parseBtn": "AI 解析",
|
||||
"parseHint": "解析后会弹窗展示 FOFA 语法(可编辑),确认无误后再填入查询框并执行查询。",
|
||||
"minFields": "最小字段",
|
||||
"webCommon": "Web 常用",
|
||||
"intelEnhanced": "情报增强",
|
||||
"presetApache": "Apache + 中国",
|
||||
"presetLogin": "登录页 + 中国",
|
||||
"presetDomain": "指定域名",
|
||||
"presetIp": "指定 IP",
|
||||
"nlPlaceholder": "例如:找美国 Missouri 的 Apache 站点,标题包含 Home",
|
||||
"showHideColumns": "显示/隐藏字段",
|
||||
"exportCsvTitle": "导出当前结果为 CSV(UTF-8,兼容中文)",
|
||||
"exportJsonTitle": "导出当前结果为 JSON",
|
||||
"exportXlsxTitle": "导出当前结果为 Excel",
|
||||
"batchScanTitle": "将所选行创建为批量任务队列"
|
||||
},
|
||||
"vulnerabilityPage": {
|
||||
"statTotal": "总漏洞数",
|
||||
"filter": "筛选",
|
||||
"clear": "清除",
|
||||
"vulnId": "漏洞ID",
|
||||
"conversationId": "会话ID",
|
||||
"severity": "严重程度",
|
||||
"status": "状态",
|
||||
"statusOpen": "待处理",
|
||||
"statusConfirmed": "已确认",
|
||||
"statusFixed": "已修复",
|
||||
"statusFalsePositive": "误报",
|
||||
"searchVulnId": "搜索漏洞ID",
|
||||
"filterConversation": "筛选特定会话",
|
||||
"loading": "加载中...",
|
||||
"noRecords": "暂无漏洞记录"
|
||||
},
|
||||
"tasksPage": {
|
||||
"statusFilter": "状态筛选",
|
||||
"statusPending": "待执行",
|
||||
"statusPaused": "已暂停",
|
||||
"statusCancelled": "已取消",
|
||||
"searchQueuePlaceholder": "搜索队列ID、标题或创建时间",
|
||||
"searchKeywordPlaceholder": "输入关键字搜索..."
|
||||
},
|
||||
"skillsPage": {
|
||||
"clearStats": "清空统计",
|
||||
"clearStatsTitle": "清空所有统计数据",
|
||||
"skillsCallStats": "Skills调用统计",
|
||||
"searchPlaceholder": "搜索Skills...",
|
||||
"loading": "加载中..."
|
||||
},
|
||||
"settingsBasic": {
|
||||
"basicTitle": "基本设置",
|
||||
"openaiConfig": "OpenAI 配置",
|
||||
"fofaConfig": "FOFA 配置",
|
||||
"agentConfig": "Agent 配置",
|
||||
"knowledgeConfig": "知识库配置",
|
||||
"baseUrl": "Base URL",
|
||||
"apiKey": "API Key",
|
||||
"model": "模型",
|
||||
"openaiBaseUrlPlaceholder": "https://api.openai.com/v1",
|
||||
"openaiApiKeyPlaceholder": "输入OpenAI API Key",
|
||||
"modelPlaceholder": "gpt-4",
|
||||
"fofaBaseUrlPlaceholder": "https://fofa.info/api/v1/search/all(可选)",
|
||||
"fofaBaseUrlHint": "留空则使用默认地址。",
|
||||
"email": "Email",
|
||||
"fofaEmailPlaceholder": "输入 FOFA 账号邮箱",
|
||||
"fofaApiKeyPlaceholder": "输入 FOFA API Key",
|
||||
"fofaApiKeyHint": "仅保存在服务器配置中(`config.yaml`)。",
|
||||
"maxIterations": "最大迭代次数",
|
||||
"iterationsPlaceholder": "30",
|
||||
"enableKnowledge": "启用知识检索功能",
|
||||
"knowledgeBasePath": "知识库路径",
|
||||
"knowledgeBasePathPlaceholder": "knowledge_base",
|
||||
"knowledgeBasePathHint": "相对于配置文件所在目录的路径",
|
||||
"embeddingConfig": "嵌入模型配置",
|
||||
"provider": "提供商",
|
||||
"embeddingBaseUrlPlaceholder": "留空则使用OpenAI配置的base_url",
|
||||
"embeddingApiKeyPlaceholder": "留空则使用OpenAI配置的api_key",
|
||||
"modelName": "模型名称",
|
||||
"embeddingModelPlaceholder": "text-embedding-v4",
|
||||
"retrievalConfig": "检索配置",
|
||||
"topK": "Top-K 结果数量",
|
||||
"topKPlaceholder": "5",
|
||||
"topKHint": "检索返回的Top-K结果数量",
|
||||
"similarityThreshold": "相似度阈值",
|
||||
"similarityPlaceholder": "0.7",
|
||||
"similarityHint": "相似度阈值(0-1),低于此值的结果将被过滤",
|
||||
"hybridWeight": "混合检索权重",
|
||||
"hybridPlaceholder": "0.7",
|
||||
"hybridHint": "向量检索的权重(0-1),1.0表示纯向量检索,0.0表示纯关键词检索",
|
||||
"indexConfig": "索引配置",
|
||||
"chunkSize": "分块大小(Chunk Size)",
|
||||
"chunkSizePlaceholder": "512",
|
||||
"chunkSizeHint": "每个块的最大 token 数(默认 512),长文本会被分割成多个块",
|
||||
"chunkOverlap": "分块重叠(Chunk Overlap)",
|
||||
"chunkOverlapPlaceholder": "50",
|
||||
"chunkOverlapHint": "块之间的重叠 token 数(默认 50),保持上下文连贯性",
|
||||
"maxChunksPerItem": "单个知识项最大块数",
|
||||
"maxChunksPlaceholder": "0",
|
||||
"maxChunksHint": "单个知识项的最大块数量(0 表示不限制),防止单个文件消耗过多 API 配额",
|
||||
"maxRpm": "每分钟最大请求数(Max RPM)",
|
||||
"maxRpmPlaceholder": "0",
|
||||
"maxRpmHint": "每分钟最大请求数(默认 0 表示不限制),如 OpenAI 默认 200 RPM",
|
||||
"rateLimitDelay": "请求间隔延迟(毫秒)",
|
||||
"rateLimitPlaceholder": "300",
|
||||
"rateLimitHint": "请求间隔毫秒数(默认 300),用于避免 API 速率限制,设为 0 不限制",
|
||||
"maxRetries": "最大重试次数",
|
||||
"maxRetriesPlaceholder": "3",
|
||||
"maxRetriesHint": "最大重试次数(默认 3),遇到速率限制或服务器错误时自动重试",
|
||||
"retryDelay": "重试间隔(毫秒)",
|
||||
"retryDelayPlaceholder": "1000",
|
||||
"retryDelayHint": "重试间隔毫秒数(默认 1000),每次重试会递增延迟"
|
||||
},
|
||||
"settingsTerminal": {
|
||||
"title": "终端",
|
||||
"description": "在服务器上执行命令,便于运维与调试。命令在服务端执行,请勿执行敏感或破坏性操作。",
|
||||
"terminalTab": "终端 {{n}}",
|
||||
"close": "关闭",
|
||||
"newTerminal": "新终端"
|
||||
},
|
||||
"settingsSecurity": {
|
||||
"changePasswordTitle": "修改密码",
|
||||
"changePasswordDesc": "修改登录密码后,需要使用新密码重新登录。",
|
||||
"currentPassword": "当前密码",
|
||||
"currentPasswordPlaceholder": "输入当前登录密码",
|
||||
"newPassword": "新密码",
|
||||
"newPasswordPlaceholder": "设置新密码(至少 8 位)",
|
||||
"confirmPassword": "确认新密码",
|
||||
"confirmPasswordPlaceholder": "再次输入新密码",
|
||||
"clear": "清空",
|
||||
"changePasswordBtn": "修改密码"
|
||||
},
|
||||
"settingsRobotsExtra": {
|
||||
"botCommandsTitle": "机器人命令说明",
|
||||
"botCommandsDesc": "在对话中可发送以下命令(支持中英文):"
|
||||
},
|
||||
"mcpDetailModal": {
|
||||
"title": "工具调用详情",
|
||||
"execInfo": "执行信息",
|
||||
"tool": "工具",
|
||||
"status": "状态",
|
||||
"time": "时间",
|
||||
"executionId": "执行 ID",
|
||||
"requestParams": "请求参数",
|
||||
"copyJson": "复制 JSON",
|
||||
"responseResult": "响应结果",
|
||||
"copyContent": "复制内容",
|
||||
"correctInfo": "正确信息",
|
||||
"errorInfo": "错误信息",
|
||||
"copyError": "复制错误"
|
||||
},
|
||||
"attackChainModal": {
|
||||
"title": "攻击链可视化",
|
||||
"regenerate": "重新生成",
|
||||
"regenerateTitle": "重新生成攻击链(包含最新对话内容)",
|
||||
"exportPng": "导出为PNG",
|
||||
"exportSvg": "导出为SVG",
|
||||
"refreshTitle": "刷新当前攻击链(不重新生成)",
|
||||
"nodesEdges": "节点: {{nodes}} | 边: {{edges}}",
|
||||
"searchPlaceholder": "搜索节点...",
|
||||
"allTypes": "所有类型",
|
||||
"target": "目标",
|
||||
"action": "行动",
|
||||
"vulnerability": "漏洞",
|
||||
"allRisks": "所有风险",
|
||||
"highRisk": "高风险 (80-100)",
|
||||
"mediumHighRisk": "中高风险 (60-79)",
|
||||
"mediumRisk": "中风险 (40-59)",
|
||||
"lowRisk": "低风险 (0-39)",
|
||||
"resetFilter": "重置筛选",
|
||||
"loading": "加载中...",
|
||||
"riskLevel": "风险等级",
|
||||
"lineMeaning": "连接线含义",
|
||||
"blueLine": "蓝色线:行动发现漏洞",
|
||||
"redLine": "红色线:使能/促成关系",
|
||||
"grayLine": "灰色线:逻辑顺序",
|
||||
"nodeDetails": "节点详情",
|
||||
"closeDetails": "关闭详情"
|
||||
},
|
||||
"externalMcpModal": {
|
||||
"configJson": "配置JSON",
|
||||
"formatLabel": "配置格式:",
|
||||
"formatDesc": "JSON对象,key为配置名称,value为配置内容。状态通过\"启动/停止\"按钮控制,无需在JSON中配置。",
|
||||
"formatJson": "格式化JSON",
|
||||
"loadExample": "加载示例"
|
||||
},
|
||||
"skillModal": {
|
||||
"addSkill": "添加Skill",
|
||||
"editSkill": "编辑Skill",
|
||||
"skillName": "Skill名称",
|
||||
"skillNamePlaceholder": "例如: sql-injection-testing",
|
||||
"skillNameHint": "只能包含字母、数字、连字符和下划线",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "Skill的简短描述",
|
||||
"contentLabel": "内容(Markdown格式)",
|
||||
"contentPlaceholder": "输入skill内容,支持Markdown格式...",
|
||||
"contentHint": "支持YAML front matter格式(可选)"
|
||||
},
|
||||
"knowledgeItemModal": {
|
||||
"addKnowledge": "添加知识",
|
||||
"editKnowledge": "编辑知识",
|
||||
"category": "分类(风险类型)",
|
||||
"categoryPlaceholder": "例如:SQL注入",
|
||||
"title": "标题",
|
||||
"titlePlaceholder": "知识项标题",
|
||||
"contentLabel": "内容(Markdown格式)",
|
||||
"contentPlaceholder": "输入知识内容,支持Markdown格式..."
|
||||
},
|
||||
"batchManageModal": {
|
||||
"title": "管理对话记录·共{{count}}条",
|
||||
"searchPlaceholder": "搜索历史记录",
|
||||
"conversationName": "对话名称",
|
||||
"lastTime": "最近一次对话时间",
|
||||
"action": "操作",
|
||||
"selectAll": "全选",
|
||||
"deleteSelected": "删除所选",
|
||||
"confirmDeleteNone": "请先选择要删除的对话",
|
||||
"confirmDeleteN": "确定要删除选中的 {{count}} 条对话吗?",
|
||||
"deleteFailed": "删除失败",
|
||||
"unnamedConversation": "未命名对话"
|
||||
},
|
||||
"createGroupModal": {
|
||||
"title": "创建分组",
|
||||
"description": "分组功能可将对话集中归类管理,让对话更加井然有序。",
|
||||
"selectIcon": "点击选择图标",
|
||||
"groupNamePlaceholder": "请输入分组名称",
|
||||
"pickIcon": "选择图标",
|
||||
"customIcon": "自定义",
|
||||
"confirmIcon": "确定",
|
||||
"create": "创建",
|
||||
"cancel": "取消",
|
||||
"suggestionPenetrationTest": "渗透测试",
|
||||
"suggestionCtf": "CTF",
|
||||
"suggestionRedTeam": "红队",
|
||||
"suggestionVulnerabilityMining": "漏洞挖掘",
|
||||
"nameExists": "分组名称已存在,请使用其他名称",
|
||||
"createFailed": "创建失败",
|
||||
"unknownError": "未知错误"
|
||||
},
|
||||
"contextMenu": {
|
||||
"viewAttackChain": "查看攻击链",
|
||||
"rename": "重命名",
|
||||
"pinConversation": "置顶此对话",
|
||||
"unpinConversation": "取消置顶",
|
||||
"batchManage": "批量管理",
|
||||
"moveToGroup": "移动到分组",
|
||||
"deleteConversation": "删除此对话",
|
||||
"pinGroup": "置顶此分组",
|
||||
"unpinGroup": "取消置顶",
|
||||
"deleteGroup": "删除此分组"
|
||||
},
|
||||
"batchImportModal": {
|
||||
"title": "新建任务",
|
||||
"queueTitle": "任务标题",
|
||||
"queueTitlePlaceholder": "请输入任务标题(可选,用于标识和筛选)",
|
||||
"queueTitleHint": "为批量任务队列设置一个标题,方便后续查找和管理。",
|
||||
"role": "角色",
|
||||
"defaultRole": "默认",
|
||||
"roleHint": "选择一个角色,所有任务将使用该角色的配置(提示词和工具)执行。",
|
||||
"tasksList": "任务列表(每行一个任务)",
|
||||
"tasksListPlaceholder": "请输入任务列表,每行一个任务",
|
||||
"tasksListPlaceholderExample": "请输入任务列表,每行一个任务,例如:\n扫描 192.168.1.1 的开放端口\n检查 https://example.com 是否存在SQL注入\n枚举 example.com 的子域名",
|
||||
"tasksListHint": "每行输入一个任务指令,系统将依次执行这些任务。空行会被自动忽略。",
|
||||
"tasksListHintFull": "提示:每行输入一个任务指令,系统将依次执行这些任务。空行会被自动忽略。",
|
||||
"createQueue": "创建队列"
|
||||
},
|
||||
"batchQueueDetailModal": {
|
||||
"title": "批量任务队列详情",
|
||||
"addTask": "添加任务",
|
||||
"startExecute": "开始执行",
|
||||
"pauseQueue": "暂停队列",
|
||||
"deleteQueue": "删除队列",
|
||||
"queueTitle": "任务标题",
|
||||
"role": "角色",
|
||||
"defaultRole": "默认",
|
||||
"queueId": "队列ID",
|
||||
"status": "状态",
|
||||
"createdAt": "创建时间",
|
||||
"startedAt": "开始时间",
|
||||
"completedAt": "完成时间",
|
||||
"taskTotal": "任务总数",
|
||||
"taskList": "任务列表",
|
||||
"startLabel": "开始",
|
||||
"completeLabel": "完成",
|
||||
"errorLabel": "错误",
|
||||
"resultLabel": "结果"
|
||||
},
|
||||
"editBatchTaskModal": {
|
||||
"title": "编辑任务",
|
||||
"taskMessage": "任务消息",
|
||||
"taskMessagePlaceholder": "请输入任务消息"
|
||||
},
|
||||
"addBatchTaskModal": {
|
||||
"title": "添加任务",
|
||||
"taskMessage": "任务消息",
|
||||
"taskMessagePlaceholder": "请输入任务消息",
|
||||
"add": "添加"
|
||||
},
|
||||
"vulnerabilityModal": {
|
||||
"conversationId": "会话ID",
|
||||
"conversationIdPlaceholder": "输入会话ID",
|
||||
"title": "标题",
|
||||
"titlePlaceholder": "漏洞标题",
|
||||
"description": "描述",
|
||||
"descriptionPlaceholder": "漏洞详细描述",
|
||||
"severity": "严重程度",
|
||||
"pleaseSelect": "请选择",
|
||||
"severityCritical": "严重",
|
||||
"severityHigh": "高危",
|
||||
"severityMedium": "中危",
|
||||
"severityLow": "低危",
|
||||
"severityInfo": "信息",
|
||||
"status": "状态",
|
||||
"statusOpen": "待处理",
|
||||
"statusConfirmed": "已确认",
|
||||
"statusFixed": "已修复",
|
||||
"statusFalsePositive": "误报",
|
||||
"type": "漏洞类型",
|
||||
"typePlaceholder": "如:SQL注入、XSS、CSRF等",
|
||||
"target": "目标",
|
||||
"targetPlaceholder": "受影响的目标(URL、IP地址等)",
|
||||
"proof": "证明(POC)",
|
||||
"proofPlaceholder": "漏洞证明,如请求/响应、截图等",
|
||||
"impact": "影响",
|
||||
"impactPlaceholder": "漏洞影响说明",
|
||||
"recommendation": "修复建议",
|
||||
"recommendationPlaceholder": "修复建议"
|
||||
},
|
||||
"roleModal": {
|
||||
"addRole": "添加角色",
|
||||
"editRole": "编辑角色",
|
||||
"roleName": "角色名称",
|
||||
"roleNamePlaceholder": "输入角色名称",
|
||||
"roleDescription": "角色描述",
|
||||
"roleDescriptionPlaceholder": "输入角色描述",
|
||||
"roleIcon": "角色图标",
|
||||
"roleIconPlaceholder": "输入emoji图标,例如: 🏆",
|
||||
"roleIconHint": "输入一个emoji作为角色的图标,将显示在角色选择器中。",
|
||||
"userPrompt": "用户提示词",
|
||||
"userPromptPlaceholder": "输入用户提示词,会在用户消息前追加此提示词...",
|
||||
"userPromptHint": "此提示词会追加到用户消息前,用于指导AI的行为。注意:这不会修改系统提示词。",
|
||||
"relatedTools": "关联的工具(可选)",
|
||||
"defaultRoleToolsTitle": "默认角色使用所有工具",
|
||||
"defaultRoleToolsDesc": "默认角色会自动使用MCP管理中启用的所有工具,无需单独配置。",
|
||||
"searchToolsPlaceholder": "搜索工具...",
|
||||
"loadingTools": "正在加载工具列表...",
|
||||
"relatedToolsHint": "勾选要关联的工具,留空则使用MCP管理中的全部工具配置。",
|
||||
"relatedSkills": "关联的Skills(可选)",
|
||||
"searchSkillsPlaceholder": "搜索skill...",
|
||||
"loadingSkills": "正在加载skills列表...",
|
||||
"relatedSkillsHint": "勾选要关联的skills,这些skills的内容会在执行任务前注入到系统提示词中,帮助AI更好地理解相关专业知识。",
|
||||
"enableRole": "启用此角色"
|
||||
}
|
||||
}
|
||||
+27
-7
@@ -123,12 +123,20 @@ async function ensureAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
|
||||
function handleUnauthorized({ message = '认证已过期,请重新登录', silent = false } = {}) {
|
||||
function handleUnauthorized({ message = null, silent = false } = {}) {
|
||||
clearAuthStorage();
|
||||
authPromise = null;
|
||||
authPromiseResolvers = [];
|
||||
let finalMessage = message;
|
||||
if (!finalMessage) {
|
||||
if (typeof window !== 'undefined' && typeof window.t === 'function') {
|
||||
finalMessage = window.t('auth.sessionExpired');
|
||||
} else {
|
||||
finalMessage = '认证已过期,请重新登录';
|
||||
}
|
||||
}
|
||||
if (!silent) {
|
||||
showLoginOverlay(message);
|
||||
showLoginOverlay(finalMessage);
|
||||
} else {
|
||||
showLoginOverlay();
|
||||
}
|
||||
@@ -147,7 +155,10 @@ async function apiFetch(url, options = {}) {
|
||||
const response = await fetch(url, opts);
|
||||
if (response.status === 401) {
|
||||
handleUnauthorized();
|
||||
throw new Error('未授权访问');
|
||||
const msg = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('auth.unauthorized')
|
||||
: '未授权访问';
|
||||
throw new Error(msg);
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -165,7 +176,10 @@ async function submitLogin(event) {
|
||||
const password = passwordInput.value.trim();
|
||||
if (!password) {
|
||||
if (errorBox) {
|
||||
errorBox.textContent = '请输入密码';
|
||||
const msgEmpty = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('auth.enterPassword')
|
||||
: '请输入密码';
|
||||
errorBox.textContent = msgEmpty;
|
||||
errorBox.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
@@ -186,7 +200,10 @@ async function submitLogin(event) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
if (!response.ok || !result.token) {
|
||||
if (errorBox) {
|
||||
errorBox.textContent = result.error || '登录失败,请检查密码';
|
||||
const fallback = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('auth.loginFailedCheck')
|
||||
: '登录失败,请检查密码';
|
||||
errorBox.textContent = result.error || fallback;
|
||||
errorBox.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
@@ -203,7 +220,10 @@ async function submitLogin(event) {
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error);
|
||||
if (errorBox) {
|
||||
errorBox.textContent = '登录失败,请稍后重试';
|
||||
const fallback = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('auth.loginFailedRetry')
|
||||
: '登录失败,请稍后重试';
|
||||
errorBox.textContent = fallback;
|
||||
errorBox.style.display = 'block';
|
||||
}
|
||||
} finally {
|
||||
@@ -375,7 +395,7 @@ async function logout() {
|
||||
// 无论如何都清除本地认证信息
|
||||
clearAuthStorage();
|
||||
hideLoginOverlay();
|
||||
showLoginOverlay('已退出登录');
|
||||
showLoginOverlay(typeof window.t === 'function' ? window.t('auth.loggedOut') : '已退出登录');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+145
-66
@@ -44,10 +44,15 @@ function saveChatDraftDebounced(content) {
|
||||
// 保存输入框草稿到localStorage
|
||||
function saveChatDraft(content) {
|
||||
try {
|
||||
if (content && content.trim().length > 0) {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
const placeholderText = chatInput ? (chatInput.getAttribute('placeholder') || '').trim() : '';
|
||||
const trimmed = (content || '').trim();
|
||||
|
||||
// 不要把占位提示本身当作草稿保存
|
||||
if (trimmed && (!placeholderText || trimmed !== placeholderText)) {
|
||||
localStorage.setItem(DRAFT_STORAGE_KEY, content);
|
||||
} else {
|
||||
// 如果内容为空,清除保存的草稿
|
||||
// 如果内容为空或等于占位提示,清除保存的草稿
|
||||
localStorage.removeItem(DRAFT_STORAGE_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -63,17 +68,27 @@ function restoreChatDraft() {
|
||||
if (!chatInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const placeholderText = (chatInput.getAttribute('placeholder') || '').trim();
|
||||
// 若当前 value 与 placeholder 相同,说明提示被误当作内容,清空以便正确显示占位符
|
||||
if (placeholderText && chatInput.value.trim() === placeholderText) {
|
||||
chatInput.value = '';
|
||||
}
|
||||
// 如果输入框已有内容,不恢复草稿(避免覆盖用户输入)
|
||||
if (chatInput.value && chatInput.value.trim().length > 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const draft = localStorage.getItem(DRAFT_STORAGE_KEY);
|
||||
if (draft && draft.trim().length > 0) {
|
||||
const trimmedDraft = draft ? draft.trim() : '';
|
||||
|
||||
// 如果草稿内容和占位提示一样,则认为是无效草稿,不恢复
|
||||
if (trimmedDraft && (!placeholderText || trimmedDraft !== placeholderText)) {
|
||||
chatInput.value = draft;
|
||||
// 调整输入框高度以适应内容
|
||||
adjustTextareaHeight(chatInput);
|
||||
} else if (trimmedDraft && placeholderText && trimmedDraft === placeholderText) {
|
||||
// 清理掉无效草稿,避免之后继续干扰
|
||||
localStorage.removeItem(DRAFT_STORAGE_KEY);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('恢复草稿失败:', error);
|
||||
@@ -263,7 +278,7 @@ function renderChatFileChips() {
|
||||
const remove = document.createElement('button');
|
||||
remove.type = 'button';
|
||||
remove.className = 'chat-file-chip-remove';
|
||||
remove.title = '移除';
|
||||
remove.title = typeof window.t === 'function' ? window.t('chatGroup.remove') : '移除';
|
||||
remove.innerHTML = '×';
|
||||
remove.setAttribute('aria-label', '移除 ' + a.fileName);
|
||||
remove.addEventListener('click', () => removeChatAttachment(i));
|
||||
@@ -720,14 +735,14 @@ function renderMentionSuggestions({ showLoading = false } = {}) {
|
||||
const previousScrollTop = canPreserveScroll ? existingList.scrollTop : 0;
|
||||
|
||||
if (showLoading) {
|
||||
mentionSuggestionsEl.innerHTML = '<div class="mention-empty">正在加载工具...</div>';
|
||||
mentionSuggestionsEl.innerHTML = '<div class="mention-empty">' + (typeof window.t === 'function' ? window.t('chat.loadingTools') : '正在加载工具...') + '</div>';
|
||||
mentionSuggestionsEl.style.display = 'block';
|
||||
delete mentionSuggestionsEl.dataset.lastMentionQuery;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mentionFilteredTools.length) {
|
||||
mentionSuggestionsEl.innerHTML = '<div class="mention-empty">没有匹配的工具</div>';
|
||||
mentionSuggestionsEl.innerHTML = '<div class="mention-empty">' + (typeof window.t === 'function' ? window.t('chat.noMatchTools') : '没有匹配的工具') + '</div>';
|
||||
mentionSuggestionsEl.style.display = 'block';
|
||||
mentionSuggestionsEl.dataset.lastMentionQuery = currentQuery;
|
||||
return;
|
||||
@@ -937,7 +952,8 @@ function initializeChatUI() {
|
||||
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
if (messagesDiv && messagesDiv.childElementCount === 0) {
|
||||
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
|
||||
const readyMsg = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsg);
|
||||
}
|
||||
|
||||
addAttackChainButton(currentConversationId);
|
||||
@@ -1040,12 +1056,23 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
}
|
||||
};
|
||||
|
||||
// 助手消息中的已知中文错误前缀做国际化替换(后端固定返回中文)
|
||||
let displayContent = content;
|
||||
if (role === 'assistant' && typeof displayContent === 'string' && typeof window.t === 'function') {
|
||||
if (displayContent.indexOf('执行失败: ') === 0) {
|
||||
displayContent = window.t('chat.executeFailed') + ': ' + displayContent.slice('执行失败: '.length);
|
||||
}
|
||||
if (displayContent.indexOf('调用OpenAI失败:') !== -1) {
|
||||
displayContent = displayContent.replace(/调用OpenAI失败:/g, window.t('chat.callOpenAIFailed') + ':');
|
||||
}
|
||||
}
|
||||
|
||||
// 对于用户消息,直接转义HTML,不进行Markdown解析,以保留所有特殊字符
|
||||
if (role === 'user') {
|
||||
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
} else if (typeof DOMPurify !== 'undefined') {
|
||||
// 直接解析Markdown(代码块会被包裹在<code>/<pre>中,DOMPurify会保留其文本内容)
|
||||
let parsedContent = parseMarkdown(content);
|
||||
let parsedContent = parseMarkdown(role === 'assistant' ? displayContent : content);
|
||||
if (!parsedContent) {
|
||||
parsedContent = content;
|
||||
}
|
||||
@@ -1087,14 +1114,16 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
|
||||
formattedContent = DOMPurify.sanitize(parsedContent, defaultSanitizeConfig);
|
||||
} else if (typeof marked !== 'undefined') {
|
||||
const parsedContent = parseMarkdown(content);
|
||||
const rawForParse = role === 'assistant' ? displayContent : content;
|
||||
const parsedContent = parseMarkdown(rawForParse);
|
||||
if (parsedContent) {
|
||||
formattedContent = parsedContent;
|
||||
} else {
|
||||
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
formattedContent = escapeHtml(rawForParse).replace(/\n/g, '<br>');
|
||||
}
|
||||
} else {
|
||||
formattedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||
const rawForEscape = role === 'assistant' ? displayContent : content;
|
||||
formattedContent = escapeHtml(rawForEscape).replace(/\n/g, '<br>');
|
||||
}
|
||||
|
||||
bubble.innerHTML = formattedContent;
|
||||
@@ -1129,8 +1158,8 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
if (role === 'assistant') {
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.className = 'message-copy-btn';
|
||||
copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>复制</span>';
|
||||
copyBtn.title = '复制消息内容';
|
||||
copyBtn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="9" y="9" width="13" height="13" rx="2" ry="2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>' + (typeof window.t === 'function' ? window.t('common.copy') : '复制') + '</span>';
|
||||
copyBtn.title = typeof window.t === 'function' ? window.t('chat.copyMessageTitle') : '复制消息内容';
|
||||
copyBtn.onclick = function(e) {
|
||||
e.stopPropagation();
|
||||
copyMessageToClipboard(messageDiv, this);
|
||||
@@ -1169,7 +1198,7 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
|
||||
const mcpLabel = document.createElement('div');
|
||||
mcpLabel.className = 'mcp-call-label';
|
||||
mcpLabel.textContent = '📋 渗透测试详情';
|
||||
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
mcpSection.appendChild(mcpLabel);
|
||||
|
||||
const buttonsContainer = document.createElement('div');
|
||||
@@ -1192,7 +1221,7 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr
|
||||
if (progressId) {
|
||||
const progressDetailBtn = document.createElement('button');
|
||||
progressDetailBtn.className = 'mcp-detail-btn process-detail-btn';
|
||||
progressDetailBtn.innerHTML = '<span>展开详情</span>';
|
||||
progressDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
|
||||
progressDetailBtn.onclick = () => toggleProcessDetails(progressId, messageDiv.id);
|
||||
buttonsContainer.appendChild(progressDetailBtn);
|
||||
// 存储进度ID到消息元素
|
||||
@@ -1259,7 +1288,7 @@ function copyMessageToClipboard(messageDiv, button) {
|
||||
function showCopySuccess(button) {
|
||||
if (button) {
|
||||
const originalText = button.innerHTML;
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>已复制</span>';
|
||||
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg><span>' + (typeof window.t === 'function' ? window.t('common.copied') : '已复制') + '</span>';
|
||||
button.style.color = '#10b981';
|
||||
button.style.background = 'rgba(16, 185, 129, 0.1)';
|
||||
button.style.borderColor = 'rgba(16, 185, 129, 0.3)';
|
||||
@@ -1301,11 +1330,11 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
if (!mcpLabel && !buttonsContainer) {
|
||||
mcpLabel = document.createElement('div');
|
||||
mcpLabel.className = 'mcp-call-label';
|
||||
mcpLabel.textContent = '📋 渗透测试详情';
|
||||
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
mcpSection.appendChild(mcpLabel);
|
||||
} else if (mcpLabel && mcpLabel.textContent !== '📋 渗透测试详情') {
|
||||
} else if (mcpLabel && mcpLabel.textContent !== ('📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情'))) {
|
||||
// 如果标签存在但不是统一格式,更新它
|
||||
mcpLabel.textContent = '📋 渗透测试详情';
|
||||
mcpLabel.textContent = '📋 ' + (typeof window.t === 'function' ? window.t('chat.penetrationTestDetail') : '渗透测试详情');
|
||||
}
|
||||
|
||||
// 如果没有按钮容器,创建一个
|
||||
@@ -1320,7 +1349,7 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
if (!processDetailBtn) {
|
||||
processDetailBtn = document.createElement('button');
|
||||
processDetailBtn.className = 'mcp-detail-btn process-detail-btn';
|
||||
processDetailBtn.innerHTML = '<span>展开详情</span>';
|
||||
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
|
||||
processDetailBtn.onclick = () => toggleProcessDetails(null, messageId);
|
||||
buttonsContainer.appendChild(processDetailBtn);
|
||||
}
|
||||
@@ -1360,7 +1389,7 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
// 如果没有processDetails或为空,显示空状态
|
||||
if (!processDetails || processDetails.length === 0) {
|
||||
// 显示空状态提示
|
||||
timeline.innerHTML = '<div class="progress-timeline-empty">暂无过程详情(可能执行过快或未触发详细事件)</div>';
|
||||
timeline.innerHTML = '<div class="progress-timeline-empty">' + (typeof window.t === 'function' ? window.t('chat.noProcessDetail') : '暂无过程详情(可能执行过快或未触发详细事件)') + '</div>';
|
||||
// 默认折叠
|
||||
timeline.classList.remove('expanded');
|
||||
return;
|
||||
@@ -1425,7 +1454,7 @@ function renderProcessDetails(messageId, processDetails) {
|
||||
// 更新按钮文本为"展开详情"
|
||||
const processDetailBtn = messageElement.querySelector('.process-detail-btn');
|
||||
if (processDetailBtn) {
|
||||
processDetailBtn.innerHTML = '<span>展开详情</span>';
|
||||
processDetailBtn.innerHTML = '<span>' + (typeof window.t === 'function' ? window.t('chat.expandDetail') : '展开详情') + '</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1679,7 +1708,8 @@ async function startNewConversation() {
|
||||
currentConversationId = null;
|
||||
currentConversationGroupId = null; // 新对话不属于任何分组
|
||||
document.getElementById('chat-messages').innerHTML = '';
|
||||
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
|
||||
const readyMsgNew = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsgNew);
|
||||
addAttackChainButton(null);
|
||||
updateActiveConversation();
|
||||
// 刷新分组列表,清除分组高亮
|
||||
@@ -2087,7 +2117,8 @@ async function loadConversation(conversationId) {
|
||||
}
|
||||
});
|
||||
} else {
|
||||
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
|
||||
const readyMsgEmpty = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsgEmpty);
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
@@ -2127,7 +2158,8 @@ async function deleteConversation(conversationId, skipConfirm = false) {
|
||||
if (conversationId === currentConversationId) {
|
||||
currentConversationId = null;
|
||||
document.getElementById('chat-messages').innerHTML = '';
|
||||
addMessage('assistant', '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。');
|
||||
const readyMsgLoad = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsgLoad);
|
||||
addAttackChainButton(null);
|
||||
}
|
||||
|
||||
@@ -4171,13 +4203,13 @@ async function showConversationContextMenu(event) {
|
||||
attackChainMenuItem.style.opacity = '1';
|
||||
attackChainMenuItem.style.cursor = 'pointer';
|
||||
attackChainMenuItem.onclick = showAttackChainFromContext;
|
||||
attackChainMenuItem.title = '查看当前对话的攻击链';
|
||||
attackChainMenuItem.title = (typeof window.t === 'function' ? window.t('chat.viewAttackChainCurrentConv') : '查看当前对话的攻击链');
|
||||
}
|
||||
} else {
|
||||
attackChainMenuItem.style.opacity = '0.5';
|
||||
attackChainMenuItem.style.cursor = 'not-allowed';
|
||||
attackChainMenuItem.onclick = null;
|
||||
attackChainMenuItem.title = '请选择一个对话以查看攻击链';
|
||||
attackChainMenuItem.title = (typeof window.t === 'function' ? window.t('chat.viewAttackChainSelectConv') : '请选择一个对话以查看攻击链');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4210,21 +4242,25 @@ async function showConversationContextMenu(event) {
|
||||
|
||||
// 更新菜单文本
|
||||
const pinMenuText = document.getElementById('pin-conversation-menu-text');
|
||||
if (pinMenuText) {
|
||||
if (pinMenuText && typeof window.t === 'function') {
|
||||
pinMenuText.textContent = isPinned ? window.t('contextMenu.unpinConversation') : window.t('contextMenu.pinConversation');
|
||||
} else if (pinMenuText) {
|
||||
pinMenuText.textContent = isPinned ? '取消置顶' : '置顶此对话';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取对话置顶状态失败:', error);
|
||||
// 如果获取失败,使用默认文本
|
||||
const pinMenuText = document.getElementById('pin-conversation-menu-text');
|
||||
if (pinMenuText) {
|
||||
if (pinMenuText && typeof window.t === 'function') {
|
||||
pinMenuText.textContent = window.t('contextMenu.pinConversation');
|
||||
} else if (pinMenuText) {
|
||||
pinMenuText.textContent = '置顶此对话';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 如果没有对话ID,使用默认文本
|
||||
const pinMenuText = document.getElementById('pin-conversation-menu-text');
|
||||
if (pinMenuText) {
|
||||
if (pinMenuText && typeof window.t === 'function') {
|
||||
pinMenuText.textContent = window.t('contextMenu.pinConversation');
|
||||
} else if (pinMenuText) {
|
||||
pinMenuText.textContent = '置顶此对话';
|
||||
}
|
||||
}
|
||||
@@ -4333,14 +4369,17 @@ async function showGroupContextMenu(event, groupId) {
|
||||
|
||||
// 更新菜单文本
|
||||
const pinMenuText = document.getElementById('pin-group-menu-text');
|
||||
if (pinMenuText) {
|
||||
if (pinMenuText && typeof window.t === 'function') {
|
||||
pinMenuText.textContent = isPinned ? window.t('contextMenu.unpinGroup') : window.t('contextMenu.pinGroup');
|
||||
} else if (pinMenuText) {
|
||||
pinMenuText.textContent = isPinned ? '取消置顶' : '置顶此分组';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分组置顶状态失败:', error);
|
||||
// 如果获取失败,使用默认文本
|
||||
const pinMenuText = document.getElementById('pin-group-menu-text');
|
||||
if (pinMenuText) {
|
||||
if (pinMenuText && typeof window.t === 'function') {
|
||||
pinMenuText.textContent = window.t('contextMenu.pinGroup');
|
||||
} else if (pinMenuText) {
|
||||
pinMenuText.textContent = '置顶此分组';
|
||||
}
|
||||
}
|
||||
@@ -4443,7 +4482,9 @@ async function renameConversation() {
|
||||
loadConversationsWithGroups();
|
||||
} catch (error) {
|
||||
console.error('重命名对话失败:', error);
|
||||
alert('重命名失败: ' + (error.message || '未知错误'));
|
||||
const failedLabel = typeof window.t === 'function' ? window.t('chat.renameFailed') : '重命名失败';
|
||||
const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
|
||||
alert(failedLabel + ': ' + (error.message || unknownErr));
|
||||
}
|
||||
|
||||
closeContextMenu();
|
||||
@@ -4636,13 +4677,14 @@ async function showMoveToGroupSubmenu() {
|
||||
}
|
||||
|
||||
// 始终显示"创建分组"选项
|
||||
const addGroupLabel = typeof window.t === 'function' ? window.t('chat.addNewGroup') : '+ 新增分组';
|
||||
const addItem = document.createElement('div');
|
||||
addItem.className = 'context-submenu-item add-group-item';
|
||||
addItem.innerHTML = `
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>+ 新增分组</span>
|
||||
<span>${addGroupLabel}</span>
|
||||
`;
|
||||
addItem.onclick = () => {
|
||||
showCreateGroupModal(true);
|
||||
@@ -4917,7 +4959,8 @@ function deleteConversationFromContext() {
|
||||
const convId = contextMenuConversationId;
|
||||
if (!convId) return;
|
||||
|
||||
if (confirm('确定要删除此对话吗?')) {
|
||||
const confirmMsg = typeof window.t === 'function' ? window.t('chat.deleteConversationConfirm') : '确定要删除此对话吗?';
|
||||
if (confirm(confirmMsg)) {
|
||||
deleteConversation(convId, true); // 跳过内部确认,因为这里已经确认过了
|
||||
}
|
||||
closeContextMenu();
|
||||
@@ -4944,6 +4987,15 @@ function closeContextMenu() {
|
||||
// 显示批量管理模态框
|
||||
let allConversationsForBatch = [];
|
||||
|
||||
// 更新批量管理模态框标题(含条数),支持 i18n;count 为当前条数
|
||||
function updateBatchManageTitle(count) {
|
||||
const titleEl = document.getElementById('batch-manage-title');
|
||||
if (!titleEl || typeof window.t !== 'function') return;
|
||||
const template = window.t('batchManageModal.title', { count: '__C__' });
|
||||
const parts = template.split('__C__');
|
||||
titleEl.innerHTML = (parts[0] || '') + '<span id="batch-manage-count">' + (count || 0) + '</span>' + (parts[1] || '');
|
||||
}
|
||||
|
||||
async function showBatchManageModal() {
|
||||
try {
|
||||
const response = await apiFetch('/api/conversations?limit=1000');
|
||||
@@ -4957,10 +5009,7 @@ async function showBatchManageModal() {
|
||||
}
|
||||
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
const countEl = document.getElementById('batch-manage-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = allConversationsForBatch.length;
|
||||
}
|
||||
updateBatchManageTitle(allConversationsForBatch.length);
|
||||
|
||||
renderBatchConversations();
|
||||
if (modal) {
|
||||
@@ -4971,10 +5020,7 @@ async function showBatchManageModal() {
|
||||
// 错误时使用空数组,不显示错误提示(更友好的用户体验)
|
||||
allConversationsForBatch = [];
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
const countEl = document.getElementById('batch-manage-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = 0;
|
||||
}
|
||||
updateBatchManageTitle(0);
|
||||
if (modal) {
|
||||
renderBatchConversations();
|
||||
modal.style.display = 'flex';
|
||||
@@ -5041,7 +5087,7 @@ function renderBatchConversations(filtered = null) {
|
||||
|
||||
const name = document.createElement('div');
|
||||
name.className = 'batch-table-col-name';
|
||||
const originalTitle = conv.title || '未命名对话';
|
||||
const originalTitle = conv.title || (typeof window.t === 'function' ? window.t('batchManageModal.unnamedConversation') : '未命名对话');
|
||||
// 使用安全截断函数,限制最大长度为45个字符(留出空间显示省略号)
|
||||
const truncatedTitle = safeTruncateText(originalTitle, 45);
|
||||
name.textContent = truncatedTitle;
|
||||
@@ -5051,7 +5097,8 @@ function renderBatchConversations(filtered = null) {
|
||||
const time = document.createElement('div');
|
||||
time.className = 'batch-table-col-time';
|
||||
const dateObj = conv.updatedAt ? new Date(conv.updatedAt) : new Date();
|
||||
time.textContent = dateObj.toLocaleString('zh-CN', {
|
||||
const locale = (typeof i18next !== 'undefined' && i18next.language) ? i18next.language : 'zh-CN';
|
||||
time.textContent = dateObj.toLocaleString(locale, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
@@ -5105,11 +5152,12 @@ function toggleSelectAllBatch() {
|
||||
async function deleteSelectedConversations() {
|
||||
const checkboxes = document.querySelectorAll('.batch-conversation-checkbox:checked');
|
||||
if (checkboxes.length === 0) {
|
||||
alert('请先选择要删除的对话');
|
||||
alert(typeof window.t === 'function' ? window.t('batchManageModal.confirmDeleteNone') : '请先选择要删除的对话');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`确定要删除选中的 ${checkboxes.length} 条对话吗?`)) {
|
||||
const confirmMsg = typeof window.t === 'function' ? window.t('batchManageModal.confirmDeleteN', { count: checkboxes.length }) : '确定要删除选中的 ' + checkboxes.length + ' 条对话吗?';
|
||||
if (!confirm(confirmMsg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5123,7 +5171,9 @@ async function deleteSelectedConversations() {
|
||||
loadConversationsWithGroups();
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error);
|
||||
alert('删除失败: ' + (error.message || '未知错误'));
|
||||
const failedMsg = typeof window.t === 'function' ? window.t('batchManageModal.deleteFailed') : '删除失败';
|
||||
const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
|
||||
alert(failedMsg + ': ' + (error.message || unknownErr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5140,6 +5190,14 @@ function closeBatchManageModal() {
|
||||
allConversationsForBatch = [];
|
||||
}
|
||||
|
||||
// 语言切换时刷新批量管理模态框标题(若当前正在显示)
|
||||
document.addEventListener('languagechange', function () {
|
||||
const modal = document.getElementById('batch-manage-modal');
|
||||
if (modal && modal.style.display === 'flex') {
|
||||
updateBatchManageTitle(allConversationsForBatch.length);
|
||||
}
|
||||
});
|
||||
|
||||
// 显示创建分组模态框
|
||||
function showCreateGroupModal(andMoveConversation = false) {
|
||||
const modal = document.getElementById('create-group-modal');
|
||||
@@ -5208,6 +5266,15 @@ function selectSuggestion(name) {
|
||||
}
|
||||
}
|
||||
|
||||
// 按 i18n key 选择建议标签(用于国际化下填充当前语言的文案)
|
||||
function selectSuggestionByKey(i18nKey) {
|
||||
const input = document.getElementById('create-group-name-input');
|
||||
if (input && typeof window.t === 'function') {
|
||||
input.value = window.t(i18nKey);
|
||||
input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// 切换图标选择器显示状态
|
||||
function toggleGroupIconPicker() {
|
||||
const picker = document.getElementById('group-icon-picker');
|
||||
@@ -5299,7 +5366,7 @@ async function createGroup(event) {
|
||||
|
||||
const name = input.value.trim();
|
||||
if (!name) {
|
||||
alert('请输入分组名称');
|
||||
alert(typeof window.t === 'function' ? window.t('createGroupModal.groupNamePlaceholder') : '请输入分组名称');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5320,7 +5387,7 @@ async function createGroup(event) {
|
||||
|
||||
const nameExists = groups.some(g => g.name === name);
|
||||
if (nameExists) {
|
||||
alert('分组名称已存在,请使用其他名称');
|
||||
alert(typeof window.t === 'function' ? window.t('createGroupModal.nameExists') : '分组名称已存在,请使用其他名称');
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -5345,11 +5412,13 @@ async function createGroup(event) {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
const nameExistsMsg = typeof window.t === 'function' ? window.t('createGroupModal.nameExists') : '分组名称已存在,请使用其他名称';
|
||||
if (error.error && error.error.includes('已存在')) {
|
||||
alert('分组名称已存在,请使用其他名称');
|
||||
alert(nameExistsMsg);
|
||||
return;
|
||||
}
|
||||
throw new Error(error.error || '创建失败');
|
||||
const createFailedMsg = typeof window.t === 'function' ? window.t('createGroupModal.createFailed') : '创建失败';
|
||||
throw new Error(error.error || createFailedMsg);
|
||||
}
|
||||
|
||||
const newGroup = await response.json();
|
||||
@@ -5375,7 +5444,9 @@ async function createGroup(event) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('创建分组失败:', error);
|
||||
alert('创建失败: ' + (error.message || '未知错误'));
|
||||
const createFailedMsg = typeof window.t === 'function' ? window.t('createGroupModal.createFailed') : '创建失败';
|
||||
const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
|
||||
alert(createFailedMsg + ': ' + (error.message || unknownErr));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5517,10 +5588,12 @@ async function loadGroupConversations(groupId, searchQuery = '') {
|
||||
list.innerHTML = '';
|
||||
|
||||
if (groupConvs.length === 0) {
|
||||
const emptyMsg = typeof window.t === 'function' ? window.t('chat.emptyGroupConversations') : '该分组暂无对话';
|
||||
const noMatchMsg = typeof window.t === 'function' ? window.t('chat.noMatchingConversationsInGroup') : '未找到匹配的对话';
|
||||
if (searchQuery && searchQuery.trim()) {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">未找到匹配的对话</div>';
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (noMatchMsg || '未找到匹配的对话') + '</div>';
|
||||
} else {
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">该分组暂无对话</div>';
|
||||
list.innerHTML = '<div style="padding: 40px; text-align: center; color: var(--text-muted);">' + (emptyMsg || '该分组暂无对话') + '</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -5651,7 +5724,8 @@ async function editGroup() {
|
||||
const group = await response.json();
|
||||
if (!group) return;
|
||||
|
||||
const newName = prompt('请输入新名称:', group.name);
|
||||
const renamePrompt = typeof window.t === 'function' ? window.t('chat.renameGroupPrompt') : '请输入新名称:';
|
||||
const newName = prompt(renamePrompt, group.name);
|
||||
if (newName === null || !newName.trim()) return;
|
||||
|
||||
const trimmedName = newName.trim();
|
||||
@@ -5672,7 +5746,7 @@ async function editGroup() {
|
||||
|
||||
const nameExists = groups.some(g => g.name === trimmedName && g.id !== currentGroupId);
|
||||
if (nameExists) {
|
||||
alert('分组名称已存在,请使用其他名称');
|
||||
alert(typeof window.t === 'function' ? window.t('createGroupModal.nameExists') : '分组名称已存在,请使用其他名称');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5712,7 +5786,8 @@ async function editGroup() {
|
||||
async function deleteGroup() {
|
||||
if (!currentGroupId) return;
|
||||
|
||||
if (!confirm('确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。')) {
|
||||
const deleteConfirmMsg = typeof window.t === 'function' ? window.t('chat.deleteGroupConfirm') : '确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。';
|
||||
if (!confirm(deleteConfirmMsg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5758,7 +5833,8 @@ async function renameGroupFromContext() {
|
||||
const group = await response.json();
|
||||
if (!group) return;
|
||||
|
||||
const newName = prompt('请输入新名称:', group.name);
|
||||
const renamePrompt = typeof window.t === 'function' ? window.t('chat.renameGroupPrompt') : '请输入新名称:';
|
||||
const newName = prompt(renamePrompt, group.name);
|
||||
if (newName === null || !newName.trim()) {
|
||||
closeGroupContextMenu();
|
||||
return;
|
||||
@@ -5782,7 +5858,7 @@ async function renameGroupFromContext() {
|
||||
|
||||
const nameExists = groups.some(g => g.name === trimmedName && g.id !== groupId);
|
||||
if (nameExists) {
|
||||
alert('分组名称已存在,请使用其他名称');
|
||||
alert(typeof window.t === 'function' ? window.t('createGroupModal.nameExists') : '分组名称已存在,请使用其他名称');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -5817,7 +5893,9 @@ async function renameGroupFromContext() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重命名分组失败:', error);
|
||||
alert('重命名失败: ' + (error.message || '未知错误'));
|
||||
const failedLabel = typeof window.t === 'function' ? window.t('chat.renameFailed') : '重命名失败';
|
||||
const unknownErr = typeof window.t === 'function' ? window.t('createGroupModal.unknownError') : '未知错误';
|
||||
alert(failedLabel + ': ' + (error.message || unknownErr));
|
||||
}
|
||||
|
||||
closeGroupContextMenu();
|
||||
@@ -5867,7 +5945,8 @@ async function deleteGroupFromContext() {
|
||||
const groupId = contextMenuGroupId;
|
||||
if (!groupId) return;
|
||||
|
||||
if (!confirm('确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。')) {
|
||||
const deleteConfirmMsg = typeof window.t === 'function' ? window.t('chat.deleteGroupConfirm') : '确定要删除此分组吗?分组中的对话不会被删除,但会从分组中移除。';
|
||||
if (!confirm(deleteConfirmMsg)) {
|
||||
closeGroupContextMenu();
|
||||
return;
|
||||
}
|
||||
|
||||
+11
-11
@@ -17,7 +17,7 @@ async function refreshDashboard() {
|
||||
setEl('dashboard-kpi-tools-calls', '…');
|
||||
setEl('dashboard-kpi-success-rate', '…');
|
||||
var chartPlaceholder = document.getElementById('dashboard-tools-pie-placeholder');
|
||||
if (chartPlaceholder) { chartPlaceholder.style.removeProperty('display'); chartPlaceholder.textContent = '加载中…'; }
|
||||
if (chartPlaceholder) { chartPlaceholder.style.removeProperty('display'); chartPlaceholder.textContent = (typeof window.t === 'function' ? window.t('common.loading') : '加载中…'); }
|
||||
var barChartEl = document.getElementById('dashboard-tools-bar-chart');
|
||||
if (barChartEl) { barChartEl.style.display = 'none'; barChartEl.innerHTML = ''; }
|
||||
|
||||
@@ -77,7 +77,7 @@ async function refreshDashboard() {
|
||||
setEl('dashboard-batch-pending', String(pending));
|
||||
setEl('dashboard-batch-running', String(running));
|
||||
setEl('dashboard-batch-done', String(done));
|
||||
setEl('dashboard-batch-total', total > 0 ? `共 ${total} 个` : '暂无任务');
|
||||
setEl('dashboard-batch-total', total > 0 ? (typeof window.t === 'function' ? window.t('dashboard.totalCount', { count: total }) : `共 ${total} 个`) : (typeof window.t === 'function' ? window.t('dashboard.noTasks') : '暂无任务'));
|
||||
|
||||
// 更新进度条
|
||||
if (total > 0) {
|
||||
@@ -138,7 +138,7 @@ async function refreshDashboard() {
|
||||
if (knowledgeRes && typeof knowledgeRes === 'object') {
|
||||
if (knowledgeRes.enabled === false) {
|
||||
// 功能未启用:用状态标签展示,数值保持为 "-"
|
||||
if (knowledgeStatusEl) knowledgeStatusEl.textContent = '未启用';
|
||||
if (knowledgeStatusEl) knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.notEnabled') : '未启用');
|
||||
if (knowledgeItemsEl) knowledgeItemsEl.textContent = '-';
|
||||
if (knowledgeCategoriesEl) knowledgeCategoriesEl.textContent = '-';
|
||||
} else {
|
||||
@@ -149,9 +149,9 @@ async function refreshDashboard() {
|
||||
// 根据数据量给个轻量状态文案
|
||||
if (knowledgeStatusEl) {
|
||||
if (items > 0 || categories > 0) {
|
||||
knowledgeStatusEl.textContent = '已启用';
|
||||
knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.enabled') : '已启用');
|
||||
} else {
|
||||
knowledgeStatusEl.textContent = '待配置';
|
||||
knowledgeStatusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.toConfigure') : '待配置');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,15 +172,15 @@ async function refreshDashboard() {
|
||||
const statusEl = document.getElementById('dashboard-skills-status');
|
||||
if (statusEl) {
|
||||
if (totalCalls === 0) {
|
||||
statusEl.textContent = '待使用';
|
||||
statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.toUse') : '待使用');
|
||||
statusEl.style.background = 'rgba(0, 0, 0, 0.05)';
|
||||
statusEl.style.color = 'var(--text-secondary)';
|
||||
} else if (totalCalls < 10) {
|
||||
statusEl.textContent = '活跃';
|
||||
statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.active') : '活跃');
|
||||
statusEl.style.background = 'rgba(16, 185, 129, 0.1)';
|
||||
statusEl.style.color = '#10b981';
|
||||
} else {
|
||||
statusEl.textContent = '高频';
|
||||
statusEl.textContent = (typeof window.t === 'function' ? window.t('dashboard.highFreq') : '高频');
|
||||
statusEl.style.background = 'rgba(59, 130, 246, 0.1)';
|
||||
statusEl.style.color = '#3b82f6';
|
||||
}
|
||||
@@ -200,7 +200,7 @@ async function refreshDashboard() {
|
||||
setEl('dashboard-kpi-tools-calls', '-');
|
||||
renderDashboardToolsBar(null);
|
||||
var ph = document.getElementById('dashboard-tools-pie-placeholder');
|
||||
if (ph) { ph.style.removeProperty('display'); ph.textContent = '暂无调用数据'; }
|
||||
if (ph) { ph.style.removeProperty('display'); ph.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据'); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,7 +257,7 @@ function renderDashboardToolsBar(monitorRes) {
|
||||
|
||||
if (!monitorRes || typeof monitorRes !== 'object') {
|
||||
placeholder.style.removeProperty('display');
|
||||
placeholder.textContent = '暂无调用数据';
|
||||
placeholder.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据');
|
||||
barChartEl.style.display = 'none';
|
||||
barChartEl.innerHTML = '';
|
||||
return;
|
||||
@@ -273,7 +273,7 @@ function renderDashboardToolsBar(monitorRes) {
|
||||
|
||||
if (entries.length === 0) {
|
||||
placeholder.style.removeProperty('display');
|
||||
placeholder.textContent = '暂无调用数据';
|
||||
placeholder.textContent = (typeof window.t === 'function' ? window.t('dashboard.noCallData') : '暂无调用数据');
|
||||
barChartEl.style.display = 'none';
|
||||
barChartEl.innerHTML = '';
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
// 前端国际化初始化(基于 i18next 浏览器版本)
|
||||
(function () {
|
||||
const DEFAULT_LANG = 'zh-CN';
|
||||
const STORAGE_KEY = 'csai_lang';
|
||||
const RESOURCES_PREFIX = '/static/i18n';
|
||||
|
||||
const loadedLangs = {};
|
||||
|
||||
function detectInitialLang() {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
return stored;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('无法读取语言设置:', e);
|
||||
}
|
||||
|
||||
const navLang = (navigator.language || navigator.userLanguage || '').toLowerCase();
|
||||
if (navLang.startsWith('zh')) {
|
||||
return 'zh-CN';
|
||||
}
|
||||
if (navLang.startsWith('en')) {
|
||||
return 'en-US';
|
||||
}
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
|
||||
async function loadLanguageResources(lang) {
|
||||
if (loadedLangs[lang]) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(RESOURCES_PREFIX + '/' + lang + '.json', {
|
||||
cache: 'no-cache'
|
||||
});
|
||||
if (!resp.ok) {
|
||||
console.warn('加载语言包失败:', lang, resp.status);
|
||||
return;
|
||||
}
|
||||
const data = await resp.json();
|
||||
if (typeof i18next !== 'undefined') {
|
||||
i18next.addResourceBundle(lang, 'translation', data, true, true);
|
||||
}
|
||||
loadedLangs[lang] = true;
|
||||
} catch (e) {
|
||||
console.error('加载语言包异常:', lang, e);
|
||||
}
|
||||
}
|
||||
|
||||
function applyTranslations(root) {
|
||||
if (typeof i18next === 'undefined') return;
|
||||
const container = root || document;
|
||||
if (!container) return;
|
||||
|
||||
const elements = container.querySelectorAll('[data-i18n]');
|
||||
elements.forEach(function (el) {
|
||||
const key = el.getAttribute('data-i18n');
|
||||
if (!key) return;
|
||||
const skipText = el.getAttribute('data-i18n-skip-text') === 'true';
|
||||
const isFormControl = (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA');
|
||||
const attrList = el.getAttribute('data-i18n-attr');
|
||||
const text = i18next.t(key);
|
||||
|
||||
// 仅当未使用 data-i18n-attr 时才替换元素文本内容(否则会覆盖卡片内的数字、子节点等)
|
||||
// input/textarea:永不设置 textContent(会变成 value),只更新属性
|
||||
if (!attrList && !skipText && !isFormControl && text && typeof text === 'string') {
|
||||
el.textContent = text;
|
||||
}
|
||||
|
||||
if (attrList) {
|
||||
attrList.split(',').map(function (s) { return s.trim(); }).forEach(function (attr) {
|
||||
if (!attr) return;
|
||||
if (text && typeof text === 'string') {
|
||||
el.setAttribute(attr, text);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 对话输入框:若 value 与 placeholder 相同,清空 value 以便正确显示占位提示
|
||||
try {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
if (chatInput && chatInput.tagName === 'TEXTAREA') {
|
||||
const ph = (chatInput.getAttribute('placeholder') || '').trim();
|
||||
if (ph && chatInput.value.trim() === ph) {
|
||||
chatInput.value = '';
|
||||
}
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
// 更新 html lang 属性
|
||||
try {
|
||||
if (document && document.documentElement) {
|
||||
document.documentElement.lang = i18next.language || DEFAULT_LANG;
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function updateLangLabel() {
|
||||
const label = document.getElementById('current-lang-label');
|
||||
if (!label || typeof i18next === 'undefined') return;
|
||||
const lang = (i18next.language || DEFAULT_LANG).toLowerCase();
|
||||
if (lang.indexOf('zh') === 0) {
|
||||
label.textContent = '中文';
|
||||
} else {
|
||||
label.textContent = 'English';
|
||||
}
|
||||
}
|
||||
|
||||
function closeLangDropdown() {
|
||||
const dropdown = document.getElementById('lang-dropdown');
|
||||
if (dropdown) {
|
||||
dropdown.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function handleGlobalClickForLangDropdown(ev) {
|
||||
const dropdown = document.getElementById('lang-dropdown');
|
||||
const btn = document.querySelector('.lang-switcher-btn');
|
||||
if (!dropdown || dropdown.style.display !== 'block') return;
|
||||
const target = ev.target;
|
||||
if (btn && btn.contains(target)) {
|
||||
return;
|
||||
}
|
||||
if (!dropdown.contains(target)) {
|
||||
closeLangDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
async function changeLanguage(lang) {
|
||||
if (typeof i18next === 'undefined') return;
|
||||
const current = i18next.language || DEFAULT_LANG;
|
||||
if (lang === current) return;
|
||||
await loadLanguageResources(lang);
|
||||
await i18next.changeLanguage(lang);
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, lang);
|
||||
} catch (e) {
|
||||
console.warn('无法保存语言设置:', e);
|
||||
}
|
||||
applyTranslations(document);
|
||||
updateLangLabel();
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('languagechange', { detail: { lang: lang } }));
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
async function initI18n() {
|
||||
if (typeof i18next === 'undefined') {
|
||||
console.warn('i18next 未加载,跳过前端国际化初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
const initialLang = detectInitialLang();
|
||||
await i18next.init({
|
||||
lng: initialLang,
|
||||
fallbackLng: DEFAULT_LANG,
|
||||
debug: false,
|
||||
resources: {}
|
||||
});
|
||||
|
||||
await loadLanguageResources(initialLang);
|
||||
applyTranslations(document);
|
||||
updateLangLabel();
|
||||
|
||||
// 导出全局函数供其他脚本调用(支持插值参数,如 _t('key', { count: 2 }))
|
||||
window.t = function (key, opts) {
|
||||
if (typeof i18next === 'undefined') return key;
|
||||
return i18next.t(key, opts);
|
||||
};
|
||||
window.changeLanguage = changeLanguage;
|
||||
window.applyTranslations = applyTranslations;
|
||||
|
||||
// 语言切换下拉支持
|
||||
window.toggleLangDropdown = function () {
|
||||
const dropdown = document.getElementById('lang-dropdown');
|
||||
if (!dropdown) return;
|
||||
if (dropdown.style.display === 'block') {
|
||||
dropdown.style.display = 'none';
|
||||
} else {
|
||||
dropdown.style.display = 'block';
|
||||
}
|
||||
};
|
||||
window.onLanguageSelect = function (lang) {
|
||||
changeLanguage(lang);
|
||||
closeLangDropdown();
|
||||
};
|
||||
|
||||
document.addEventListener('click', handleGlobalClickForLangDropdown);
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// i18n 初始化在 DOM Ready 后执行
|
||||
initI18n().catch(function (e) {
|
||||
console.error('初始化国际化失败:', e);
|
||||
});
|
||||
});
|
||||
})();
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
// 信息收集页面(FOFA)
|
||||
function _t(key, opts) {
|
||||
return typeof window.t === 'function' ? window.t(key, opts) : key;
|
||||
}
|
||||
|
||||
const FOFA_FORM_STORAGE_KEY = 'info-collect-fofa-form';
|
||||
const FOFA_HIDDEN_FIELDS_STORAGE_KEY = 'info-collect-fofa-hidden-fields';
|
||||
@@ -197,12 +200,12 @@ async function submitFofaSearch() {
|
||||
const full = !!els.full?.checked;
|
||||
|
||||
if (!query) {
|
||||
alert('请输入 FOFA 查询语法');
|
||||
alert(_t('infoCollect.enterFofaQuery'));
|
||||
return;
|
||||
}
|
||||
|
||||
saveFofaFormToStorage({ query, size, page, fields, full });
|
||||
setFofaMeta('查询中...');
|
||||
setFofaMeta(_t('infoCollect.querying'));
|
||||
setFofaLoading(true);
|
||||
|
||||
try {
|
||||
@@ -219,9 +222,9 @@ async function submitFofaSearch() {
|
||||
renderFofaResults(result);
|
||||
} catch (e) {
|
||||
console.error('FOFA 查询失败:', e);
|
||||
setFofaMeta('查询失败');
|
||||
setFofaMeta(_t('infoCollect.queryFailed'));
|
||||
renderFofaResults({ query, fields: [], results: [], total: 0, page: 1, size: 0 });
|
||||
alert('FOFA 查询失败: ' + (e && e.message ? e.message : String(e)));
|
||||
alert(_t('infoCollect.queryFailed') + ': ' + (e && e.message ? e.message : String(e)));
|
||||
} finally {
|
||||
setFofaLoading(false);
|
||||
}
|
||||
@@ -231,7 +234,7 @@ async function parseFofaNaturalLanguage() {
|
||||
const els = getFofaFormElements();
|
||||
const text = (els.nl?.value || '').trim();
|
||||
if (!text) {
|
||||
alert('请输入自然语言描述');
|
||||
alert(_t('infoCollect.enterNaturalLanguage'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -243,16 +246,16 @@ async function parseFofaNaturalLanguage() {
|
||||
|
||||
// 先创建 controller,避免极快的重复点击触发并发请求
|
||||
fofaParseAbortController = new AbortController();
|
||||
setFofaParseLoading(true, 'AI 解析中...');
|
||||
setFofaParseLoading(true, _t('infoCollect.parsePending'));
|
||||
|
||||
// 持续提示:直到请求完成/取消/失败才消失
|
||||
fofaParseToastHandle = showInlineToast('AI 解析中...(点击按钮可取消)', { duration: 0, id: 'fofa-parse-pending' });
|
||||
fofaParseToastHandle = showInlineToast(_t('infoCollect.parsePendingClickCancel'), { duration: 0, id: 'fofa-parse-pending' });
|
||||
|
||||
// 如果超过一小段时间还没返回,再强调“仍在进行中”,降低误判为失败的概率
|
||||
fofaParseSlowTimer = setTimeout(() => {
|
||||
const status = document.getElementById('fofa-nl-status');
|
||||
if (status) {
|
||||
status.textContent = 'AI 解析耗时较长,仍在处理中…';
|
||||
status.textContent = _t('infoCollect.parseSlow');
|
||||
status.style.display = 'block';
|
||||
}
|
||||
}, 1800);
|
||||
@@ -269,15 +272,15 @@ async function parseFofaNaturalLanguage() {
|
||||
throw new Error(result.error || `请求失败: ${resp.status}`);
|
||||
}
|
||||
showFofaParseModal(text, result);
|
||||
showInlineToast('AI 解析完成');
|
||||
showInlineToast(_t('infoCollect.parseDone'));
|
||||
} catch (e) {
|
||||
// AbortController 取消:不视为失败
|
||||
if (e && (e.name === 'AbortError' || String(e).includes('AbortError'))) {
|
||||
showInlineToast('已取消 AI 解析');
|
||||
showInlineToast(_t('infoCollect.parseCancelled'));
|
||||
return;
|
||||
}
|
||||
console.error('FOFA 自然语言解析失败:', e);
|
||||
showInlineToast('AI 解析失败:' + (e && e.message ? e.message : String(e)), { duration: 2800 });
|
||||
showInlineToast(_t('infoCollect.parseFailed') + (e && e.message ? e.message : String(e)), { duration: 2800 });
|
||||
}
|
||||
finally {
|
||||
fofaParseAbortController = null;
|
||||
@@ -298,17 +301,17 @@ function setFofaParseLoading(loading, statusText) {
|
||||
const status = document.getElementById('fofa-nl-status');
|
||||
if (btn) {
|
||||
if (loading) {
|
||||
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent || 'AI 解析';
|
||||
if (!btn.dataset.originalText) btn.dataset.originalText = btn.textContent || _t('infoCollectPage.parseBtn');
|
||||
btn.classList.add('btn-loading');
|
||||
btn.textContent = '取消解析';
|
||||
btn.title = '点击取消 AI 解析';
|
||||
btn.textContent = _t('infoCollect.cancelParse');
|
||||
btn.title = _t('infoCollect.clickToCancelParse');
|
||||
btn.dataset.loading = '1';
|
||||
btn.setAttribute('aria-busy', 'true');
|
||||
btn.disabled = false;
|
||||
} else {
|
||||
btn.classList.remove('btn-loading');
|
||||
btn.textContent = btn.dataset.originalText || 'AI 解析';
|
||||
btn.title = '将自然语言解析为 FOFA 查询语法';
|
||||
btn.textContent = btn.dataset.originalText || _t('infoCollectPage.parseBtn');
|
||||
btn.title = _t('infoCollect.parseToFofa');
|
||||
btn.disabled = false;
|
||||
delete btn.dataset.loading;
|
||||
btn.removeAttribute('aria-busy');
|
||||
@@ -336,7 +339,7 @@ function showFofaParseModal(nlText, parsed) {
|
||||
|
||||
const warningsHtml = warnings.length
|
||||
? `<ul style="margin: 8px 0 0 18px;">${warnings.map(w => `<li>${escapeHtml(w)}</li>`).join('')}</ul>`
|
||||
: `<div class="muted" style="margin-top: 8px;">无</div>`;
|
||||
: '<div class="muted" style="margin-top: 8px;">' + _t('infoCollect.none') + '</div>';
|
||||
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'fofa-parse-modal';
|
||||
@@ -345,23 +348,23 @@ function showFofaParseModal(nlText, parsed) {
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 900px;">
|
||||
<div class="modal-header">
|
||||
<h2>AI 解析结果</h2>
|
||||
<span class="modal-close" id="fofa-parse-modal-close" title="关闭">×</span>
|
||||
<h2>${_t('infoCollect.parseResultTitle')}</h2>
|
||||
<span class="modal-close" id="fofa-parse-modal-close" title="${_t('common.close')}">×</span>
|
||||
</div>
|
||||
<div style="padding: 18px 28px; overflow: auto;">
|
||||
<div class="form-group">
|
||||
<label>自然语言</label>
|
||||
<label>${_t('infoCollect.naturalLanguageLabel')}</label>
|
||||
<div class="muted" style="margin-top: 6px; white-space: pre-wrap;">${safeNL || '-'}</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 14px;">
|
||||
<label for="fofa-parse-query">FOFA 查询语法(可编辑)</label>
|
||||
<textarea id="fofa-parse-query" class="info-collect-query-input" rows="2" placeholder='例如:app="Apache" && country="CN"'></textarea>
|
||||
<small class="form-hint">请人工确认语法与范围无误后再执行查询。</small>
|
||||
<label for="fofa-parse-query">${_t('infoCollect.fofaQueryEditable')}</label>
|
||||
<textarea id="fofa-parse-query" class="info-collect-query-input" rows="2" placeholder="${_t('infoCollect.queryPlaceholder')}"></textarea>
|
||||
<small class="form-hint">${_t('infoCollect.confirmBeforeQuery')}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-top: 14px;">
|
||||
<label>提醒</label>
|
||||
<label>${_t('infoCollect.reminder')}</label>
|
||||
<div style="background: #fff8e1; border: 1px solid #ffe8a3; border-radius: 10px; padding: 10px 12px;">
|
||||
${warningsHtml}
|
||||
</div>
|
||||
@@ -369,14 +372,14 @@ function showFofaParseModal(nlText, parsed) {
|
||||
|
||||
${explanation ? `
|
||||
<div class="form-group" style="margin-top: 14px;">
|
||||
<label>解析说明</label>
|
||||
<label>${_t('infoCollect.explanation')}</label>
|
||||
<pre style="margin-top: 8px; white-space: pre-wrap; background: var(--bg-tertiary); border: 1px solid var(--border-color); border-radius: 10px; padding: 10px 12px; font-size: 13px;">${escapeHtml(explanation)}</pre>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
<div class="modal-footer" style="padding: 18px 28px;">
|
||||
<button class="btn-secondary" type="button" id="fofa-parse-cancel">取消</button>
|
||||
<button class="btn-secondary" type="button" id="fofa-parse-apply">填入查询框</button>
|
||||
<button class="btn-primary" type="button" id="fofa-parse-apply-run">填入并查询</button>
|
||||
<button class="btn-secondary" type="button" id="fofa-parse-cancel">${_t('infoCollect.parseModalCancel')}</button>
|
||||
<button class="btn-secondary" type="button" id="fofa-parse-apply">${_t('infoCollect.parseModalApply')}</button>
|
||||
<button class="btn-primary" type="button" id="fofa-parse-apply-run">${_t('infoCollect.parseModalApplyRun')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -402,7 +405,7 @@ function showFofaParseModal(nlText, parsed) {
|
||||
const els = getFofaFormElements();
|
||||
const q = (queryTextarea?.value || '').trim();
|
||||
if (!q) {
|
||||
showInlineToast('解析结果为空:请在弹窗中补充/修改 FOFA 查询语法', { duration: 2600 });
|
||||
showInlineToast(_t('infoCollect.parseResultEmpty'), { duration: 2600 });
|
||||
return;
|
||||
}
|
||||
if (els.query) {
|
||||
@@ -444,7 +447,7 @@ function setFofaMeta(text) {
|
||||
function updateSelectedMeta() {
|
||||
const els = getFofaFormElements();
|
||||
if (els.selectedMeta) {
|
||||
els.selectedMeta.textContent = `已选择 ${infoCollectState.selectedRowIndexes.size} 条`;
|
||||
els.selectedMeta.textContent = _t('infoCollectPage.selectedRows', { count: infoCollectState.selectedRowIndexes.size });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -454,7 +457,7 @@ function setFofaLoading(loading) {
|
||||
if (loading) {
|
||||
const fieldsCount = (document.getElementById('fofa-fields')?.value || '').split(',').filter(Boolean).length;
|
||||
const colspan = Math.max(1, fieldsCount + 1);
|
||||
els.tbody.innerHTML = `<tr><td class="muted" style="padding: 16px;" colspan="${colspan}">加载中...</td></tr>`;
|
||||
els.tbody.innerHTML = '<tr><td class="muted" style="padding: 16px;" colspan="' + colspan + '">' + escapeHtml(_t('infoCollect.loading')) + '</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -490,7 +493,7 @@ function renderFofaResults(payload) {
|
||||
const size = typeof payload.size === 'number' ? payload.size : 0;
|
||||
const page = typeof payload.page === 'number' ? payload.page : 1;
|
||||
|
||||
setFofaMeta(`共 ${total} 条 · 本页 ${results.length} 条 · page=${page} · size=${size}`);
|
||||
setFofaMeta(_t('infoCollect.resultsMeta', { total, count: results.length, page, size }));
|
||||
|
||||
// 可见字段
|
||||
const visibleFields = fields.filter(f => !infoCollectState.hiddenFields.has(f));
|
||||
@@ -500,16 +503,16 @@ function renderFofaResults(payload) {
|
||||
|
||||
// 表头(左:勾选列;右:操作列固定)
|
||||
const headerCells = [
|
||||
'<th class="info-collect-col-select"><input type="checkbox" id="fofa-select-all" title="全选/全不选"/></th>',
|
||||
'<th class="info-collect-col-select"><input type="checkbox" id="fofa-select-all" title="' + escapeHtml(_t('infoCollect.selectAll')) + '"/></th>',
|
||||
...visibleFields.map(f => `<th>${escapeHtml(String(f))}</th>`),
|
||||
'<th class="info-collect-col-actions">操作</th>'
|
||||
'<th class="info-collect-col-actions">' + escapeHtml(_t('infoCollect.actions')) + '</th>'
|
||||
].join('');
|
||||
els.thead.innerHTML = `<tr>${headerCells}</tr>`;
|
||||
|
||||
// 表体
|
||||
if (results.length === 0) {
|
||||
const colspan = Math.max(1, visibleFields.length + 2);
|
||||
els.tbody.innerHTML = `<tr><td class="muted" style="padding: 16px;" colspan="${colspan}">暂无数据</td></tr>`;
|
||||
els.tbody.innerHTML = '<tr><td class="muted" style="padding: 16px;" colspan="' + colspan + '">' + escapeHtml(_t('common.noData')) + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -519,7 +522,7 @@ function renderFofaResults(payload) {
|
||||
const encoded = encodeURIComponent(JSON.stringify(safeRow));
|
||||
const encodedTarget = encodeURIComponent(target || '');
|
||||
|
||||
const selectHtml = `<td class="info-collect-col-select"><input class="fofa-row-select" type="checkbox" data-index="${idx}" title="选择该行"/></td>`;
|
||||
const selectHtml = '<td class="info-collect-col-select"><input class="fofa-row-select" type="checkbox" data-index="' + idx + '" title="' + escapeHtml(_t('infoCollect.selectRow')) + '"/></td>';
|
||||
|
||||
const cellsHtml = visibleFields.map(f => {
|
||||
const val = safeRow[f];
|
||||
@@ -537,13 +540,13 @@ function renderFofaResults(payload) {
|
||||
|
||||
const actionHtml = `
|
||||
<div class="info-collect-actions">
|
||||
<button class="btn-icon" onclick="copyFofaTargetEncoded('${encodedTarget}'); event.stopPropagation();" title="复制目标">
|
||||
<button class="btn-icon" onclick="copyFofaTargetEncoded('${encodedTarget}'); event.stopPropagation();" title="${escapeHtml(_t('infoCollect.copyTarget'))}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn-icon" onclick="scanFofaRow('${encoded}', event); event.stopPropagation();" title="发送到对话(可编辑;Ctrl/⌘+点击可直接发送)">
|
||||
<button class="btn-icon" onclick="scanFofaRow('${encoded}', event); event.stopPropagation();" title="${escapeHtml(_t('infoCollect.sendToChat'))}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5 13.5l3-3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
<path d="M8 8H5a4 4 0 1 0 0 8h3" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
@@ -602,14 +605,14 @@ function normalizeHttpLink(raw) {
|
||||
function copyFofaTarget(target) {
|
||||
const text = (target || '').trim();
|
||||
if (!text) {
|
||||
alert('没有可复制的目标');
|
||||
alert(_t('infoCollect.noTargetToCopy'));
|
||||
return;
|
||||
}
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
// 简单提示
|
||||
showInlineToast('已复制目标');
|
||||
showInlineToast(_t('infoCollect.targetCopied'));
|
||||
}).catch(() => {
|
||||
alert('复制失败,请手动复制:' + text);
|
||||
alert(_t('infoCollect.manualCopyHint') + text);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -655,7 +658,7 @@ function showInlineToast(text, options) {
|
||||
function truncateForPreview(value, maxLen) {
|
||||
const s = value == null ? '' : String(value);
|
||||
if (maxLen <= 0 || s.length <= maxLen) return s;
|
||||
return s.slice(0, maxLen) + '...(已截断)';
|
||||
return s.slice(0, maxLen) + '...(' + _t('infoCollect.truncated') + ')';
|
||||
}
|
||||
|
||||
function formatFofaRowSummary(row, fields) {
|
||||
@@ -707,7 +710,7 @@ function scanFofaRow(encodedRowJson, clickEvent) {
|
||||
const fields = (document.getElementById('fofa-fields')?.value || '').split(',').map(s => s.trim()).filter(Boolean);
|
||||
const target = inferTargetFromRow(row, fields);
|
||||
if (!target) {
|
||||
alert('无法从该行推断扫描目标(建议在 fields 中包含 host/ip/port/domain)');
|
||||
alert(_t('infoCollect.cannotInferTarget'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -745,10 +748,10 @@ function scanFofaRow(encodedRowJson, clickEvent) {
|
||||
if (typeof sendMessage === 'function') {
|
||||
sendMessage();
|
||||
} else {
|
||||
alert('未找到 sendMessage(),请刷新页面后重试');
|
||||
alert(_t('infoCollect.noSendMessage'));
|
||||
}
|
||||
} else {
|
||||
showInlineToast('已填入对话输入框,可编辑后发送');
|
||||
showInlineToast(_t('infoCollect.filledToInput'));
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
@@ -910,7 +913,7 @@ function hideAllFofaColumns() {
|
||||
function exportFofaResults(format) {
|
||||
const p = infoCollectState.currentPayload;
|
||||
if (!p || !Array.isArray(p.results) || p.results.length === 0) {
|
||||
alert('暂无可导出的结果');
|
||||
alert(_t('infoCollect.noExportResult'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -936,7 +939,7 @@ function exportFofaResults(format) {
|
||||
if (format === 'xlsx') {
|
||||
// 使用 SheetJS 生成 XLSX(需在页面中引入 xlsx 库)
|
||||
if (typeof XLSX === 'undefined') {
|
||||
alert('未加载 XLSX 库,请刷新页面后重试');
|
||||
alert(_t('infoCollect.xlsxNotLoaded'));
|
||||
return;
|
||||
}
|
||||
const aoa = [visibleFields].concat(p.results.map(row => {
|
||||
@@ -945,7 +948,7 @@ function exportFofaResults(format) {
|
||||
}));
|
||||
const ws = XLSX.utils.aoa_to_sheet(aoa);
|
||||
const wb = XLSX.utils.book_new();
|
||||
XLSX.utils.book_append_sheet(wb, ws, 'FOFA结果');
|
||||
XLSX.utils.book_append_sheet(wb, ws, _t('infoCollect.batchScanTitle'));
|
||||
XLSX.writeFile(wb, `fofa_results_${ts}.xlsx`);
|
||||
return;
|
||||
}
|
||||
@@ -982,12 +985,12 @@ function downloadBlob(content, filename, mime) {
|
||||
async function batchScanSelectedFofaRows() {
|
||||
const p = infoCollectState.currentPayload;
|
||||
if (!p || !Array.isArray(p.results) || p.results.length === 0) {
|
||||
alert('暂无结果');
|
||||
alert(_t('infoCollect.noResults'));
|
||||
return;
|
||||
}
|
||||
const selected = Array.from(infoCollectState.selectedRowIndexes).sort((a, b) => a - b);
|
||||
if (selected.length === 0) {
|
||||
alert('请先勾选需要扫描的行');
|
||||
alert(_t('infoCollect.selectRowsFirst'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1009,11 +1012,11 @@ async function batchScanSelectedFofaRows() {
|
||||
});
|
||||
|
||||
if (tasks.length === 0) {
|
||||
alert('未能从所选行推断任何可扫描目标(建议 fields 中包含 host/ip/port/domain)');
|
||||
alert(_t('infoCollect.noScanTarget'));
|
||||
return;
|
||||
}
|
||||
|
||||
const title = (p.query ? `FOFA 批量扫描:${p.query}` : 'FOFA 批量扫描').slice(0, 80);
|
||||
const title = (p.query ? _t('infoCollect.batchScanTitle') + ':' + p.query : _t('infoCollect.batchScanTitle')).slice(0, 80);
|
||||
try {
|
||||
// 不强制切换到“信息收集”角色:沿用当前已选角色;若为默认则传空字符串交给后端走默认逻辑
|
||||
let role = '';
|
||||
@@ -1029,7 +1032,7 @@ async function batchScanSelectedFofaRows() {
|
||||
});
|
||||
const result = await resp.json().catch(() => ({}));
|
||||
if (!resp.ok) {
|
||||
throw new Error(result.error || `创建批量队列失败: ${resp.status}`);
|
||||
throw new Error(result.error || _t('infoCollect.createQueueFailed') + ': ' + resp.status);
|
||||
}
|
||||
const queueId = result.queueId;
|
||||
if (!queueId) {
|
||||
@@ -1045,13 +1048,13 @@ async function batchScanSelectedFofaRows() {
|
||||
}, 250);
|
||||
|
||||
if (skipped.length > 0) {
|
||||
showInlineToast(`已创建队列(跳过 ${skipped.length} 条无目标行)`);
|
||||
showInlineToast(_t('infoCollect.queueCreatedSkipped', { n: skipped.length }));
|
||||
} else {
|
||||
showInlineToast('已创建批量扫描队列');
|
||||
showInlineToast(_t('infoCollect.batchQueueCreated'));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('批量扫描失败:', e);
|
||||
alert('批量扫描失败: ' + (e && e.message ? e.message : String(e)));
|
||||
alert(_t('infoCollect.batchScanFailed') + ': ' + (e && e.message ? e.message : String(e)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1065,8 +1068,8 @@ function showCellDetailModal(field, fullText) {
|
||||
modal.innerHTML = `
|
||||
<div class="info-collect-cell-modal-content" role="dialog" aria-modal="true">
|
||||
<div class="info-collect-cell-modal-header">
|
||||
<div class="info-collect-cell-modal-title">${escapeHtml(field || '字段')}</div>
|
||||
<button class="btn-icon" type="button" id="info-collect-cell-modal-close" title="关闭">
|
||||
<div class="info-collect-cell-modal-title">${escapeHtml(field || _t('infoCollect.field'))}</div>
|
||||
<button class="btn-icon" type="button" id="info-collect-cell-modal-close" title="${_t('common.close')}">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
@@ -1076,8 +1079,8 @@ function showCellDetailModal(field, fullText) {
|
||||
<pre class="info-collect-cell-modal-pre">${escapeHtml(fullText || '')}</pre>
|
||||
</div>
|
||||
<div class="info-collect-cell-modal-footer">
|
||||
<button class="btn-secondary" type="button" id="info-collect-cell-modal-copy">复制</button>
|
||||
<button class="btn-primary" type="button" id="info-collect-cell-modal-ok">关闭</button>
|
||||
<button class="btn-secondary" type="button" id="info-collect-cell-modal-copy">${_t('common.copy')}</button>
|
||||
<button class="btn-primary" type="button" id="info-collect-cell-modal-ok">${_t('common.close')}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1091,7 +1094,7 @@ function showCellDetailModal(field, fullText) {
|
||||
document.getElementById('info-collect-cell-modal-close')?.addEventListener('click', close);
|
||||
document.getElementById('info-collect-cell-modal-ok')?.addEventListener('click', close);
|
||||
document.getElementById('info-collect-cell-modal-copy')?.addEventListener('click', () => {
|
||||
navigator.clipboard.writeText(fullText || '').then(() => showInlineToast('已复制')).catch(() => alert('复制失败'));
|
||||
navigator.clipboard.writeText(fullText || '').then(() => showInlineToast(_t('common.copied'))).catch(() => alert(_t('common.copyFailed')));
|
||||
});
|
||||
|
||||
// Esc 关闭
|
||||
@@ -1122,3 +1125,13 @@ window.toggleFofaColumn = toggleFofaColumn;
|
||||
window.exportFofaResults = exportFofaResults;
|
||||
window.batchScanSelectedFofaRows = batchScanSelectedFofaRows;
|
||||
|
||||
document.addEventListener('languagechange', function () {
|
||||
updateSelectedMeta();
|
||||
});
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', function () { updateSelectedMeta(); });
|
||||
} else {
|
||||
updateSelectedMeta();
|
||||
}
|
||||
|
||||
|
||||
+108
-102
@@ -1,4 +1,37 @@
|
||||
// 知识库管理相关功能
|
||||
function _t(key, opts) {
|
||||
return typeof window.t === 'function' ? window.t(key, opts) : key;
|
||||
}
|
||||
|
||||
// 返回「知识库未启用」提示区块的 HTML(使用 data-i18n 以便语言切换时自动更新)
|
||||
function getKnowledgeNotEnabledHTML() {
|
||||
return `
|
||||
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
|
||||
<h3 data-i18n="knowledge.notEnabledTitle" style="margin-bottom: 10px; color: #666;"></h3>
|
||||
<p data-i18n="knowledge.notEnabledHint" style="color: #999; margin-bottom: 20px;"></p>
|
||||
<button data-i18n="knowledge.goToSettings" onclick="switchToSettings()" style="
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
"></button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 渲染「知识库未启用」状态到容器,并应用当前语言
|
||||
function renderKnowledgeNotEnabledState(container) {
|
||||
if (!container) return;
|
||||
container.innerHTML = getKnowledgeNotEnabledHTML();
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(container);
|
||||
}
|
||||
}
|
||||
|
||||
let knowledgeCategories = [];
|
||||
let knowledgeItems = [];
|
||||
let currentEditingItemId = null;
|
||||
@@ -32,26 +65,8 @@ async function loadKnowledgeCategories() {
|
||||
|
||||
// 检查知识库功能是否启用
|
||||
if (data.enabled === false) {
|
||||
// 功能未启用,显示友好提示
|
||||
const container = document.getElementById('knowledge-items-list');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
|
||||
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
|
||||
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
|
||||
<button onclick="switchToSettings()" style="
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
">前往设置</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
// 功能未启用,显示友好提示(使用 data-i18n,切换语言时会自动更新)
|
||||
renderKnowledgeNotEnabledState(document.getElementById('knowledge-items-list'));
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -116,25 +131,10 @@ async function loadKnowledgeItems(category = '', page = 1, pageSize = 10) {
|
||||
|
||||
// 检查知识库功能是否启用
|
||||
if (data.enabled === false) {
|
||||
// 功能未启用,显示友好提示(如果还没有显示的话)
|
||||
// 功能未启用,显示友好提示(如果还没有显示的话;使用 data-i18n,切换语言时会自动更新)
|
||||
const container = document.getElementById('knowledge-items-list');
|
||||
if (container && !container.querySelector('.empty-state')) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
|
||||
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
|
||||
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
|
||||
<button onclick="switchToSettings()" style="
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
">前往设置</button>
|
||||
</div>
|
||||
`;
|
||||
renderKnowledgeNotEnabledState(container);
|
||||
}
|
||||
knowledgeItems = [];
|
||||
knowledgePagination.total = 0;
|
||||
@@ -753,25 +753,7 @@ async function searchKnowledgeItems() {
|
||||
|
||||
// 检查知识库功能是否启用
|
||||
if (data.enabled === false) {
|
||||
const container = document.getElementById('knowledge-items-list');
|
||||
if (container) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state" style="text-align: center; padding: 40px 20px;">
|
||||
<div style="font-size: 48px; margin-bottom: 20px;">📚</div>
|
||||
<h3 style="margin-bottom: 10px; color: #666;">知识库功能未启用</h3>
|
||||
<p style="color: #999; margin-bottom: 20px;">${data.message || '请前往系统设置启用知识检索功能'}</p>
|
||||
<button onclick="switchToSettings()" style="
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
">前往设置</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
renderKnowledgeNotEnabledState(document.getElementById('knowledge-items-list'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1312,7 +1294,7 @@ async function loadRetrievalLogs(conversationId = '', messageId = '') {
|
||||
renderRetrievalLogs([]);
|
||||
// 只在非空筛选条件下才显示错误通知(避免在没有数据时显示错误)
|
||||
if (conversationId || messageId) {
|
||||
showNotification('加载检索日志失败: ' + error.message, 'error');
|
||||
showNotification(_t('retrievalLogs.loadError') + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1326,7 +1308,7 @@ function renderRetrievalLogs(logs) {
|
||||
updateRetrievalStats(logs);
|
||||
|
||||
if (logs.length === 0) {
|
||||
container.innerHTML = '<div class="empty-state">暂无检索记录</div>';
|
||||
container.innerHTML = '<div class="empty-state">' + _t('retrievalLogs.noRecords') + '</div>';
|
||||
retrievalLogsData = [];
|
||||
return;
|
||||
}
|
||||
@@ -1386,7 +1368,7 @@ function renderRetrievalLogs(logs) {
|
||||
</div>
|
||||
<div class="retrieval-log-main-info">
|
||||
<div class="retrieval-log-query">
|
||||
${escapeHtml(log.query || '无查询内容')}
|
||||
${escapeHtml(log.query || _t('retrievalLogs.noQuery'))}
|
||||
</div>
|
||||
<div class="retrieval-log-meta">
|
||||
<span class="retrieval-log-time" title="${formatTime(log.createdAt)}">
|
||||
@@ -1396,33 +1378,33 @@ function renderRetrievalLogs(logs) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="retrieval-log-result-badge ${hasResults ? 'success' : 'empty'}">
|
||||
${hasResults ? (itemCount > 0 ? `${itemCount} 项` : '有结果') : '无结果'}
|
||||
${hasResults ? (itemCount > 0 ? itemCount + ' ' + _t('retrievalLogs.itemsUnit') : _t('retrievalLogs.hasResults')) : _t('retrievalLogs.noResults')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="retrieval-log-card-body">
|
||||
<div class="retrieval-log-details-grid">
|
||||
${log.conversationId ? `
|
||||
<div class="retrieval-log-detail-item">
|
||||
<span class="detail-label">对话ID</span>
|
||||
<code class="detail-value" title="点击复制" onclick="navigator.clipboard.writeText('${escapeHtml(log.conversationId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);" style="cursor: pointer;">${escapeHtml(log.conversationId)}</code>
|
||||
<span class="detail-label">${_t('retrievalLogs.conversationId')}</span>
|
||||
<code class="detail-value" title="${_t('retrievalLogs.clickToCopy')}" data-copy-title-copied="${_t('common.copied')}" data-copy-title-click="${_t('retrievalLogs.clickToCopy')}" onclick="var t=this; navigator.clipboard.writeText('${escapeHtml(log.conversationId)}').then(function(){ t.title=t.getAttribute('data-copy-title-copied')||'Copied!'; setTimeout(function(){ t.title=t.getAttribute('data-copy-title-click')||'Click to copy'; }, 2000); });" style="cursor: pointer;">${escapeHtml(log.conversationId)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
${log.messageId ? `
|
||||
<div class="retrieval-log-detail-item">
|
||||
<span class="detail-label">消息ID</span>
|
||||
<code class="detail-value" title="点击复制" onclick="navigator.clipboard.writeText('${escapeHtml(log.messageId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);" style="cursor: pointer;">${escapeHtml(log.messageId)}</code>
|
||||
<span class="detail-label">${_t('retrievalLogs.messageId')}</span>
|
||||
<code class="detail-value" title="${_t('retrievalLogs.clickToCopy')}" data-copy-title-copied="${_t('common.copied')}" data-copy-title-click="${_t('retrievalLogs.clickToCopy')}" onclick="var el=this; navigator.clipboard.writeText('${escapeHtml(log.messageId)}').then(function(){ el.title=el.getAttribute('data-copy-title-copied')||el.title; setTimeout(function(){ el.title=el.getAttribute('data-copy-title-click')||el.title; }, 2000); });" style="cursor: pointer;">${escapeHtml(log.messageId)}</code>
|
||||
</div>
|
||||
` : ''}
|
||||
<div class="retrieval-log-detail-item">
|
||||
<span class="detail-label">检索结果</span>
|
||||
<span class="detail-label">${_t('retrievalLogs.retrievalResult')}</span>
|
||||
<span class="detail-value ${hasResults ? 'text-success' : 'text-muted'}">
|
||||
${hasResults ? (itemCount > 0 ? `找到 ${itemCount} 个相关知识项` : '找到相关知识项(数量未知)') : '未找到匹配的知识项'}
|
||||
${hasResults ? (itemCount > 0 ? _t('retrievalLogs.foundCount', { count: itemCount }) : _t('retrievalLogs.foundUnknown')) : _t('retrievalLogs.noMatch')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
${hasResults && log.retrievedItems && log.retrievedItems.length > 0 ? `
|
||||
<div class="retrieval-log-items-preview">
|
||||
<div class="retrieval-log-items-label">检索到的知识项:</div>
|
||||
<div class="retrieval-log-items-label">${_t('retrievalLogs.retrievedItemsLabel')}</div>
|
||||
<div class="retrieval-log-items-list">
|
||||
${log.retrievedItems.slice(0, 3).map((itemId, idx) => `
|
||||
<span class="retrieval-log-item-tag">${idx + 1}</span>
|
||||
@@ -1437,13 +1419,13 @@ function renderRetrievalLogs(logs) {
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
查看详情
|
||||
${_t('retrievalLogs.viewDetails')}
|
||||
</button>
|
||||
<button class="btn-secondary btn-sm retrieval-log-delete-btn" onclick="deleteRetrievalLog('${escapeHtml(log.id)}', ${index})" style="margin-top: 12px; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: var(--error-color, #dc3545); border-color: var(--error-color, #dc3545);" onmouseover="this.style.backgroundColor='rgba(220, 53, 69, 0.1)'; this.style.color='#dc3545';" onmouseout="this.style.backgroundColor=''; this.style.color='var(--error-color, #dc3545)';" title="删除">
|
||||
<button class="btn-secondary btn-sm retrieval-log-delete-btn" onclick="deleteRetrievalLog('${escapeHtml(log.id)}', ${index})" style="margin-top: 12px; margin-left: 8px; display: inline-flex; align-items: center; gap: 4px; color: var(--error-color, #dc3545); border-color: var(--error-color, #dc3545);" onmouseover="this.style.backgroundColor='rgba(220, 53, 69, 0.1)'; this.style.color='#dc3545';" onmouseout="this.style.backgroundColor=''; this.style.color='var(--error-color, #dc3545)';" title="${_t('common.delete')}">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
删除
|
||||
${_t('common.delete')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1480,22 +1462,25 @@ function updateRetrievalStats(logs) {
|
||||
|
||||
statsContainer.innerHTML = `
|
||||
<div class="retrieval-stat-item">
|
||||
<span class="retrieval-stat-label">总检索次数</span>
|
||||
<span class="retrieval-stat-label" data-i18n="retrievalLogs.totalRetrievals">总检索次数</span>
|
||||
<span class="retrieval-stat-value">${totalLogs}</span>
|
||||
</div>
|
||||
<div class="retrieval-stat-item">
|
||||
<span class="retrieval-stat-label">成功检索</span>
|
||||
<span class="retrieval-stat-label" data-i18n="retrievalLogs.successRetrievals">成功检索</span>
|
||||
<span class="retrieval-stat-value text-success">${successfulLogs}</span>
|
||||
</div>
|
||||
<div class="retrieval-stat-item">
|
||||
<span class="retrieval-stat-label">成功率</span>
|
||||
<span class="retrieval-stat-label" data-i18n="retrievalLogs.successRate">成功率</span>
|
||||
<span class="retrieval-stat-value">${successRate}%</span>
|
||||
</div>
|
||||
<div class="retrieval-stat-item">
|
||||
<span class="retrieval-stat-label">检索到知识项</span>
|
||||
<span class="retrieval-stat-label" data-i18n="retrievalLogs.retrievedItems">检索到知识项</span>
|
||||
<span class="retrieval-stat-value">${totalItems}</span>
|
||||
</div>
|
||||
`;
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(statsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取相对时间
|
||||
@@ -1591,7 +1576,7 @@ function refreshRetrievalLogs() {
|
||||
|
||||
// 删除检索日志
|
||||
async function deleteRetrievalLog(id, index) {
|
||||
if (!confirm('确定要删除这条检索记录吗?')) {
|
||||
if (!confirm(_t('retrievalLogs.deleteConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1677,7 +1662,7 @@ async function deleteRetrievalLog(id, index) {
|
||||
}
|
||||
}
|
||||
|
||||
showNotification('❌ 删除检索日志失败: ' + error.message, 'error');
|
||||
showNotification(_t('retrievalLogs.deleteError') + ': ' + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1699,12 +1684,11 @@ function updateRetrievalStatsAfterDelete() {
|
||||
const badge = card.querySelector('.retrieval-log-result-badge');
|
||||
if (badge && badge.classList.contains('success')) {
|
||||
const text = badge.textContent.trim();
|
||||
const match = text.match(/(\d+)\s*项/);
|
||||
const match = text.match(/(\d+)/);
|
||||
if (match) {
|
||||
return sum + parseInt(match[1]);
|
||||
} else if (text === '有结果') {
|
||||
return sum + 1; // 简化处理,假设为1
|
||||
return sum + parseInt(match[1], 10);
|
||||
}
|
||||
return sum + 1; // 有结果但数量未知(如 "Has results" / "有结果")
|
||||
}
|
||||
return sum;
|
||||
}, 0);
|
||||
@@ -1713,28 +1697,31 @@ function updateRetrievalStatsAfterDelete() {
|
||||
|
||||
statsContainer.innerHTML = `
|
||||
<div class="retrieval-stat-item">
|
||||
<span class="retrieval-stat-label">总检索次数</span>
|
||||
<span class="retrieval-stat-label" data-i18n="retrievalLogs.totalRetrievals">总检索次数</span>
|
||||
<span class="retrieval-stat-value">${totalLogs}</span>
|
||||
</div>
|
||||
<div class="retrieval-stat-item">
|
||||
<span class="retrieval-stat-label">成功检索</span>
|
||||
<span class="retrieval-stat-label" data-i18n="retrievalLogs.successRetrievals">成功检索</span>
|
||||
<span class="retrieval-stat-value text-success">${successfulLogs}</span>
|
||||
</div>
|
||||
<div class="retrieval-stat-item">
|
||||
<span class="retrieval-stat-label">成功率</span>
|
||||
<span class="retrieval-stat-label" data-i18n="retrievalLogs.successRate">成功率</span>
|
||||
<span class="retrieval-stat-value">${successRate}%</span>
|
||||
</div>
|
||||
<div class="retrieval-stat-item">
|
||||
<span class="retrieval-stat-label">检索到知识项</span>
|
||||
<span class="retrieval-stat-label" data-i18n="retrievalLogs.retrievedItems">检索到知识项</span>
|
||||
<span class="retrieval-stat-value">${totalItems}</span>
|
||||
</div>
|
||||
`;
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(statsContainer);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示检索日志详情
|
||||
async function showRetrievalLogDetails(index) {
|
||||
if (!retrievalLogsData || index < 0 || index >= retrievalLogsData.length) {
|
||||
showNotification('无法获取检索详情', 'error');
|
||||
showNotification(_t('retrievalLogs.detailError'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1783,16 +1770,19 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="max-width: 900px; max-height: 90vh; overflow-y: auto;">
|
||||
<div class="modal-header">
|
||||
<h2>检索详情</h2>
|
||||
<h2 data-i18n="retrievalLogs.detailsTitle">检索详情</h2>
|
||||
<span class="modal-close" onclick="closeRetrievalLogDetailsModal()">×</span>
|
||||
</div>
|
||||
<div class="modal-body" id="retrieval-log-details-content">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn-secondary" onclick="closeRetrievalLogDetailsModal()">关闭</button>
|
||||
<button class="btn-secondary" onclick="closeRetrievalLogDetailsModal()" data-i18n="common.close">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(modal);
|
||||
}
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
@@ -1816,57 +1806,57 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
|
||||
return `
|
||||
<div class="retrieval-detail-item-card" style="margin-bottom: 16px; padding: 16px; border: 1px solid var(--border-color); border-radius: 8px; background: var(--bg-secondary);">
|
||||
<div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;">
|
||||
<h4 style="margin: 0; color: var(--text-primary);">${idx + 1}. ${escapeHtml(item.title || '未命名')}</h4>
|
||||
<span style="font-size: 0.875rem; color: var(--text-secondary);">${escapeHtml(item.category || '未分类')}</span>
|
||||
<h4 style="margin: 0; color: var(--text-primary);">${idx + 1}. ${escapeHtml(item.title || _t('retrievalLogs.untitled'))}</h4>
|
||||
<span style="font-size: 0.875rem; color: var(--text-secondary);">${escapeHtml(item.category || _t('retrievalLogs.uncategorized'))}</span>
|
||||
</div>
|
||||
${item.filePath ? `<div style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 8px;">📁 ${escapeHtml(item.filePath)}</div>` : ''}
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); line-height: 1.6;">
|
||||
${escapeHtml(previewText || '无内容预览')}
|
||||
${escapeHtml(previewText || _t('retrievalLogs.noContentPreview'))}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
} else {
|
||||
itemsHtml = '<div style="padding: 16px; text-align: center; color: var(--text-muted);">未找到知识项详情</div>';
|
||||
itemsHtml = '<div style="padding: 16px; text-align: center; color: var(--text-muted);">' + _t('retrievalLogs.noItemDetails') + '</div>';
|
||||
}
|
||||
|
||||
content.innerHTML = `
|
||||
<div style="display: flex; flex-direction: column; gap: 20px;">
|
||||
<div class="retrieval-detail-section">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">查询信息</h3>
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">${_t('retrievalLogs.queryInfo')}</h3>
|
||||
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px; border-left: 3px solid var(--accent-color);">
|
||||
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">查询内容:</div>
|
||||
<div style="color: var(--text-primary); line-height: 1.6; word-break: break-word;">${escapeHtml(log.query || '无查询内容')}</div>
|
||||
<div style="font-weight: 500; margin-bottom: 8px; color: var(--text-primary);">${_t('retrievalLogs.queryContent')}</div>
|
||||
<div style="color: var(--text-primary); line-height: 1.6; word-break: break-word;">${escapeHtml(log.query || _t('retrievalLogs.noQuery'))}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="retrieval-detail-section">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">检索信息</h3>
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">${_t('retrievalLogs.retrievalInfo')}</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
||||
${log.riskType ? `
|
||||
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">风险类型</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.riskType')}</div>
|
||||
<div style="font-weight: 500; color: var(--text-primary);">${escapeHtml(log.riskType)}</div>
|
||||
</div>
|
||||
` : ''}
|
||||
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">检索时间</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.retrievalTime')}</div>
|
||||
<div style="font-weight: 500; color: var(--text-primary);" title="${fullTime}">${timeAgo}</div>
|
||||
</div>
|
||||
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">检索结果</div>
|
||||
<div style="font-weight: 500; color: var(--text-primary);">${retrievedItems.length} 个知识项</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.retrievalResult')}</div>
|
||||
<div style="font-weight: 500; color: var(--text-primary);">${_t('retrievalLogs.itemsCount', { count: retrievedItems.length })}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
${log.conversationId || log.messageId ? `
|
||||
<div class="retrieval-detail-section">
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">关联信息</h3>
|
||||
<h3 style="margin: 0 0 12px 0; font-size: 1.125rem; color: var(--text-primary);">${_t('retrievalLogs.relatedInfo')}</h3>
|
||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px;">
|
||||
${log.conversationId ? `
|
||||
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">对话ID</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.conversationId')}</div>
|
||||
<code style="font-size: 0.8125rem; color: var(--text-primary); word-break: break-all; cursor: pointer;"
|
||||
onclick="navigator.clipboard.writeText('${escapeHtml(log.conversationId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
|
||||
title="点击复制">${escapeHtml(log.conversationId)}</code>
|
||||
@@ -1874,7 +1864,7 @@ function showRetrievalLogDetailsModal(log, retrievedItems) {
|
||||
` : ''}
|
||||
${log.messageId ? `
|
||||
<div style="padding: 12px; background: var(--bg-secondary); border-radius: 6px;">
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">消息ID</div>
|
||||
<div style="font-size: 0.875rem; color: var(--text-secondary); margin-bottom: 4px;">${_t('retrievalLogs.messageId')}</div>
|
||||
<code style="font-size: 0.8125rem; color: var(--text-primary); word-break: break-all; cursor: pointer;"
|
||||
onclick="navigator.clipboard.writeText('${escapeHtml(log.messageId)}'); this.title='已复制!'; setTimeout(() => this.title='点击复制', 2000);"
|
||||
title="点击复制">${escapeHtml(log.messageId)}</code>
|
||||
@@ -1910,6 +1900,22 @@ window.addEventListener('click', function(event) {
|
||||
}
|
||||
});
|
||||
|
||||
// 语言切换时重新渲染检索历史列表与统计,使动态内容随语言更新;知识管理页的「未启用」区块已使用 data-i18n,会由 applyTranslations(document) 自动更新
|
||||
document.addEventListener('languagechange', function () {
|
||||
var cur = typeof window.currentPage === 'function' ? window.currentPage() : (window.currentPage || '');
|
||||
if (cur === 'knowledge-retrieval-logs') {
|
||||
if (retrievalLogsData && retrievalLogsData.length >= 0) {
|
||||
renderRetrievalLogs(retrievalLogsData);
|
||||
}
|
||||
} else if (cur === 'knowledge-management') {
|
||||
// 仅对「知识库未启用」状态:已有 data-i18n,applyTranslations 已处理;此处可选地重新应用一次以兼容旧 DOM
|
||||
var listEl = document.getElementById('knowledge-items-list');
|
||||
if (listEl && typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(listEl);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 页面切换时加载数据
|
||||
if (typeof switchPage === 'function') {
|
||||
const originalSwitchPage = switchPage;
|
||||
|
||||
+102
-57
@@ -1138,10 +1138,10 @@ async function refreshMonitorPanel(page = null) {
|
||||
} catch (error) {
|
||||
console.error('刷新监控面板失败:', error);
|
||||
if (statsContainer) {
|
||||
statsContainer.innerHTML = `<div class="monitor-error">无法加载统计信息:${escapeHtml(error.message)}</div>`;
|
||||
statsContainer.innerHTML = `<div class="monitor-error">${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
if (execContainer) {
|
||||
execContainer.innerHTML = `<div class="monitor-error">无法加载执行记录:${escapeHtml(error.message)}</div>`;
|
||||
execContainer.innerHTML = `<div class="monitor-error">${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadExecutionsError') : '无法加载执行记录')}:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1215,10 +1215,10 @@ async function refreshMonitorPanelWithFilter(statusFilter = 'all', toolFilter =
|
||||
} catch (error) {
|
||||
console.error('刷新监控面板失败:', error);
|
||||
if (statsContainer) {
|
||||
statsContainer.innerHTML = `<div class="monitor-error">无法加载统计信息:${escapeHtml(error.message)}</div>`;
|
||||
statsContainer.innerHTML = `<div class="monitor-error">${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadStatsError') : '无法加载统计信息')}:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
if (execContainer) {
|
||||
execContainer.innerHTML = `<div class="monitor-error">无法加载执行记录:${escapeHtml(error.message)}</div>`;
|
||||
execContainer.innerHTML = `<div class="monitor-error">${escapeHtml(typeof window.t === 'function' ? window.t('mcpMonitor.loadExecutionsError') : '无法加载执行记录')}:${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1232,7 +1232,8 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
|
||||
|
||||
const entries = Object.values(statsMap);
|
||||
if (entries.length === 0) {
|
||||
container.innerHTML = '<div class="monitor-empty">暂无统计数据</div>';
|
||||
const noStats = typeof window.t === 'function' ? window.t('mcpMonitor.noStatsData') : '暂无统计数据';
|
||||
container.innerHTML = '<div class="monitor-empty">' + escapeHtml(noStats) + '</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1252,24 +1253,32 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
|
||||
);
|
||||
|
||||
const successRate = totals.total > 0 ? ((totals.success / totals.total) * 100).toFixed(1) : '0.0';
|
||||
const lastUpdatedText = lastFetchedAt ? lastFetchedAt.toLocaleString('zh-CN') : 'N/A';
|
||||
const lastCallText = totals.lastCallTime ? totals.lastCallTime.toLocaleString('zh-CN') : '暂无调用';
|
||||
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined;
|
||||
const lastUpdatedText = lastFetchedAt ? (lastFetchedAt.toLocaleString ? lastFetchedAt.toLocaleString(locale || 'en-US') : String(lastFetchedAt)) : 'N/A';
|
||||
const noCallsYet = typeof window.t === 'function' ? window.t('mcpMonitor.noCallsYet') : '暂无调用';
|
||||
const lastCallText = totals.lastCallTime ? (totals.lastCallTime.toLocaleString ? totals.lastCallTime.toLocaleString(locale || 'en-US') : String(totals.lastCallTime)) : noCallsYet;
|
||||
const totalCallsLabel = typeof window.t === 'function' ? window.t('mcpMonitor.totalCalls') : '总调用次数';
|
||||
const successFailedLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successFailed', { success: totals.success, failed: totals.failed }) : `成功 ${totals.success} / 失败 ${totals.failed}`;
|
||||
const successRateLabel = typeof window.t === 'function' ? window.t('mcpMonitor.successRate') : '成功率';
|
||||
const statsFromAll = typeof window.t === 'function' ? window.t('mcpMonitor.statsFromAllTools') : '统计自全部工具调用';
|
||||
const lastCallLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastCall') : '最近一次调用';
|
||||
const lastRefreshLabel = typeof window.t === 'function' ? window.t('mcpMonitor.lastRefreshTime') : '最后刷新时间';
|
||||
|
||||
let html = `
|
||||
<div class="monitor-stat-card">
|
||||
<h4>总调用次数</h4>
|
||||
<h4>${escapeHtml(totalCallsLabel)}</h4>
|
||||
<div class="monitor-stat-value">${totals.total}</div>
|
||||
<div class="monitor-stat-meta">成功 ${totals.success} / 失败 ${totals.failed}</div>
|
||||
<div class="monitor-stat-meta">${escapeHtml(successFailedLabel)}</div>
|
||||
</div>
|
||||
<div class="monitor-stat-card">
|
||||
<h4>成功率</h4>
|
||||
<h4>${escapeHtml(successRateLabel)}</h4>
|
||||
<div class="monitor-stat-value">${successRate}%</div>
|
||||
<div class="monitor-stat-meta">统计自全部工具调用</div>
|
||||
<div class="monitor-stat-meta">${escapeHtml(statsFromAll)}</div>
|
||||
</div>
|
||||
<div class="monitor-stat-card">
|
||||
<h4>最近一次调用</h4>
|
||||
<div class="monitor-stat-value" style="font-size:1rem;">${lastCallText}</div>
|
||||
<div class="monitor-stat-meta">最后刷新时间:${lastUpdatedText}</div>
|
||||
<h4>${escapeHtml(lastCallLabel)}</h4>
|
||||
<div class="monitor-stat-value" style="font-size:1rem;">${escapeHtml(lastCallText)}</div>
|
||||
<div class="monitor-stat-meta">${escapeHtml(lastRefreshLabel)}:${escapeHtml(lastUpdatedText)}</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1280,14 +1289,16 @@ function renderMonitorStats(statsMap = {}, lastFetchedAt = null) {
|
||||
.sort((a, b) => (b.totalCalls || 0) - (a.totalCalls || 0))
|
||||
.slice(0, 4);
|
||||
|
||||
const unknownToolLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknownTool') : '未知工具';
|
||||
topTools.forEach(tool => {
|
||||
const toolSuccessRate = tool.totalCalls > 0 ? ((tool.successCalls || 0) / tool.totalCalls * 100).toFixed(1) : '0.0';
|
||||
const toolMeta = typeof window.t === 'function' ? window.t('mcpMonitor.successFailedRate', { success: tool.successCalls || 0, failed: tool.failedCalls || 0, rate: toolSuccessRate }) : `成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%`;
|
||||
html += `
|
||||
<div class="monitor-stat-card">
|
||||
<h4>${escapeHtml(tool.toolName || '未知工具')}</h4>
|
||||
<h4>${escapeHtml(tool.toolName || unknownToolLabel)}</h4>
|
||||
<div class="monitor-stat-value">${tool.totalCalls || 0}</div>
|
||||
<div class="monitor-stat-meta">
|
||||
成功 ${tool.successCalls || 0} / 失败 ${tool.failedCalls || 0} · 成功率 ${toolSuccessRate}%
|
||||
${escapeHtml(toolMeta)}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1307,10 +1318,12 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
const toolFilter = document.getElementById('monitor-tool-filter');
|
||||
const currentToolFilter = toolFilter ? toolFilter.value : 'all';
|
||||
const hasFilter = (statusFilter && statusFilter !== 'all') || (currentToolFilter && currentToolFilter !== 'all');
|
||||
const noRecordsFilter = typeof window.t === 'function' ? window.t('mcpMonitor.noRecordsWithFilter') : '当前筛选条件下暂无记录';
|
||||
const noExecutions = typeof window.t === 'function' ? window.t('mcpMonitor.noExecutions') : '暂无执行记录';
|
||||
if (hasFilter) {
|
||||
container.innerHTML = '<div class="monitor-empty">当前筛选条件下暂无记录</div>';
|
||||
container.innerHTML = '<div class="monitor-empty">' + escapeHtml(noRecordsFilter) + '</div>';
|
||||
} else {
|
||||
container.innerHTML = '<div class="monitor-empty">暂无执行记录</div>';
|
||||
container.innerHTML = '<div class="monitor-empty">' + escapeHtml(noExecutions) + '</div>';
|
||||
}
|
||||
// 隐藏批量操作栏
|
||||
const batchActions = document.getElementById('monitor-batch-actions');
|
||||
@@ -1322,14 +1335,22 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
|
||||
// 由于筛选已经在后端完成,这里直接使用所有传入的执行记录
|
||||
// 不再需要前端再次筛选,因为后端已经返回了筛选后的数据
|
||||
const unknownLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknown') : '未知';
|
||||
const unknownToolLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknownTool') : '未知工具';
|
||||
const viewDetailLabel = typeof window.t === 'function' ? window.t('mcpMonitor.viewDetail') : '查看详情';
|
||||
const deleteLabel = typeof window.t === 'function' ? window.t('mcpMonitor.delete') : '删除';
|
||||
const deleteExecTitle = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecTitle') : '删除此执行记录';
|
||||
const statusKeyMap = { pending: 'statusPending', running: 'statusRunning', completed: 'statusCompleted', failed: 'statusFailed' };
|
||||
const locale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : undefined;
|
||||
const rows = executions
|
||||
.map(exec => {
|
||||
const status = (exec.status || 'unknown').toLowerCase();
|
||||
const statusClass = `monitor-status-chip ${status}`;
|
||||
const statusLabel = getStatusText(status);
|
||||
const startTime = exec.startTime ? new Date(exec.startTime).toLocaleString('zh-CN') : '未知';
|
||||
const statusKey = statusKeyMap[status];
|
||||
const statusLabel = (typeof window.t === 'function' && statusKey) ? window.t('mcpMonitor.' + statusKey) : getStatusText(status);
|
||||
const startTime = exec.startTime ? (new Date(exec.startTime).toLocaleString ? new Date(exec.startTime).toLocaleString(locale || 'en-US') : String(exec.startTime)) : unknownLabel;
|
||||
const duration = formatExecutionDuration(exec.startTime, exec.endTime);
|
||||
const toolName = escapeHtml(exec.toolName || '未知工具');
|
||||
const toolName = escapeHtml(exec.toolName || unknownToolLabel);
|
||||
const executionId = escapeHtml(exec.id || '');
|
||||
return `
|
||||
<tr>
|
||||
@@ -1337,13 +1358,13 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
<input type="checkbox" class="monitor-execution-checkbox" value="${executionId}" onchange="updateBatchActionsState()" />
|
||||
</td>
|
||||
<td>${toolName}</td>
|
||||
<td><span class="${statusClass}">${statusLabel}</span></td>
|
||||
<td>${startTime}</td>
|
||||
<td>${duration}</td>
|
||||
<td><span class="${statusClass}">${escapeHtml(statusLabel)}</span></td>
|
||||
<td>${escapeHtml(startTime)}</td>
|
||||
<td>${escapeHtml(duration)}</td>
|
||||
<td>
|
||||
<div class="monitor-execution-actions">
|
||||
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">查看详情</button>
|
||||
<button class="btn-secondary btn-delete" onclick="deleteExecution('${executionId}')" title="删除此执行记录">删除</button>
|
||||
<button class="btn-secondary" onclick="showMCPDetail('${executionId}')">${escapeHtml(viewDetailLabel)}</button>
|
||||
<button class="btn-secondary btn-delete" onclick="deleteExecution('${executionId}')" title="${escapeHtml(deleteExecTitle)}">${escapeHtml(deleteLabel)}</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -1365,6 +1386,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
// 创建表格容器
|
||||
const tableContainer = document.createElement('div');
|
||||
tableContainer.className = 'monitor-table-container';
|
||||
const colTool = typeof window.t === 'function' ? window.t('mcpMonitor.columnTool') : '工具';
|
||||
const colStatus = typeof window.t === 'function' ? window.t('mcpMonitor.columnStatus') : '状态';
|
||||
const colStartTime = typeof window.t === 'function' ? window.t('mcpMonitor.columnStartTime') : '开始时间';
|
||||
const colDuration = typeof window.t === 'function' ? window.t('mcpMonitor.columnDuration') : '耗时';
|
||||
const colActions = typeof window.t === 'function' ? window.t('mcpMonitor.columnActions') : '操作';
|
||||
tableContainer.innerHTML = `
|
||||
<table class="monitor-table">
|
||||
<thead>
|
||||
@@ -1372,11 +1398,11 @@ function renderMonitorExecutions(executions = [], statusFilter = 'all') {
|
||||
<th style="width: 40px;">
|
||||
<input type="checkbox" id="monitor-select-all" onchange="toggleSelectAll(this)" />
|
||||
</th>
|
||||
<th>工具</th>
|
||||
<th>状态</th>
|
||||
<th>开始时间</th>
|
||||
<th>耗时</th>
|
||||
<th>操作</th>
|
||||
<th>${escapeHtml(colTool)}</th>
|
||||
<th>${escapeHtml(colStatus)}</th>
|
||||
<th>${escapeHtml(colStartTime)}</th>
|
||||
<th>${escapeHtml(colDuration)}</th>
|
||||
<th>${escapeHtml(colActions)}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>${rows}</tbody>
|
||||
@@ -1415,12 +1441,18 @@ function renderMonitorPagination() {
|
||||
// 处理没有数据的情况
|
||||
const startItem = total === 0 ? 0 : (page - 1) * pageSize + 1;
|
||||
const endItem = total === 0 ? 0 : Math.min(page * pageSize, total);
|
||||
|
||||
const paginationInfoText = typeof window.t === 'function' ? window.t('mcpMonitor.paginationInfo', { start: startItem, end: endItem, total: total }) : `显示 ${startItem}-${endItem} / 共 ${total} 条记录`;
|
||||
const perPageLabel = typeof window.t === 'function' ? window.t('mcpMonitor.perPageLabel') : '每页显示';
|
||||
const firstPageLabel = typeof window.t === 'function' ? window.t('mcp.firstPage') : '首页';
|
||||
const prevPageLabel = typeof window.t === 'function' ? window.t('mcp.prevPage') : '上一页';
|
||||
const pageInfoText = typeof window.t === 'function' ? window.t('mcp.pageInfo', { page: page, total: totalPages || 1 }) : `第 ${page} / ${totalPages || 1} 页`;
|
||||
const nextPageLabel = typeof window.t === 'function' ? window.t('mcp.nextPage') : '下一页';
|
||||
const lastPageLabel = typeof window.t === 'function' ? window.t('mcp.lastPage') : '末页';
|
||||
pagination.innerHTML = `
|
||||
<div class="pagination-info">
|
||||
<span>显示 ${startItem}-${endItem} / 共 ${total} 条记录</span>
|
||||
<span>${escapeHtml(paginationInfoText)}</span>
|
||||
<label class="pagination-page-size">
|
||||
每页显示
|
||||
${escapeHtml(perPageLabel)}
|
||||
<select id="monitor-page-size" onchange="changeMonitorPageSize()">
|
||||
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
|
||||
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
|
||||
@@ -1430,11 +1462,11 @@ function renderMonitorPagination() {
|
||||
</label>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(1)" ${page === 1 || total === 0 ? 'disabled' : ''}>首页</button>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${page - 1})" ${page === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
|
||||
<span class="pagination-page">第 ${page} / ${totalPages || 1} 页</span>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${page + 1})" ${page >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${totalPages || 1})" ${page >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(1)" ${page === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(firstPageLabel)}</button>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${page - 1})" ${page === 1 || total === 0 ? 'disabled' : ''}>${escapeHtml(prevPageLabel)}</button>
|
||||
<span class="pagination-page">${escapeHtml(pageInfoText)}</span>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${page + 1})" ${page >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(nextPageLabel)}</button>
|
||||
<button class="btn-secondary" onclick="refreshMonitorPanel(${totalPages || 1})" ${page >= totalPages || total === 0 ? 'disabled' : ''}>${escapeHtml(lastPageLabel)}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1450,8 +1482,8 @@ async function deleteExecution(executionId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 确认删除
|
||||
if (!confirm('确定要删除此执行记录吗?此操作不可恢复。')) {
|
||||
const deleteConfirmMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecConfirmSingle') : '确定要删除此执行记录吗?此操作不可恢复。';
|
||||
if (!confirm(deleteConfirmMsg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1462,17 +1494,20 @@ async function deleteExecution(executionId) {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.error || '删除执行记录失败');
|
||||
const deleteFailedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecFailed') : '删除执行记录失败';
|
||||
throw new Error(error.error || deleteFailedMsg);
|
||||
}
|
||||
|
||||
// 删除成功后刷新当前页面
|
||||
const currentPage = monitorState.pagination.page;
|
||||
await refreshMonitorPanel(currentPage);
|
||||
|
||||
alert('执行记录已删除');
|
||||
const execDeletedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.execDeleted') : '执行记录已删除';
|
||||
alert(execDeletedMsg);
|
||||
} catch (error) {
|
||||
console.error('删除执行记录失败:', error);
|
||||
alert('删除执行记录失败: ' + error.message);
|
||||
const deleteFailedMsg = typeof window.t === 'function' ? window.t('mcpMonitor.deleteExecFailed') : '删除执行记录失败';
|
||||
alert(deleteFailedMsg + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1488,7 +1523,7 @@ function updateBatchActionsState() {
|
||||
batchActions.style.display = 'flex';
|
||||
}
|
||||
if (selectedCountSpan) {
|
||||
selectedCountSpan.textContent = `已选择 ${selectedCount} 项`;
|
||||
selectedCountSpan.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: selectedCount }) : `已选择 ${selectedCount} 项`;
|
||||
}
|
||||
} else {
|
||||
if (batchActions) {
|
||||
@@ -1547,15 +1582,15 @@ function deselectAllExecutions() {
|
||||
async function batchDeleteExecutions() {
|
||||
const checkboxes = document.querySelectorAll('.monitor-execution-checkbox:checked');
|
||||
if (checkboxes.length === 0) {
|
||||
alert('请先选择要删除的执行记录');
|
||||
const selectFirstMsg = typeof window.t === 'function' ? window.t('mcpMonitor.selectExecFirst') : '请先选择要删除的执行记录';
|
||||
alert(selectFirstMsg);
|
||||
return;
|
||||
}
|
||||
|
||||
const ids = Array.from(checkboxes).map(cb => cb.value);
|
||||
const count = ids.length;
|
||||
|
||||
// 确认删除
|
||||
if (!confirm(`确定要删除选中的 ${count} 条执行记录吗?此操作不可恢复。`)) {
|
||||
const batchConfirmMsg = typeof window.t === 'function' ? window.t('mcpMonitor.batchDeleteConfirm', { count: count }) : `确定要删除选中的 ${count} 条执行记录吗?此操作不可恢复。`;
|
||||
if (!confirm(batchConfirmMsg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1570,7 +1605,8 @@ async function batchDeleteExecutions() {
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.error || '批量删除执行记录失败');
|
||||
const batchFailedMsg = typeof window.t === 'function' ? window.t('mcp.batchDeleteFailed') : '批量删除执行记录失败';
|
||||
throw new Error(error.error || batchFailedMsg);
|
||||
}
|
||||
|
||||
const result = await response.json().catch(() => ({}));
|
||||
@@ -1580,33 +1616,42 @@ async function batchDeleteExecutions() {
|
||||
const currentPage = monitorState.pagination.page;
|
||||
await refreshMonitorPanel(currentPage);
|
||||
|
||||
alert(`成功删除 ${deletedCount} 条执行记录`);
|
||||
const batchSuccessMsg = typeof window.t === 'function' ? window.t('mcpMonitor.batchDeleteSuccess', { count: deletedCount }) : `成功删除 ${deletedCount} 条执行记录`;
|
||||
alert(batchSuccessMsg);
|
||||
} catch (error) {
|
||||
console.error('批量删除执行记录失败:', error);
|
||||
alert('批量删除执行记录失败: ' + error.message);
|
||||
const batchFailedMsg = typeof window.t === 'function' ? window.t('mcp.batchDeleteFailed') : '批量删除执行记录失败';
|
||||
alert(batchFailedMsg + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function formatExecutionDuration(start, end) {
|
||||
const unknownLabel = typeof window.t === 'function' ? window.t('mcpMonitor.unknown') : '未知';
|
||||
if (!start) {
|
||||
return '未知';
|
||||
return unknownLabel;
|
||||
}
|
||||
const startTime = new Date(start);
|
||||
const endTime = end ? new Date(end) : new Date();
|
||||
if (Number.isNaN(startTime.getTime()) || Number.isNaN(endTime.getTime())) {
|
||||
return '未知';
|
||||
return unknownLabel;
|
||||
}
|
||||
const diffMs = Math.max(0, endTime - startTime);
|
||||
const seconds = Math.floor(diffMs / 1000);
|
||||
if (seconds < 60) {
|
||||
return `${seconds} 秒`;
|
||||
return typeof window.t === 'function' ? window.t('mcpMonitor.durationSeconds', { n: seconds }) : seconds + ' 秒';
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) {
|
||||
const remain = seconds % 60;
|
||||
return remain > 0 ? `${minutes} 分 ${remain} 秒` : `${minutes} 分`;
|
||||
if (remain > 0) {
|
||||
return typeof window.t === 'function' ? window.t('mcpMonitor.durationMinutes', { minutes: minutes, seconds: remain }) : minutes + ' 分 ' + remain + ' 秒';
|
||||
}
|
||||
return typeof window.t === 'function' ? window.t('mcpMonitor.durationMinutesOnly', { minutes: minutes }) : minutes + ' 分';
|
||||
}
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainMinutes = minutes % 60;
|
||||
return remainMinutes > 0 ? `${hours} 小时 ${remainMinutes} 分` : `${hours} 小时`;
|
||||
if (remainMinutes > 0) {
|
||||
return typeof window.t === 'function' ? window.t('mcpMonitor.durationHours', { hours: hours, minutes: remainMinutes }) : hours + ' 小时 ' + remainMinutes + ' 分';
|
||||
}
|
||||
return typeof window.t === 'function' ? window.t('mcpMonitor.durationHoursOnly', { hours: hours }) : hours + ' 小时';
|
||||
}
|
||||
|
||||
@@ -108,11 +108,13 @@ function updateRoleSelectorDisplay() {
|
||||
}
|
||||
}
|
||||
roleSelectorIcon.textContent = icon;
|
||||
roleSelectorText.textContent = selectedRole.name || '默认';
|
||||
const displayName = (selectedRole.name === '默认' || !selectedRole.name) && typeof window.t === 'function'
|
||||
? window.t('chat.defaultRole') : (selectedRole.name || (typeof window.t === 'function' ? window.t('chat.defaultRole') : '默认'));
|
||||
roleSelectorText.textContent = displayName;
|
||||
} else {
|
||||
// 默认角色
|
||||
roleSelectorIcon.textContent = '🔵';
|
||||
roleSelectorText.textContent = '默认';
|
||||
roleSelectorText.textContent = typeof window.t === 'function' ? window.t('chat.defaultRole') : '默认';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+88
-58
@@ -255,7 +255,10 @@ async function loadConfig(loadTools = true) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载配置失败:', error);
|
||||
alert('加载配置失败: ' + error.message);
|
||||
const baseMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('settings.apply.loadFailed')
|
||||
: '加载配置失败';
|
||||
alert(baseMsg + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,7 +272,7 @@ async function loadToolsList(page = 1, searchKeyword = '') {
|
||||
// 显示加载状态
|
||||
if (toolsList) {
|
||||
// 清空整个容器,包括可能存在的分页控件
|
||||
toolsList.innerHTML = '<div class="tools-list-items"><div class="loading" style="padding: 20px; text-align: center; color: var(--text-muted);">⏳ 正在加载工具列表...</div></div>';
|
||||
toolsList.innerHTML = '<div class="tools-list-items"><div class="loading" style="padding: 20px; text-align: center; color: var(--text-muted);">⏳ ' + (typeof window.t === 'function' ? window.t('mcp.loadingTools') : '正在加载工具列表...') + '</div></div>';
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -324,8 +327,8 @@ async function loadToolsList(page = 1, searchKeyword = '') {
|
||||
if (toolsList) {
|
||||
const isTimeout = error.name === 'AbortError' || error.message.includes('timeout');
|
||||
const errorMsg = isTimeout
|
||||
? '加载工具列表超时,可能是外部MCP连接较慢。请点击"刷新"按钮重试,或检查外部MCP连接状态。'
|
||||
: `加载工具列表失败: ${escapeHtml(error.message)}`;
|
||||
? (typeof window.t === 'function' ? window.t('mcp.loadToolsTimeout') : '加载工具列表超时,可能是外部MCP连接较慢。请点击"刷新"按钮重试,或检查外部MCP连接状态。')
|
||||
: (typeof window.t === 'function' ? window.t('mcp.loadToolsFailed') : '加载工具列表失败') + ': ' + escapeHtml(error.message);
|
||||
toolsList.innerHTML = `<div class="error" style="padding: 20px; text-align: center;">${errorMsg}</div>`;
|
||||
}
|
||||
}
|
||||
@@ -399,7 +402,7 @@ function renderToolsList() {
|
||||
listContainer.innerHTML = '';
|
||||
|
||||
if (allTools.length === 0) {
|
||||
listContainer.innerHTML = '<div class="empty">暂无工具</div>';
|
||||
listContainer.innerHTML = '<div class="empty">' + (typeof window.t === 'function' ? window.t('mcp.noTools') : '暂无工具') + '</div>';
|
||||
if (!toolsList.contains(listContainer)) {
|
||||
toolsList.appendChild(listContainer);
|
||||
}
|
||||
@@ -428,8 +431,8 @@ function renderToolsList() {
|
||||
let externalBadge = '';
|
||||
if (toolState.is_external || tool.is_external) {
|
||||
const externalMcpName = toolState.external_mcp || tool.external_mcp || '';
|
||||
const badgeText = externalMcpName ? `外部 (${escapeHtml(externalMcpName)})` : '外部';
|
||||
const badgeTitle = externalMcpName ? `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}` : '外部MCP工具';
|
||||
const badgeText = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalFrom', { name: escapeHtml(externalMcpName) }) : `外部 (${escapeHtml(externalMcpName)})`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部');
|
||||
const badgeTitle = externalMcpName ? (typeof window.t === 'function' ? window.t('mcp.externalToolFrom', { name: escapeHtml(externalMcpName) }) : `外部MCP工具 - 来源:${escapeHtml(externalMcpName)}`) : (typeof window.t === 'function' ? window.t('mcp.externalBadge') : '外部MCP工具');
|
||||
externalBadge = `<span class="external-tool-badge" title="${badgeTitle}">${badgeText}</span>`;
|
||||
}
|
||||
|
||||
@@ -443,7 +446,7 @@ function renderToolsList() {
|
||||
${escapeHtml(tool.name)}
|
||||
${externalBadge}
|
||||
</div>
|
||||
<div class="tool-item-desc">${escapeHtml(tool.description || '无描述')}</div>
|
||||
<div class="tool-item-desc">${escapeHtml(tool.description || (typeof window.t === 'function' ? window.t('mcp.noDescription') : '无描述'))}</div>
|
||||
</div>
|
||||
`;
|
||||
listContainer.appendChild(toolItem);
|
||||
@@ -481,12 +484,19 @@ function renderToolsPagination() {
|
||||
const endItem = Math.min(page * toolsPagination.pageSize, total);
|
||||
|
||||
const savedPageSize = getToolsPageSize();
|
||||
const t = typeof window.t === 'function' ? window.t : (k) => k;
|
||||
const paginationT = (key, opts) => {
|
||||
if (typeof window.t === 'function') return window.t(key, opts);
|
||||
if (key === 'mcp.paginationInfo' && opts) return `显示 ${opts.start}-${opts.end} / 共 ${opts.total} 个工具`;
|
||||
if (key === 'mcp.pageInfo' && opts) return `第 ${opts.page} / ${opts.total} 页`;
|
||||
return key;
|
||||
};
|
||||
pagination.innerHTML = `
|
||||
<div class="pagination-info">
|
||||
显示 ${startItem}-${endItem} / 共 ${total} 个工具${toolsSearchKeyword ? ` (搜索: "${escapeHtml(toolsSearchKeyword)}")` : ''}
|
||||
${paginationT('mcp.paginationInfo', { start: startItem, end: endItem, total: total })}${toolsSearchKeyword ? ` (${t('common.search')}: "${escapeHtml(toolsSearchKeyword)}")` : ''}
|
||||
</div>
|
||||
<div class="pagination-page-size">
|
||||
<label for="tools-page-size-pagination">每页:</label>
|
||||
<label for="tools-page-size-pagination">${t('mcp.perPage')}</label>
|
||||
<select id="tools-page-size-pagination" onchange="changeToolsPageSize()">
|
||||
<option value="10" ${savedPageSize === 10 ? 'selected' : ''}>10</option>
|
||||
<option value="20" ${savedPageSize === 20 ? 'selected' : ''}>20</option>
|
||||
@@ -495,11 +505,11 @@ function renderToolsPagination() {
|
||||
</select>
|
||||
</div>
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="loadToolsList(1, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>首页</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page - 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>上一页</button>
|
||||
<span class="pagination-page">第 ${page} / ${totalPages} 页</span>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page + 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>下一页</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${totalPages}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>末页</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(1, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${t('mcp.firstPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page - 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === 1 ? 'disabled' : ''}>${t('mcp.prevPage')}</button>
|
||||
<span class="pagination-page">${paginationT('mcp.pageInfo', { page: page, total: totalPages })}</span>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${page + 1}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${t('mcp.nextPage')}</button>
|
||||
<button class="btn-secondary" onclick="loadToolsList(${totalPages}, '${escapeHtml(toolsSearchKeyword)}')" ${page === totalPages ? 'disabled' : ''}>${t('mcp.lastPage')}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -693,9 +703,10 @@ async function updateToolsStats() {
|
||||
totalEnabled = currentPageEnabled;
|
||||
}
|
||||
|
||||
const tStats = typeof window.t === 'function' ? window.t : (k) => k;
|
||||
statsEl.innerHTML = `
|
||||
<span title="当前页启用的工具数">✅ 当前页已启用: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
|
||||
<span title="所有工具中启用的工具总数">📊 总计已启用: <strong>${totalEnabled}</strong> / ${totalTools}</span>
|
||||
<span title="${tStats('mcp.currentPageEnabled')}">✅ ${tStats('mcp.currentPageEnabled')}: <strong>${currentPageEnabled}</strong> / ${currentPageTotal}</span>
|
||||
<span title="${tStats('mcp.totalEnabled')}">📊 ${tStats('mcp.totalEnabled')}: <strong>${totalEnabled}</strong> / ${totalTools}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -737,7 +748,10 @@ async function applySettings() {
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
alert('请填写所有必填字段(标记为 * 的字段)');
|
||||
const msg = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('settings.apply.fillRequired')
|
||||
: '请填写所有必填字段(标记为 * 的字段)';
|
||||
alert(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -896,7 +910,10 @@ async function applySettings() {
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
const error = await updateResponse.json();
|
||||
throw new Error(error.error || '更新配置失败');
|
||||
const fallback = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('settings.apply.applyFailed')
|
||||
: '应用配置失败';
|
||||
throw new Error(error.error || fallback);
|
||||
}
|
||||
|
||||
// 应用配置
|
||||
@@ -906,14 +923,23 @@ async function applySettings() {
|
||||
|
||||
if (!applyResponse.ok) {
|
||||
const error = await applyResponse.json();
|
||||
throw new Error(error.error || '应用配置失败');
|
||||
const fallback = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('settings.apply.applyFailed')
|
||||
: '应用配置失败';
|
||||
throw new Error(error.error || fallback);
|
||||
}
|
||||
|
||||
alert('配置已成功应用!');
|
||||
const successMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('settings.apply.applySuccess')
|
||||
: '配置已成功应用!';
|
||||
alert(successMsg);
|
||||
closeSettings();
|
||||
} catch (error) {
|
||||
console.error('应用配置失败:', error);
|
||||
alert('应用配置失败: ' + error.message);
|
||||
const baseMsg = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('settings.apply.applyFailed')
|
||||
: '应用配置失败';
|
||||
alert(baseMsg + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1024,7 +1050,7 @@ async function saveToolsConfig() {
|
||||
throw new Error(error.error || '应用配置失败');
|
||||
}
|
||||
|
||||
alert('工具配置已成功保存!');
|
||||
alert(typeof window.t === 'function' ? window.t('mcp.toolsConfigSaved') : '工具配置已成功保存!');
|
||||
|
||||
// 重新加载工具列表以反映最新状态
|
||||
if (typeof loadToolsList === 'function') {
|
||||
@@ -1032,7 +1058,7 @@ async function saveToolsConfig() {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('保存工具配置失败:', error);
|
||||
alert('保存工具配置失败: ' + error.message);
|
||||
alert((typeof window.t === 'function' ? window.t('mcp.saveToolsConfigFailed') : '保存工具配置失败') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1079,7 +1105,7 @@ async function changePassword() {
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
alert('请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。');
|
||||
alert(typeof window.t === 'function' ? window.t('settings.security.fillPasswordHint') : '请正确填写当前密码和新密码,新密码至少 8 位且需要两次输入一致。');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1104,13 +1130,14 @@ async function changePassword() {
|
||||
throw new Error(result.error || '修改密码失败');
|
||||
}
|
||||
|
||||
alert('密码已更新,请使用新密码重新登录。');
|
||||
const pwdMsg = typeof window.t === 'function' ? window.t('settings.security.passwordUpdated') : '密码已更新,请使用新密码重新登录。';
|
||||
alert(pwdMsg);
|
||||
resetPasswordForm();
|
||||
handleUnauthorized({ message: '密码已更新,请使用新密码重新登录。', silent: false });
|
||||
handleUnauthorized({ message: pwdMsg, silent: false });
|
||||
closeSettings();
|
||||
} catch (error) {
|
||||
console.error('修改密码失败:', error);
|
||||
alert('修改密码失败: ' + error.message);
|
||||
alert((typeof window.t === 'function' ? window.t('settings.security.changePasswordFailed') : '修改密码失败') + ': ' + error.message);
|
||||
} finally {
|
||||
if (submitBtn) {
|
||||
submitBtn.disabled = false;
|
||||
@@ -1173,7 +1200,8 @@ function renderExternalMCPList(servers) {
|
||||
if (!list) return;
|
||||
|
||||
if (Object.keys(servers).length === 0) {
|
||||
list.innerHTML = '<div class="empty">📋 暂无外部MCP配置<br><span style="font-size: 0.875rem; margin-top: 8px; display: block;">点击"添加外部MCP"按钮开始配置</span></div>';
|
||||
const emptyT = typeof window.t === 'function' ? window.t : (k) => k;
|
||||
list.innerHTML = '<div class="empty">📋 ' + emptyT('mcp.noExternalMCP') + '<br><span style="font-size: 0.875rem; margin-top: 8px; display: block;">' + emptyT('mcp.clickToAddExternal') + '</span></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1184,10 +1212,11 @@ function renderExternalMCPList(servers) {
|
||||
status === 'connecting' ? 'status-connecting' :
|
||||
status === 'error' ? 'status-error' :
|
||||
status === 'disabled' ? 'status-disabled' : 'status-disconnected';
|
||||
const statusText = status === 'connected' ? '已连接' :
|
||||
status === 'connecting' ? '连接中...' :
|
||||
status === 'error' ? '连接失败' :
|
||||
status === 'disabled' ? '已禁用' : '未连接';
|
||||
const statusT = typeof window.t === 'function' ? window.t : (k) => k;
|
||||
const statusText = status === 'connected' ? statusT('mcp.connected') :
|
||||
status === 'connecting' ? statusT('mcp.connecting') :
|
||||
status === 'error' ? statusT('mcp.connectionFailed') :
|
||||
status === 'disabled' ? statusT('mcp.disabled') : statusT('mcp.disconnected');
|
||||
const transport = server.config.transport || (server.config.command ? 'stdio' : 'http');
|
||||
const transportIcon = transport === 'stdio' ? '⚙️' : '🌐';
|
||||
|
||||
@@ -1200,15 +1229,15 @@ function renderExternalMCPList(servers) {
|
||||
</div>
|
||||
<div class="external-mcp-item-actions">
|
||||
${status === 'connected' || status === 'disconnected' || status === 'error' ?
|
||||
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" onclick="toggleExternalMCP('${escapeHtml(name)}', '${status}')" title="${status === 'connected' ? '停止连接' : '启动连接'}">
|
||||
${status === 'connected' ? '⏸ 停止' : '▶ 启动'}
|
||||
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" onclick="toggleExternalMCP('${escapeHtml(name)}', '${status}')" title="${status === 'connected' ? statusT('mcp.stopConnection') : statusT('mcp.startConnection')}">
|
||||
${status === 'connected' ? '⏸ ' + statusT('mcp.stop') : '▶ ' + statusT('mcp.start')}
|
||||
</button>` :
|
||||
status === 'connecting' ?
|
||||
`<button class="btn-small" id="btn-toggle-${escapeHtml(name)}" disabled style="opacity: 0.6; cursor: not-allowed;">
|
||||
⏳ 连接中...
|
||||
</button>` : ''}
|
||||
<button class="btn-small" onclick="editExternalMCP('${escapeHtml(name)}')" title="编辑配置" ${status === 'connecting' ? 'disabled' : ''}>✏️ 编辑</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteExternalMCP('${escapeHtml(name)}')" title="删除配置" ${status === 'connecting' ? 'disabled' : ''}>🗑 删除</button>
|
||||
<button class="btn-small" onclick="editExternalMCP('${escapeHtml(name)}')" title="${statusT('mcp.editConfig')}" ${status === 'connecting' ? 'disabled' : ''}>✏️ ${statusT('common.edit')}</button>
|
||||
<button class="btn-small btn-danger" onclick="deleteExternalMCP('${escapeHtml(name)}')" title="${statusT('mcp.deleteConfig')}" ${status === 'connecting' ? 'disabled' : ''}>🗑 ${statusT('common.delete')}</button>
|
||||
</div>
|
||||
</div>
|
||||
${status === 'error' && server.error ? `
|
||||
@@ -1217,31 +1246,31 @@ function renderExternalMCPList(servers) {
|
||||
</div>` : ''}
|
||||
<div class="external-mcp-item-details">
|
||||
<div>
|
||||
<strong>传输模式</strong>
|
||||
<strong>${statusT('mcp.transportMode')}</strong>
|
||||
<span>${transportIcon} ${escapeHtml(transport.toUpperCase())}</span>
|
||||
</div>
|
||||
${server.tool_count !== undefined && server.tool_count > 0 ? `
|
||||
<div>
|
||||
<strong>工具数量</strong>
|
||||
<strong>${statusT('mcp.toolCount')}</strong>
|
||||
<span style="font-weight: 600; color: var(--accent-color);">🔧 ${server.tool_count} 个工具</span>
|
||||
</div>` : server.tool_count === 0 && status === 'connected' ? `
|
||||
<div>
|
||||
<strong>工具数量</strong>
|
||||
<span style="color: var(--text-muted);">暂无工具</span>
|
||||
<strong>${statusT('mcp.toolCount')}</strong>
|
||||
<span style="color: var(--text-muted);">${statusT('mcp.noTools')}</span>
|
||||
</div>` : ''}
|
||||
${server.config.description ? `
|
||||
<div>
|
||||
<strong>描述</strong>
|
||||
<strong>${statusT('mcp.description')}</strong>
|
||||
<span>${escapeHtml(server.config.description)}</span>
|
||||
</div>` : ''}
|
||||
${server.config.timeout ? `
|
||||
<div>
|
||||
<strong>超时时间</strong>
|
||||
<strong>${statusT('mcp.timeout')}</strong>
|
||||
<span>${server.config.timeout} 秒</span>
|
||||
</div>` : ''}
|
||||
${transport === 'stdio' && server.config.command ? `
|
||||
<div>
|
||||
<strong>命令</strong>
|
||||
<strong>${statusT('mcp.command')}</strong>
|
||||
<span style="font-family: monospace; font-size: 0.8125rem;">${escapeHtml(server.config.command)}</span>
|
||||
</div>` : ''}
|
||||
${transport === 'http' && server.config.url ? `
|
||||
@@ -1267,18 +1296,19 @@ function renderExternalMCPStats(stats) {
|
||||
const disabled = stats.disabled || 0;
|
||||
const connected = stats.connected || 0;
|
||||
|
||||
const statsT = typeof window.t === 'function' ? window.t : (k) => k;
|
||||
statsEl.innerHTML = `
|
||||
<span title="总配置数">📊 总数: <strong>${total}</strong></span>
|
||||
<span title="已启用的配置数">✅ 已启用: <strong>${enabled}</strong></span>
|
||||
<span title="已停用的配置数">⏸ 已停用: <strong>${disabled}</strong></span>
|
||||
<span title="当前已连接的配置数">🔗 已连接: <strong>${connected}</strong></span>
|
||||
<span title="${statsT('mcp.totalCount')}">📊 ${statsT('mcp.totalCount')}: <strong>${total}</strong></span>
|
||||
<span title="${statsT('mcp.enabledCount')}">✅ ${statsT('mcp.enabledCount')}: <strong>${enabled}</strong></span>
|
||||
<span title="${statsT('mcp.disabledCount')}">⏸ ${statsT('mcp.disabledCount')}: <strong>${disabled}</strong></span>
|
||||
<span title="${statsT('mcp.connectedCount')}">🔗 ${statsT('mcp.connectedCount')}: <strong>${connected}</strong></span>
|
||||
`;
|
||||
}
|
||||
|
||||
// 显示添加外部MCP模态框
|
||||
function showAddExternalMCPModal() {
|
||||
currentEditingMCPName = null;
|
||||
document.getElementById('external-mcp-modal-title').textContent = '添加外部MCP';
|
||||
document.getElementById('external-mcp-modal-title').textContent = (typeof window.t === 'function' ? window.t('mcp.addExternalMCP') : '添加外部MCP');
|
||||
document.getElementById('external-mcp-json').value = '';
|
||||
document.getElementById('external-mcp-json-error').style.display = 'none';
|
||||
document.getElementById('external-mcp-json-error').textContent = '';
|
||||
@@ -1303,7 +1333,7 @@ async function editExternalMCP(name) {
|
||||
const server = await response.json();
|
||||
currentEditingMCPName = name;
|
||||
|
||||
document.getElementById('external-mcp-modal-title').textContent = '编辑外部MCP';
|
||||
document.getElementById('external-mcp-modal-title').textContent = (typeof window.t === 'function' ? window.t('mcp.editExternalMCP') : '编辑外部MCP');
|
||||
|
||||
// 将配置转换为对象格式(key为名称)
|
||||
const config = { ...server.config };
|
||||
@@ -1325,7 +1355,7 @@ async function editExternalMCP(name) {
|
||||
document.getElementById('external-mcp-modal').style.display = 'block';
|
||||
} catch (error) {
|
||||
console.error('编辑外部MCP失败:', error);
|
||||
alert('编辑失败: ' + error.message);
|
||||
alert((typeof window.t === 'function' ? window.t('mcp.operationFailed') : '编辑失败') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1528,7 +1558,7 @@ async function saveExternalMCP() {
|
||||
}
|
||||
// 轮询几次以拉取后端异步更新的工具数量(无固定延迟,拿到即停)
|
||||
pollExternalMCPToolCount(null, 5);
|
||||
alert('保存成功');
|
||||
alert(typeof window.t === 'function' ? window.t('mcp.saveSuccess') : '保存成功');
|
||||
} catch (error) {
|
||||
console.error('保存外部MCP失败:', error);
|
||||
errorDiv.textContent = '保存失败: ' + error.message;
|
||||
@@ -1539,7 +1569,7 @@ async function saveExternalMCP() {
|
||||
|
||||
// 删除外部MCP
|
||||
async function deleteExternalMCP(name) {
|
||||
if (!confirm(`确定要删除外部MCP "${name}" 吗?`)) {
|
||||
if (!confirm((typeof window.t === 'function' ? window.t('mcp.deleteExternalConfirm', { name: name }) : `确定要删除外部MCP "${name}" 吗?`))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1558,10 +1588,10 @@ async function deleteExternalMCP(name) {
|
||||
if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') {
|
||||
window.refreshMentionTools();
|
||||
}
|
||||
alert('删除成功');
|
||||
alert(typeof window.t === 'function' ? window.t('mcp.deleteSuccess') : '删除成功');
|
||||
} catch (error) {
|
||||
console.error('删除外部MCP失败:', error);
|
||||
alert('删除失败: ' + error.message);
|
||||
alert((typeof window.t === 'function' ? window.t('mcp.operationFailed') : '删除失败') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1626,7 +1656,7 @@ async function toggleExternalMCP(name, currentStatus) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换外部MCP状态失败:', error);
|
||||
alert('操作失败: ' + error.message);
|
||||
alert((typeof window.t === 'function' ? window.t('mcp.operationFailed') : '操作失败') + ': ' + error.message);
|
||||
|
||||
// 恢复按钮状态
|
||||
if (button) {
|
||||
@@ -1679,7 +1709,7 @@ async function pollExternalMCPStatus(name, maxAttempts = 30) {
|
||||
window.refreshMentionTools();
|
||||
}
|
||||
if (status === 'error') {
|
||||
alert('连接失败,请检查配置和网络连接');
|
||||
alert(typeof window.t === 'function' ? window.t('mcp.connectionFailedCheck') : '连接失败,请检查配置和网络连接');
|
||||
}
|
||||
return;
|
||||
} else if (status === 'connecting') {
|
||||
@@ -1701,7 +1731,7 @@ async function pollExternalMCPStatus(name, maxAttempts = 30) {
|
||||
if (typeof window !== 'undefined' && typeof window.refreshMentionTools === 'function') {
|
||||
window.refreshMentionTools();
|
||||
}
|
||||
alert('连接超时,请检查配置和网络连接');
|
||||
alert(typeof window.t === 'function' ? window.t('mcp.connectionTimeout') : '连接超时,请检查配置和网络连接');
|
||||
}
|
||||
|
||||
// 在打开设置时加载外部MCP列表
|
||||
|
||||
+134
-131
@@ -1,4 +1,7 @@
|
||||
// 任务管理页面功能
|
||||
function _t(key, opts) {
|
||||
return typeof window.t === 'function' ? window.t(key, opts) : key;
|
||||
}
|
||||
|
||||
// HTML转义函数(如果未定义)
|
||||
if (typeof escapeHtml === 'undefined') {
|
||||
@@ -106,7 +109,7 @@ async function loadTasks() {
|
||||
const listContainer = document.getElementById('tasks-list');
|
||||
if (!listContainer) return;
|
||||
|
||||
listContainer.innerHTML = '<div class="loading-spinner">加载中...</div>';
|
||||
listContainer.innerHTML = '<div class="loading-spinner">' + _t('tasks.loadingTasks') + '</div>';
|
||||
|
||||
try {
|
||||
// 并行加载运行中的任务和已完成的任务历史
|
||||
@@ -117,7 +120,7 @@ async function loadTasks() {
|
||||
|
||||
// 处理运行中的任务
|
||||
if (activeResponse.status === 'rejected' || !activeResponse.value || !activeResponse.value.ok) {
|
||||
throw new Error('获取任务列表失败');
|
||||
throw new Error(_t('tasks.loadTaskListFailed'));
|
||||
}
|
||||
|
||||
const activeResult = await activeResponse.value.json();
|
||||
@@ -177,8 +180,8 @@ async function loadTasks() {
|
||||
console.error('加载任务失败:', error);
|
||||
listContainer.innerHTML = `
|
||||
<div class="tasks-empty">
|
||||
<p>加载失败: ${escapeHtml(error.message)}</p>
|
||||
<button class="btn-secondary" onclick="loadTasks()">重试</button>
|
||||
<p>${_t('tasks.loadFailedRetry')}: ${escapeHtml(error.message)}</p>
|
||||
<button class="btn-secondary" onclick="loadTasks()">${_t('tasks.retry')}</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
@@ -296,21 +299,21 @@ function toggleShowHistory(show) {
|
||||
|
||||
// 计算执行时长
|
||||
function calculateDuration(startedAt) {
|
||||
if (!startedAt) return '未知';
|
||||
if (!startedAt) return _t('tasks.unknown');
|
||||
const start = new Date(startedAt);
|
||||
const now = new Date();
|
||||
const diff = Math.floor((now - start) / 1000); // 秒
|
||||
const diff = Math.floor((now - start) / 1000);
|
||||
|
||||
if (diff < 60) {
|
||||
return `${diff}秒`;
|
||||
return diff + _t('tasks.durationSeconds');
|
||||
} else if (diff < 3600) {
|
||||
const minutes = Math.floor(diff / 60);
|
||||
const seconds = diff % 60;
|
||||
return `${minutes}分${seconds}秒`;
|
||||
return minutes + _t('tasks.durationMinutes') + ' ' + seconds + _t('tasks.durationSeconds');
|
||||
} else {
|
||||
const hours = Math.floor(diff / 3600);
|
||||
const minutes = Math.floor((diff % 3600) / 60);
|
||||
return `${hours}小时${minutes}分`;
|
||||
return hours + _t('tasks.durationHours') + ' ' + minutes + _t('tasks.durationMinutes');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -349,9 +352,9 @@ function renderTasks(tasks) {
|
||||
if (tasks.length === 0) {
|
||||
listContainer.innerHTML = `
|
||||
<div class="tasks-empty">
|
||||
<p>当前没有符合条件的任务</p>
|
||||
<p>${_t('tasks.noMatchingTasks')}</p>
|
||||
${tasksState.allTasks.length === 0 && tasksState.completedTasksHistory.length > 0 ?
|
||||
'<p style="margin-top: 8px; color: var(--text-muted); font-size: 0.875rem;">提示:有已完成的任务历史,请勾选"显示历史记录"查看</p>' : ''}
|
||||
'<p style="margin-top: 8px; color: var(--text-muted); font-size: 0.875rem;">' + _t('tasks.historyHint') + '</p>' : ''}
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
@@ -359,12 +362,12 @@ function renderTasks(tasks) {
|
||||
|
||||
// 状态映射
|
||||
const statusMap = {
|
||||
'running': { text: '执行中', class: 'task-status-running' },
|
||||
'cancelling': { text: '取消中', class: 'task-status-cancelling' },
|
||||
'failed': { text: '执行失败', class: 'task-status-failed' },
|
||||
'timeout': { text: '执行超时', class: 'task-status-timeout' },
|
||||
'cancelled': { text: '已取消', class: 'task-status-cancelled' },
|
||||
'completed': { text: '已完成', class: 'task-status-completed' }
|
||||
'running': { text: _t('tasks.statusRunning'), class: 'task-status-running' },
|
||||
'cancelling': { text: _t('tasks.statusCancelling'), class: 'task-status-cancelling' },
|
||||
'failed': { text: _t('tasks.statusFailed'), class: 'task-status-failed' },
|
||||
'timeout': { text: _t('tasks.statusTimeout'), class: 'task-status-timeout' },
|
||||
'cancelled': { text: _t('tasks.statusCancelled'), class: 'task-status-cancelled' },
|
||||
'completed': { text: _t('tasks.statusCompleted'), class: 'task-status-completed' }
|
||||
};
|
||||
|
||||
// 分离当前任务和历史任务
|
||||
@@ -382,8 +385,8 @@ function renderTasks(tasks) {
|
||||
if (historyTasks.length > 0) {
|
||||
html += `<div class="tasks-history-section">
|
||||
<div class="tasks-history-header">
|
||||
<span class="tasks-history-title">📜 最近完成的任务(最近24小时)</span>
|
||||
<button class="btn-secondary btn-small" onclick="clearTasksHistory()">清空历史</button>
|
||||
<span class="tasks-history-title">📜 ` + _t('tasks.recentCompletedTasks') + `</span>
|
||||
<button class="btn-secondary btn-small" onclick="clearTasksHistory()">` + _t('tasks.clearHistory') + `</button>
|
||||
</div>
|
||||
${historyTasks.map(task => renderTaskItem(task, statusMap, true)).join('')}
|
||||
</div>`;
|
||||
@@ -406,7 +409,7 @@ function renderTaskItem(task, statusMap, isHistory = false) {
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
})
|
||||
: '未知时间';
|
||||
: _t('tasks.unknownTime');
|
||||
|
||||
const completedText = completedTime && !isNaN(completedTime.getTime())
|
||||
? completedTime.toLocaleString('zh-CN', {
|
||||
@@ -438,22 +441,22 @@ function renderTaskItem(task, statusMap, isHistory = false) {
|
||||
</label>
|
||||
` : '<div class="task-checkbox-placeholder"></div>'}
|
||||
<span class="task-status ${status.class}">${status.text}</span>
|
||||
${isHistory ? '<span class="task-history-badge" title="历史记录">📜</span>' : ''}
|
||||
<span class="task-message" title="${escapeHtml(task.message || '未命名任务')}">${escapeHtml(task.message || '未命名任务')}</span>
|
||||
${isHistory ? '<span class="task-history-badge" title="' + _t('tasks.historyBadge') + '">📜</span>' : ''}
|
||||
<span class="task-message" title="${escapeHtml(task.message || _t('tasks.unnamedTask'))}">${escapeHtml(task.message || _t('tasks.unnamedTask'))}</span>
|
||||
</div>
|
||||
<div class="task-actions">
|
||||
${duration ? `<span class="task-duration" title="执行时长">⏱ ${duration}</span>` : ''}
|
||||
<span class="task-time" title="${isHistory && completedText ? '完成时间' : '开始时间'}">
|
||||
${duration ? `<span class="task-duration" title="${_t('tasks.duration')}">⏱ ${duration}</span>` : ''}
|
||||
<span class="task-time" title="${isHistory && completedText ? _t('tasks.completedAt') : _t('tasks.startedAt')}">
|
||||
${isHistory && completedText ? completedText : timeText}
|
||||
</span>
|
||||
${canCancel ? `<button class="btn-secondary btn-small" onclick="cancelTask('${task.conversationId}', this)">取消任务</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation('${task.conversationId}')">查看对话</button>` : ''}
|
||||
${canCancel ? `<button class="btn-secondary btn-small" onclick="cancelTask('${task.conversationId}', this)">` + _t('tasks.cancelTask') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewConversation('${task.conversationId}')">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
${task.conversationId ? `
|
||||
<div class="task-details">
|
||||
<span class="task-id-label">对话ID:</span>
|
||||
<span class="task-id-value" title="点击复制" onclick="copyTaskId('${task.conversationId}')">${escapeHtml(task.conversationId)}</span>
|
||||
<span class="task-id-label">` + _t('tasks.conversationIdLabel') + `:</span>
|
||||
<span class="task-id-value" title="` + _t('tasks.clickToCopy') + `" onclick="copyTaskId('${task.conversationId}')">${escapeHtml(task.conversationId)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
@@ -462,7 +465,7 @@ function renderTaskItem(task, statusMap, isHistory = false) {
|
||||
|
||||
// 清空任务历史
|
||||
function clearTasksHistory() {
|
||||
if (!confirm('确定要清空所有任务历史记录吗?')) {
|
||||
if (!confirm(_t('tasks.clearHistoryConfirm'))) {
|
||||
return;
|
||||
}
|
||||
tasksState.completedTasksHistory = [];
|
||||
@@ -490,7 +493,7 @@ function updateBatchActions() {
|
||||
const count = tasksState.selectedTasks.size;
|
||||
if (count > 0) {
|
||||
batchActions.style.display = 'flex';
|
||||
selectedCount.textContent = `已选择 ${count} 项`;
|
||||
selectedCount.textContent = typeof window.t === 'function' ? window.t('mcp.selectedCount', { count: count }) : `已选择 ${count} 项`;
|
||||
} else {
|
||||
batchActions.style.display = 'none';
|
||||
}
|
||||
@@ -509,7 +512,7 @@ async function batchCancelTasks() {
|
||||
const selected = Array.from(tasksState.selectedTasks);
|
||||
if (selected.length === 0) return;
|
||||
|
||||
if (!confirm(`确定要取消 ${selected.length} 个任务吗?`)) {
|
||||
if (!confirm(_t('tasks.confirmCancelTasks', { n: selected.length }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -545,9 +548,9 @@ async function batchCancelTasks() {
|
||||
|
||||
// 显示结果
|
||||
if (failCount > 0) {
|
||||
alert(`批量取消完成:成功 ${successCount} 个,失败 ${failCount} 个`);
|
||||
alert(_t('tasks.batchCancelResultPartial', { success: successCount, fail: failCount }));
|
||||
} else {
|
||||
alert(`成功取消 ${successCount} 个任务`);
|
||||
alert(_t('tasks.batchCancelResultSuccess', { n: successCount }));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,7 +559,7 @@ function copyTaskId(conversationId) {
|
||||
navigator.clipboard.writeText(conversationId).then(() => {
|
||||
// 显示复制成功提示
|
||||
const tooltip = document.createElement('div');
|
||||
tooltip.textContent = '已复制!';
|
||||
tooltip.textContent = _t('tasks.copiedToast');
|
||||
tooltip.style.cssText = 'position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: rgba(0,0,0,0.8); color: white; padding: 8px 16px; border-radius: 4px; z-index: 10000;';
|
||||
document.body.appendChild(tooltip);
|
||||
setTimeout(() => tooltip.remove(), 1000);
|
||||
@@ -571,7 +574,7 @@ async function cancelTask(conversationId, button) {
|
||||
|
||||
const originalText = button.textContent;
|
||||
button.disabled = true;
|
||||
button.textContent = '取消中...';
|
||||
button.textContent = _t('tasks.cancelling');
|
||||
|
||||
try {
|
||||
const response = await apiFetch('/api/agent-loop/cancel', {
|
||||
@@ -584,7 +587,7 @@ async function cancelTask(conversationId, button) {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '取消任务失败');
|
||||
throw new Error(result.error || _t('tasks.cancelTaskFailed'));
|
||||
}
|
||||
|
||||
// 从选择中移除
|
||||
@@ -595,7 +598,7 @@ async function cancelTask(conversationId, button) {
|
||||
await loadTasks();
|
||||
} catch (error) {
|
||||
console.error('取消任务失败:', error);
|
||||
alert('取消任务失败: ' + error.message);
|
||||
alert(_t('tasks.cancelTaskFailed') + ': ' + error.message);
|
||||
button.disabled = false;
|
||||
button.textContent = originalText;
|
||||
}
|
||||
@@ -738,7 +741,7 @@ async function showBatchImportModal() {
|
||||
try {
|
||||
const loadedRoles = await loadRoles();
|
||||
// 清空现有选项(除了默认选项)
|
||||
roleSelect.innerHTML = '<option value="">默认</option>';
|
||||
roleSelect.innerHTML = '<option value="">' + _t('batchImportModal.defaultRole') + '</option>';
|
||||
|
||||
// 添加已启用的角色
|
||||
const sortedRoles = loadedRoles.sort((a, b) => {
|
||||
@@ -782,7 +785,7 @@ function updateBatchImportStats(text) {
|
||||
const count = lines.length;
|
||||
|
||||
if (count > 0) {
|
||||
statsEl.innerHTML = `<div class="batch-import-stat">共 ${count} 个任务</div>`;
|
||||
statsEl.innerHTML = '<div class="batch-import-stat">' + _t('tasks.taskCount', { count: count }) + '</div>';
|
||||
statsEl.style.display = 'block';
|
||||
} else {
|
||||
statsEl.style.display = 'none';
|
||||
@@ -808,14 +811,14 @@ async function createBatchQueue() {
|
||||
|
||||
const text = input.value.trim();
|
||||
if (!text) {
|
||||
alert('请输入至少一个任务');
|
||||
alert(_t('tasks.enterTaskPrompt'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 按行分割任务
|
||||
const tasks = text.split('\n').map(line => line.trim()).filter(line => line !== '');
|
||||
if (tasks.length === 0) {
|
||||
alert('没有有效的任务');
|
||||
alert(_t('tasks.noValidTask'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -836,7 +839,7 @@ async function createBatchQueue() {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '创建批量任务队列失败');
|
||||
throw new Error(result.error || _t('tasks.createBatchQueueFailed'));
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -849,7 +852,7 @@ async function createBatchQueue() {
|
||||
refreshBatchQueues();
|
||||
} catch (error) {
|
||||
console.error('创建批量任务队列失败:', error);
|
||||
alert('创建批量任务队列失败: ' + error.message);
|
||||
alert(_t('tasks.createBatchQueueFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -916,7 +919,7 @@ async function loadBatchQueues(page) {
|
||||
try {
|
||||
const response = await apiFetch(`/api/batch-tasks?${params.toString()}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取批量任务队列失败');
|
||||
throw new Error(_t('tasks.loadFailedRetry'));
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -929,7 +932,7 @@ async function loadBatchQueues(page) {
|
||||
section.style.display = 'block';
|
||||
const list = document.getElementById('batch-queues-list');
|
||||
if (list) {
|
||||
list.innerHTML = '<div class="tasks-empty"><p>加载失败: ' + escapeHtml(error.message) + '</p><button class="btn-secondary" onclick="refreshBatchQueues()">重试</button></div>';
|
||||
list.innerHTML = '<div class="tasks-empty"><p>' + _t('tasks.loadFailedRetry') + ': ' + escapeHtml(error.message) + '</p><button class="btn-secondary" onclick="refreshBatchQueues()">' + _t('tasks.retry') + '</button></div>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -964,7 +967,7 @@ function renderBatchQueues() {
|
||||
const queues = batchQueuesState.queues;
|
||||
|
||||
if (queues.length === 0) {
|
||||
list.innerHTML = '<div class="tasks-empty"><p>当前没有批量任务队列</p></div>';
|
||||
list.innerHTML = '<div class="tasks-empty"><p>' + _t('tasks.noBatchQueues') + '</p></div>';
|
||||
if (pagination) pagination.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
@@ -976,11 +979,11 @@ function renderBatchQueues() {
|
||||
|
||||
list.innerHTML = queues.map(queue => {
|
||||
const statusMap = {
|
||||
'pending': { text: '待执行', class: 'batch-queue-status-pending' },
|
||||
'running': { text: '执行中', class: 'batch-queue-status-running' },
|
||||
'paused': { text: '已暂停', class: 'batch-queue-status-paused' },
|
||||
'completed': { text: '已完成', class: 'batch-queue-status-completed' },
|
||||
'cancelled': { text: '已取消', class: 'batch-queue-status-cancelled' }
|
||||
'pending': { text: _t('tasks.statusPending'), class: 'batch-queue-status-pending' },
|
||||
'running': { text: _t('tasks.statusRunning'), class: 'batch-queue-status-running' },
|
||||
'paused': { text: _t('tasks.statusPaused'), class: 'batch-queue-status-paused' },
|
||||
'completed': { text: _t('tasks.statusCompleted'), class: 'batch-queue-status-completed' },
|
||||
'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-queue-status-cancelled' }
|
||||
};
|
||||
|
||||
const status = statusMap[queue.status] || { text: queue.status, class: 'batch-queue-status-unknown' };
|
||||
@@ -1012,8 +1015,8 @@ function renderBatchQueues() {
|
||||
// 显示角色信息(使用正确的角色图标)
|
||||
const loadedRoles = batchQueuesState.loadedRoles || [];
|
||||
const roleIcon = getRoleIconForDisplay(queue.role, loadedRoles);
|
||||
const roleName = queue.role && queue.role !== '' ? queue.role : '默认';
|
||||
const roleDisplay = `<span class="batch-queue-role" style="margin-right: 8px;" title="角色: ${escapeHtml(roleName)}">${roleIcon} ${escapeHtml(roleName)}</span>`;
|
||||
const roleName = queue.role && queue.role !== '' ? queue.role : _t('batchQueueDetailModal.defaultRole');
|
||||
const roleDisplay = `<span class="batch-queue-role" style="margin-right: 8px;" title="${_t('batchQueueDetailModal.role')}: ${escapeHtml(roleName)}">${roleIcon} ${escapeHtml(roleName)}</span>`;
|
||||
|
||||
return `
|
||||
<div class="batch-queue-item" data-queue-id="${queue.id}" onclick="showBatchQueueDetail('${queue.id}')">
|
||||
@@ -1022,8 +1025,8 @@ function renderBatchQueues() {
|
||||
${titleDisplay}
|
||||
${roleDisplay}
|
||||
<span class="batch-queue-status ${status.class}">${status.text}</span>
|
||||
<span class="batch-queue-id">队列ID: ${escapeHtml(queue.id)}</span>
|
||||
<span class="batch-queue-time">创建时间: ${new Date(queue.createdAt).toLocaleString('zh-CN')}</span>
|
||||
<span class="batch-queue-id">${_t('tasks.queueIdLabel')}: ${escapeHtml(queue.id)}</span>
|
||||
<span class="batch-queue-time">${_t('tasks.createdTimeLabel')}: ${new Date(queue.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="batch-queue-progress">
|
||||
<div class="batch-queue-progress-bar">
|
||||
@@ -1032,16 +1035,16 @@ function renderBatchQueues() {
|
||||
<span class="batch-queue-progress-text">${progress}% (${stats.completed + stats.failed + stats.cancelled}/${stats.total})</span>
|
||||
</div>
|
||||
<div class="batch-queue-actions" style="display: flex; align-items: center; gap: 8px; margin-left: 12px;" onclick="event.stopPropagation();">
|
||||
${canDelete ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteBatchQueueFromList('${queue.id}')" title="删除队列">删除</button>` : ''}
|
||||
${canDelete ? `<button class="btn-secondary btn-small btn-danger" onclick="deleteBatchQueueFromList('${queue.id}')" title="${_t('tasks.deleteQueue')}">${_t('common.delete')}</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
<div class="batch-queue-stats">
|
||||
<span>总计: ${stats.total}</span>
|
||||
<span>待执行: ${stats.pending}</span>
|
||||
<span>执行中: ${stats.running}</span>
|
||||
<span style="color: var(--success-color);">已完成: ${stats.completed}</span>
|
||||
<span style="color: var(--error-color);">失败: ${stats.failed}</span>
|
||||
${stats.cancelled > 0 ? `<span style="color: var(--text-secondary);">已取消: ${stats.cancelled}</span>` : ''}
|
||||
<span>${_t('tasks.totalLabel')}: ${stats.total}</span>
|
||||
<span>${_t('tasks.pendingLabel')}: ${stats.pending}</span>
|
||||
<span>${_t('tasks.runningLabel')}: ${stats.running}</span>
|
||||
<span style="color: var(--success-color);">${_t('tasks.completedLabel')}: ${stats.completed}</span>
|
||||
<span style="color: var(--error-color);">${_t('tasks.failedLabel')}: ${stats.failed}</span>
|
||||
${stats.cancelled > 0 ? `<span style="color: var(--text-secondary);">${_t('tasks.cancelledLabel')}: ${stats.cancelled}</span>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1073,9 +1076,9 @@ function renderBatchQueuesPagination() {
|
||||
// 左侧:显示范围信息和每页数量选择器(参考Skills样式)
|
||||
paginationHTML += `
|
||||
<div class="pagination-info">
|
||||
<span>显示 ${start}-${end} / 共 ${total} 条</span>
|
||||
<span>` + _t('tasks.paginationShow', { start: start, end: end, total: total }) + `</span>
|
||||
<label class="pagination-page-size">
|
||||
每页显示
|
||||
` + _t('tasks.paginationPerPage') + `
|
||||
<select id="batch-queues-page-size-pagination" onchange="changeBatchQueuesPageSize()">
|
||||
<option value="10" ${pageSize === 10 ? 'selected' : ''}>10</option>
|
||||
<option value="20" ${pageSize === 20 ? 'selected' : ''}>20</option>
|
||||
@@ -1089,11 +1092,11 @@ function renderBatchQueuesPagination() {
|
||||
// 右侧:分页按钮(参考Skills样式:首页、上一页、第X/Y页、下一页、末页)
|
||||
paginationHTML += `
|
||||
<div class="pagination-controls">
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>首页</button>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>上一页</button>
|
||||
<span class="pagination-page">第 ${currentPage} / ${totalPages || 1} 页</span>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>下一页</button>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>末页</button>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(1)" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationFirst') + `</button>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage - 1})" ${currentPage === 1 || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationPrev') + `</button>
|
||||
<span class="pagination-page">` + _t('tasks.paginationPage', { current: currentPage, total: totalPages || 1 }) + `</span>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${currentPage + 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationNext') + `</button>
|
||||
<button class="btn-secondary" onclick="goBatchQueuesPage(${totalPages || 1})" ${currentPage >= totalPages || total === 0 ? 'disabled' : ''}>` + _t('tasks.paginationLast') + `</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1189,7 +1192,7 @@ async function showBatchQueueDetail(queueId) {
|
||||
|
||||
const response = await apiFetch(`/api/batch-tasks/${queueId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('获取队列详情失败');
|
||||
throw new Error(_t('tasks.getQueueDetailFailed'));
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
@@ -1198,7 +1201,7 @@ async function showBatchQueueDetail(queueId) {
|
||||
|
||||
if (title) {
|
||||
// textContent 本身会做转义;这里不要再 escapeHtml,否则会把 && 显示成 &...(看起来像“变形/乱码”)
|
||||
title.textContent = queue.title ? `批量任务队列 - ${String(queue.title)}` : '批量任务队列';
|
||||
title.textContent = queue.title ? _t('tasks.batchQueueTitle') + ' - ' + String(queue.title) : _t('tasks.batchQueueTitle');
|
||||
}
|
||||
|
||||
// 更新按钮显示
|
||||
@@ -1210,9 +1213,9 @@ async function showBatchQueueDetail(queueId) {
|
||||
// pending状态显示"开始执行",paused状态显示"继续执行"
|
||||
startBtn.style.display = (queue.status === 'pending' || queue.status === 'paused') ? 'inline-block' : 'none';
|
||||
if (startBtn && queue.status === 'paused') {
|
||||
startBtn.textContent = '继续执行';
|
||||
startBtn.textContent = _t('tasks.resumeExecute');
|
||||
} else if (startBtn && queue.status === 'pending') {
|
||||
startBtn.textContent = '开始执行';
|
||||
startBtn.textContent = _t('batchQueueDetailModal.startExecute');
|
||||
}
|
||||
}
|
||||
if (pauseBtn) {
|
||||
@@ -1226,20 +1229,20 @@ async function showBatchQueueDetail(queueId) {
|
||||
|
||||
// 队列状态映射
|
||||
const queueStatusMap = {
|
||||
'pending': { text: '待执行', class: 'batch-queue-status-pending' },
|
||||
'running': { text: '执行中', class: 'batch-queue-status-running' },
|
||||
'paused': { text: '已暂停', class: 'batch-queue-status-paused' },
|
||||
'completed': { text: '已完成', class: 'batch-queue-status-completed' },
|
||||
'cancelled': { text: '已取消', class: 'batch-queue-status-cancelled' }
|
||||
'pending': { text: _t('tasks.statusPending'), class: 'batch-queue-status-pending' },
|
||||
'running': { text: _t('tasks.statusRunning'), class: 'batch-queue-status-running' },
|
||||
'paused': { text: _t('tasks.statusPaused'), class: 'batch-queue-status-paused' },
|
||||
'completed': { text: _t('tasks.statusCompleted'), class: 'batch-queue-status-completed' },
|
||||
'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-queue-status-cancelled' }
|
||||
};
|
||||
|
||||
// 任务状态映射
|
||||
const taskStatusMap = {
|
||||
'pending': { text: '待执行', class: 'batch-task-status-pending' },
|
||||
'running': { text: '执行中', class: 'batch-task-status-running' },
|
||||
'completed': { text: '已完成', class: 'batch-task-status-completed' },
|
||||
'failed': { text: '失败', class: 'batch-task-status-failed' },
|
||||
'cancelled': { text: '已取消', class: 'batch-task-status-cancelled' }
|
||||
'pending': { text: _t('tasks.statusPending'), class: 'batch-task-status-pending' },
|
||||
'running': { text: _t('tasks.statusRunning'), class: 'batch-task-status-running' },
|
||||
'completed': { text: _t('tasks.statusCompleted'), class: 'batch-task-status-completed' },
|
||||
'failed': { text: _t('tasks.failedLabel'), class: 'batch-task-status-failed' },
|
||||
'cancelled': { text: _t('tasks.statusCancelled'), class: 'batch-task-status-cancelled' }
|
||||
};
|
||||
|
||||
// 获取角色信息(如果队列有角色配置)
|
||||
@@ -1266,51 +1269,51 @@ async function showBatchQueueDetail(queueId) {
|
||||
}
|
||||
}
|
||||
roleDisplay = `<div class="detail-item">
|
||||
<span class="detail-label">角色</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.role') + `</span>
|
||||
<span class="detail-value">${roleIcon} ${escapeHtml(roleName)}</span>
|
||||
</div>`;
|
||||
} else {
|
||||
// 默认角色
|
||||
roleDisplay = `<div class="detail-item">
|
||||
<span class="detail-label">角色</span>
|
||||
<span class="detail-value">🔵 默认</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.role') + `</span>
|
||||
<span class="detail-value">🔵 ` + _t('batchQueueDetailModal.defaultRole') + `</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="batch-queue-detail-info">
|
||||
${queue.title ? `<div class="detail-item">
|
||||
<span class="detail-label">任务标题</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.queueTitle') + `</span>
|
||||
<span class="detail-value">${escapeHtml(queue.title)}</span>
|
||||
</div>` : ''}
|
||||
${roleDisplay}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">队列ID</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.queueId') + `</span>
|
||||
<span class="detail-value"><code>${escapeHtml(queue.id)}</code></span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">状态</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.status') + `</span>
|
||||
<span class="detail-value"><span class="batch-queue-status ${queueStatusMap[queue.status]?.class || ''}">${queueStatusMap[queue.status]?.text || queue.status}</span></span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">创建时间</span>
|
||||
<span class="detail-value">${new Date(queue.createdAt).toLocaleString('zh-CN')}</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.createdAt') + `</span>
|
||||
<span class="detail-value">${new Date(queue.createdAt).toLocaleString()}</span>
|
||||
</div>
|
||||
${queue.startedAt ? `<div class="detail-item">
|
||||
<span class="detail-label">开始时间</span>
|
||||
<span class="detail-value">${new Date(queue.startedAt).toLocaleString('zh-CN')}</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.startedAt') + `</span>
|
||||
<span class="detail-value">${new Date(queue.startedAt).toLocaleString()}</span>
|
||||
</div>` : ''}
|
||||
${queue.completedAt ? `<div class="detail-item">
|
||||
<span class="detail-label">完成时间</span>
|
||||
<span class="detail-value">${new Date(queue.completedAt).toLocaleString('zh-CN')}</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.completedAt') + `</span>
|
||||
<span class="detail-value">${new Date(queue.completedAt).toLocaleString()}</span>
|
||||
</div>` : ''}
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">任务总数</span>
|
||||
<span class="detail-label">` + _t('batchQueueDetailModal.taskTotal') + `</span>
|
||||
<span class="detail-value">${queue.tasks.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="batch-queue-tasks-list">
|
||||
<h4>任务列表</h4>
|
||||
<h4>` + _t('batchQueueDetailModal.taskList') + `</h4>
|
||||
${queue.tasks.map((task, index) => {
|
||||
const taskStatus = taskStatusMap[task.status] || { text: task.status, class: 'batch-task-status-unknown' };
|
||||
const canEdit = queue.status === 'pending' && task.status === 'pending';
|
||||
@@ -1321,14 +1324,14 @@ async function showBatchQueueDetail(queueId) {
|
||||
<span class="batch-task-index">#${index + 1}</span>
|
||||
<span class="batch-task-status ${taskStatus.class}">${taskStatus.text}</span>
|
||||
<span class="batch-task-message" title="${escapeHtml(task.message)}">${escapeHtml(task.message)}</span>
|
||||
${canEdit ? `<button class="btn-secondary btn-small batch-task-edit-btn" onclick="editBatchTaskFromElement(this); event.stopPropagation();">编辑</button>` : ''}
|
||||
${canEdit ? `<button class="btn-secondary btn-small btn-danger batch-task-delete-btn" onclick="deleteBatchTaskFromElement(this); event.stopPropagation();">删除</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation('${task.conversationId}'); event.stopPropagation();">查看对话</button>` : ''}
|
||||
${canEdit ? `<button class="btn-secondary btn-small batch-task-edit-btn" onclick="editBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.edit') + `</button>` : ''}
|
||||
${canEdit ? `<button class="btn-secondary btn-small btn-danger batch-task-delete-btn" onclick="deleteBatchTaskFromElement(this); event.stopPropagation();">` + _t('common.delete') + `</button>` : ''}
|
||||
${task.conversationId ? `<button class="btn-secondary btn-small" onclick="viewBatchTaskConversation('${task.conversationId}'); event.stopPropagation();">` + _t('tasks.viewConversation') + `</button>` : ''}
|
||||
</div>
|
||||
${task.startedAt ? `<div class="batch-task-time">开始: ${new Date(task.startedAt).toLocaleString('zh-CN')}</div>` : ''}
|
||||
${task.completedAt ? `<div class="batch-task-time">完成: ${new Date(task.completedAt).toLocaleString('zh-CN')}</div>` : ''}
|
||||
${task.error ? `<div class="batch-task-error">错误: ${escapeHtml(task.error)}</div>` : ''}
|
||||
${task.result ? `<div class="batch-task-result">结果: ${escapeHtml(task.result.substring(0, 200))}${task.result.length > 200 ? '...' : ''}</div>` : ''}
|
||||
${task.startedAt ? `<div class="batch-task-time">` + _t('batchQueueDetailModal.startLabel') + `: ${new Date(task.startedAt).toLocaleString()}</div>` : ''}
|
||||
${task.completedAt ? `<div class="batch-task-time">` + _t('batchQueueDetailModal.completeLabel') + `: ${new Date(task.completedAt).toLocaleString()}</div>` : ''}
|
||||
${task.error ? `<div class="batch-task-error">` + _t('batchQueueDetailModal.errorLabel') + `: ${escapeHtml(task.error)}</div>` : ''}
|
||||
${task.result ? `<div class="batch-task-result">` + _t('batchQueueDetailModal.resultLabel') + `: ${escapeHtml(task.result.substring(0, 200))}${task.result.length > 200 ? '...' : ''}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
@@ -1343,7 +1346,7 @@ async function showBatchQueueDetail(queueId) {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取队列详情失败:', error);
|
||||
alert('获取队列详情失败: ' + error.message);
|
||||
alert(_t('tasks.getQueueDetailFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,7 +1362,7 @@ async function startBatchQueue() {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '启动批量任务失败');
|
||||
throw new Error(result.error || _t('tasks.startBatchQueueFailed'));
|
||||
}
|
||||
|
||||
// 刷新详情
|
||||
@@ -1367,7 +1370,7 @@ async function startBatchQueue() {
|
||||
refreshBatchQueues();
|
||||
} catch (error) {
|
||||
console.error('启动批量任务失败:', error);
|
||||
alert('启动批量任务失败: ' + error.message);
|
||||
alert(_t('tasks.startBatchQueueFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1376,7 +1379,7 @@ async function pauseBatchQueue() {
|
||||
const queueId = batchQueuesState.currentQueueId;
|
||||
if (!queueId) return;
|
||||
|
||||
if (!confirm('确定要暂停这个批量任务队列吗?当前正在执行的任务将被停止,后续任务将保留待执行状态。')) {
|
||||
if (!confirm(_t('tasks.pauseQueueConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1387,7 +1390,7 @@ async function pauseBatchQueue() {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '暂停批量任务失败');
|
||||
throw new Error(result.error || _t('tasks.pauseQueueFailed'));
|
||||
}
|
||||
|
||||
// 刷新详情
|
||||
@@ -1395,7 +1398,7 @@ async function pauseBatchQueue() {
|
||||
refreshBatchQueues();
|
||||
} catch (error) {
|
||||
console.error('暂停批量任务失败:', error);
|
||||
alert('暂停批量任务失败: ' + error.message);
|
||||
alert(_t('tasks.pauseQueueFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1404,7 +1407,7 @@ async function deleteBatchQueue() {
|
||||
const queueId = batchQueuesState.currentQueueId;
|
||||
if (!queueId) return;
|
||||
|
||||
if (!confirm('确定要删除这个批量任务队列吗?此操作不可恢复。')) {
|
||||
if (!confirm(_t('tasks.deleteQueueConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1415,14 +1418,14 @@ async function deleteBatchQueue() {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '删除批量任务队列失败');
|
||||
throw new Error(result.error || _t('tasks.deleteQueueFailed'));
|
||||
}
|
||||
|
||||
closeBatchQueueDetailModal();
|
||||
refreshBatchQueues();
|
||||
} catch (error) {
|
||||
console.error('删除批量任务队列失败:', error);
|
||||
alert('删除批量任务队列失败: ' + error.message);
|
||||
alert(_t('tasks.deleteQueueFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1430,7 +1433,7 @@ async function deleteBatchQueue() {
|
||||
async function deleteBatchQueueFromList(queueId) {
|
||||
if (!queueId) return;
|
||||
|
||||
if (!confirm('确定要删除这个批量任务队列吗?此操作不可恢复。')) {
|
||||
if (!confirm(_t('tasks.deleteQueueConfirm'))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1441,7 +1444,7 @@ async function deleteBatchQueueFromList(queueId) {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '删除批量任务队列失败');
|
||||
throw new Error(result.error || _t('tasks.deleteQueueFailed'));
|
||||
}
|
||||
|
||||
// 如果当前正在查看这个队列的详情,关闭详情模态框
|
||||
@@ -1453,7 +1456,7 @@ async function deleteBatchQueueFromList(queueId) {
|
||||
refreshBatchQueues();
|
||||
} catch (error) {
|
||||
console.error('删除批量任务队列失败:', error);
|
||||
alert('删除批量任务队列失败: ' + error.message);
|
||||
alert(_t('tasks.deleteQueueFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1599,18 +1602,18 @@ async function saveBatchTask() {
|
||||
const messageInput = document.getElementById('edit-task-message');
|
||||
|
||||
if (!queueId || !taskId) {
|
||||
alert('任务信息不完整');
|
||||
alert(_t('tasks.taskIncomplete'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!messageInput) {
|
||||
alert('无法获取任务消息输入框');
|
||||
alert(_t('tasks.cannotGetTaskMessageInput'));
|
||||
return;
|
||||
}
|
||||
|
||||
const message = messageInput.value.trim();
|
||||
if (!message) {
|
||||
alert('任务消息不能为空');
|
||||
alert(_t('tasks.taskMessageRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1625,7 +1628,7 @@ async function saveBatchTask() {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '更新任务失败');
|
||||
throw new Error(result.error || _t('tasks.updateTaskFailed'));
|
||||
}
|
||||
|
||||
// 关闭编辑模态框
|
||||
@@ -1640,7 +1643,7 @@ async function saveBatchTask() {
|
||||
refreshBatchQueues();
|
||||
} catch (error) {
|
||||
console.error('保存任务失败:', error);
|
||||
alert('保存任务失败: ' + error.message);
|
||||
alert(_t('tasks.saveTaskFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1648,7 +1651,7 @@ async function saveBatchTask() {
|
||||
function showAddBatchTaskModal() {
|
||||
const queueId = batchQueuesState.currentQueueId;
|
||||
if (!queueId) {
|
||||
alert('队列信息不存在');
|
||||
alert(_t('tasks.queueInfoMissing'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1706,18 +1709,18 @@ async function saveAddBatchTask() {
|
||||
const messageInput = document.getElementById('add-task-message');
|
||||
|
||||
if (!queueId) {
|
||||
alert('队列信息不存在');
|
||||
alert(_t('tasks.queueInfoMissing'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!messageInput) {
|
||||
alert('无法获取任务消息输入框');
|
||||
alert(_t('tasks.cannotGetTaskMessageInput'));
|
||||
return;
|
||||
}
|
||||
|
||||
const message = messageInput.value.trim();
|
||||
if (!message) {
|
||||
alert('任务消息不能为空');
|
||||
alert(_t('tasks.taskMessageRequired'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1732,7 +1735,7 @@ async function saveAddBatchTask() {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '添加任务失败');
|
||||
throw new Error(result.error || _t('tasks.addTaskFailed'));
|
||||
}
|
||||
|
||||
// 关闭添加任务模态框
|
||||
@@ -1747,7 +1750,7 @@ async function saveAddBatchTask() {
|
||||
refreshBatchQueues();
|
||||
} catch (error) {
|
||||
console.error('添加任务失败:', error);
|
||||
alert('添加任务失败: ' + error.message);
|
||||
alert(_t('tasks.addTaskFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1779,7 +1782,7 @@ function deleteBatchTaskFromElement(button) {
|
||||
? decodedMessage.substring(0, 50) + '...'
|
||||
: decodedMessage;
|
||||
|
||||
if (!confirm(`确定要删除这个任务吗?\n\n任务内容: ${displayMessage}\n\n此操作不可恢复。`)) {
|
||||
if (!confirm(_t('tasks.confirmDeleteTask', { message: displayMessage }))) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1789,7 +1792,7 @@ function deleteBatchTaskFromElement(button) {
|
||||
// 删除批量任务
|
||||
async function deleteBatchTask(queueId, taskId) {
|
||||
if (!queueId || !taskId) {
|
||||
alert('任务信息不完整');
|
||||
alert(_t('tasks.taskIncomplete'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1800,7 +1803,7 @@ async function deleteBatchTask(queueId, taskId) {
|
||||
|
||||
if (!response.ok) {
|
||||
const result = await response.json().catch(() => ({}));
|
||||
throw new Error(result.error || '删除任务失败');
|
||||
throw new Error(result.error || _t('tasks.deleteTaskFailed'));
|
||||
}
|
||||
|
||||
// 刷新队列详情
|
||||
@@ -1812,7 +1815,7 @@ async function deleteBatchTask(queueId, taskId) {
|
||||
refreshBatchQueues();
|
||||
} catch (error) {
|
||||
console.error('删除任务失败:', error);
|
||||
alert('删除任务失败: ' + error.message);
|
||||
alert(_t('tasks.deleteTaskFailed') + ': ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,14 +156,20 @@ async function loadVulnerabilities(page = null) {
|
||||
function renderVulnerabilities(vulnerabilities) {
|
||||
const listContainer = document.getElementById('vulnerabilities-list');
|
||||
|
||||
// 处理空值情况
|
||||
// 处理空值情况(使用 data-i18n 以便语言切换时自动更新)
|
||||
if (!vulnerabilities || !Array.isArray(vulnerabilities)) {
|
||||
listContainer.innerHTML = '<div class="empty-state">暂无漏洞记录</div>';
|
||||
listContainer.innerHTML = '<div class="empty-state" data-i18n="vulnerabilityPage.noRecords">暂无漏洞记录</div>';
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(listContainer);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (vulnerabilities.length === 0) {
|
||||
listContainer.innerHTML = '<div class="empty-state">暂无漏洞记录</div>';
|
||||
listContainer.innerHTML = '<div class="empty-state" data-i18n="vulnerabilityPage.noRecords">暂无漏洞记录</div>';
|
||||
if (typeof window.applyTranslations === 'function') {
|
||||
window.applyTranslations(listContainer);
|
||||
}
|
||||
// 清空分页信息
|
||||
const paginationContainer = document.getElementById('vulnerability-pagination');
|
||||
if (paginationContainer) {
|
||||
@@ -328,7 +334,7 @@ async function changeVulnerabilityPageSize() {
|
||||
// 显示添加漏洞模态框
|
||||
function showAddVulnerabilityModal() {
|
||||
currentVulnerabilityId = null;
|
||||
document.getElementById('vulnerability-modal-title').textContent = '添加漏洞';
|
||||
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.addVuln') : '添加漏洞');
|
||||
|
||||
// 清空表单
|
||||
document.getElementById('vulnerability-conversation-id').value = '';
|
||||
@@ -353,7 +359,7 @@ async function editVulnerability(id) {
|
||||
|
||||
const vuln = await response.json();
|
||||
currentVulnerabilityId = id;
|
||||
document.getElementById('vulnerability-modal-title').textContent = '编辑漏洞';
|
||||
document.getElementById('vulnerability-modal-title').textContent = (typeof window.t === 'function' ? window.t('vulnerability.editVuln') : '编辑漏洞');
|
||||
|
||||
// 填充表单
|
||||
document.getElementById('vulnerability-conversation-id').value = vuln.conversation_id || '';
|
||||
|
||||
Reference in New Issue
Block a user