From ee9559e0742aca15fea19570800bb9713edf0c56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:17:22 +0800 Subject: [PATCH] Add files via upload --- web/static/i18n/en-US.json | 7 ++ web/static/i18n/zh-CN.json | 7 ++ web/static/js/auth.js | 8 ++ web/static/js/chat.js | 141 ++++++++++++++++++++++++++++-------- web/static/js/i18n.js | 17 +++++ web/static/js/monitor.js | 145 ++++++++++++++++++++++++++++++++++--- web/static/js/terminal.js | 87 +++++++++++++++++++--- web/templates/index.html | 2 +- 8 files changed, 359 insertions(+), 55 deletions(-) diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 8614a817..ad03c37c 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -1028,6 +1028,13 @@ "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}}", + "welcomeLine": "CyberStrikeAI Terminal — real shell session; type commands directly. Ctrl+L to clear screen", + "sessionClosed": "[Session closed]", + "connectionError": "[Terminal connection error]", + "connectFailed": "[Cannot connect to terminal service: {{msg}}]", + "closeTabTitle": "Close", + "containerClickTitle": "Click here, then type commands", + "xtermNotLoaded": "xterm.js failed to load. Refresh the page or check your network.", "close": "×", "newTerminal": "+" }, diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 45b6323a..6e16bd15 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -1028,6 +1028,13 @@ "title": "终端", "description": "在服务器上执行命令,便于运维与调试。命令在服务端执行,请勿执行敏感或破坏性操作。", "terminalTab": "终端 {{n}}", + "welcomeLine": "CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏", + "sessionClosed": "[会话已关闭]", + "connectionError": "[终端连接出错]", + "connectFailed": "[无法连接终端服务: {{msg}}]", + "closeTabTitle": "关闭", + "containerClickTitle": "点击此处后输入命令", + "xtermNotLoaded": "未加载 xterm.js,请刷新页面或检查网络。", "close": "×", "newTerminal": "+" }, diff --git a/web/static/js/auth.js b/web/static/js/auth.js index 64810c86..d57a95f3 100644 --- a/web/static/js/auth.js +++ b/web/static/js/auth.js @@ -242,6 +242,14 @@ async function refreshAppData(showTaskErrors = false) { async function bootstrapApp() { if (!isAppInitialized) { + // 等待 i18n 首包加载完成后再插系统就绪消息,避免清除缓存后语言显示 English 气泡仍是中文 + try { + if (window.i18nReady && typeof window.i18nReady.then === 'function') { + await window.i18nReady; + } + } catch (e) { + console.warn('等待 i18n 就绪失败,继续初始化聊天', e); + } initializeChatUI(); isAppInitialized = true; } diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 9594d5a4..34e2c43e 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -953,7 +953,7 @@ function initializeChatUI() { const messagesDiv = document.getElementById('chat-messages'); if (messagesDiv && messagesDiv.childElementCount === 0) { const readyMsg = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'; - addMessage('assistant', readyMsg); + addMessage('assistant', readyMsg, null, null, null, { systemReadyMessage: true }); } addAttackChainButton(currentConversationId); @@ -989,8 +989,60 @@ function wrapTablesInBubble(bubble) { }); } -// 添加消息 -function addMessage(role, content, mcpExecutionIds = null, progressId = null, createdAt = null) { +/** + * 将「系统已就绪」类文案按当前语言重新渲染进气泡(与 addMessage 助手分支一致的安全处理) + */ +function refreshSystemReadyMessageBubbles() { + if (typeof window.t !== 'function') return; + const text = window.t('chat.systemReadyMessage'); + const escapeHtmlLocal = (s) => { + if (!s) return ''; + const div = document.createElement('div'); + div.textContent = s; + return div.innerHTML; + }; + const defaultSanitizeConfig = { + ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre', 'blockquote', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'a', 'img', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'hr'], + ALLOWED_ATTR: ['href', 'title', 'alt', 'src', 'class'], + ALLOW_DATA_ATTR: false, + }; + let formattedContent; + if (typeof marked !== 'undefined') { + try { + marked.setOptions({ breaks: true, gfm: true }); + const parsed = marked.parse(text); + formattedContent = typeof DOMPurify !== 'undefined' + ? DOMPurify.sanitize(parsed, defaultSanitizeConfig) + : parsed; + } catch (e) { + formattedContent = escapeHtmlLocal(text).replace(/\n/g, '
'); + } + } else { + formattedContent = escapeHtmlLocal(text).replace(/\n/g, '
'); + } + + document.querySelectorAll('.message.assistant[data-system-ready-message]').forEach(function (messageDiv) { + const bubble = messageDiv.querySelector('.message-bubble'); + if (!bubble) return; + const copyBtn = bubble.querySelector('.message-copy-btn'); + if (copyBtn) copyBtn.remove(); + bubble.innerHTML = formattedContent; + if (typeof wrapTablesInBubble === 'function') wrapTablesInBubble(bubble); + messageDiv.dataset.originalContent = text; + const copyBtnNew = document.createElement('button'); + copyBtnNew.className = 'message-copy-btn'; + copyBtnNew.innerHTML = '' + window.t('common.copy') + ''; + copyBtnNew.title = window.t('chat.copyMessageTitle'); + copyBtnNew.onclick = function (e) { + e.stopPropagation(); + copyMessageToClipboard(messageDiv, this); + }; + bubble.appendChild(copyBtnNew); + }); +} + +// 添加消息(options.systemReadyMessage 为 true 时,语言切换会刷新该条文案) +function addMessage(role, content, mcpExecutionIds = null, progressId = null, createdAt = null, options = null) { const messagesDiv = document.getElementById('chat-messages'); const messageDiv = document.createElement('div'); messageCounter++; @@ -1189,7 +1241,9 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr messageTime = new Date(); } const msgTimeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US'; - timeDiv.textContent = messageTime.toLocaleTimeString(msgTimeLocale, { hour: '2-digit', minute: '2-digit' }); + const msgTimeOpts = { hour: '2-digit', minute: '2-digit' }; + if (msgTimeLocale === 'zh-CN') msgTimeOpts.hour12 = false; + timeDiv.textContent = messageTime.toLocaleTimeString(msgTimeLocale, msgTimeOpts); contentWrapper.appendChild(timeDiv); // 如果有MCP执行ID或进度ID,添加查看详情区域(统一使用"渗透测试详情"样式) @@ -1234,6 +1288,10 @@ function addMessage(role, content, mcpExecutionIds = null, progressId = null, cr } messageDiv.appendChild(contentWrapper); + // 标记「系统就绪」占位消息,便于切换语言后刷新文案 + if (options && options.systemReadyMessage) { + messageDiv.setAttribute('data-system-ready-message', '1'); + } messagesDiv.appendChild(messageDiv); messagesDiv.scrollTop = messagesDiv.scrollHeight; return id; @@ -1712,7 +1770,7 @@ async function startNewConversation() { currentConversationGroupId = null; // 新对话不属于任何分组 document.getElementById('chat-messages').innerHTML = ''; const readyMsgNew = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'; - addMessage('assistant', readyMsgNew); + addMessage('assistant', readyMsgNew, null, null, null, { systemReadyMessage: true }); addAttackChainButton(null); updateActiveConversation(); // 刷新分组列表,清除分组高亮 @@ -1957,33 +2015,24 @@ function formatConversationTimestamp(dateObj, todayStart, yesterdayStart) { const fmtLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US'; const yesterdayLabel = typeof window.t === 'function' ? window.t('chat.yesterday') : '昨天'; + const timeOnlyOpts = { hour: '2-digit', minute: '2-digit' }; + const dateTimeOpts = { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }; + const fullDateOpts = { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }; + if (fmtLocale === 'zh-CN') { + timeOnlyOpts.hour12 = false; + dateTimeOpts.hour12 = false; + fullDateOpts.hour12 = false; + } if (messageDate.getTime() === referenceToday.getTime()) { - return dateObj.toLocaleTimeString(fmtLocale, { - hour: '2-digit', - minute: '2-digit' - }); + return dateObj.toLocaleTimeString(fmtLocale, timeOnlyOpts); } if (messageDate.getTime() === referenceYesterday.getTime()) { - return yesterdayLabel + ' ' + dateObj.toLocaleTimeString(fmtLocale, { - hour: '2-digit', - minute: '2-digit' - }); + return yesterdayLabel + ' ' + dateObj.toLocaleTimeString(fmtLocale, timeOnlyOpts); } if (dateObj.getFullYear() === referenceToday.getFullYear()) { - return dateObj.toLocaleString(fmtLocale, { - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); + return dateObj.toLocaleString(fmtLocale, dateTimeOpts); } - return dateObj.toLocaleString(fmtLocale, { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: '2-digit', - minute: '2-digit' - }); + return dateObj.toLocaleString(fmtLocale, fullDateOpts); } function getConversationGroup(dateObj, todayStart, startOfWeek, yesterdayStart) { @@ -2127,7 +2176,7 @@ async function loadConversation(conversationId) { }); } else { const readyMsgEmpty = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'; - addMessage('assistant', readyMsgEmpty); + addMessage('assistant', readyMsgEmpty, null, null, null, { systemReadyMessage: true }); } // 滚动到底部 @@ -2168,7 +2217,7 @@ async function deleteConversation(conversationId, skipConfirm = false) { currentConversationId = null; document.getElementById('chat-messages').innerHTML = ''; const readyMsgLoad = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。'; - addMessage('assistant', readyMsgLoad); + addMessage('assistant', readyMsgLoad, null, null, null, { systemReadyMessage: true }); addAttackChainButton(null); } @@ -2256,7 +2305,9 @@ async function showAttackChain(conversationId) { } modal.style.display = 'block'; - + // 打开时立即按当前语言刷新统计(避免红框内仍显示硬编码中文) + updateAttackChainStats({ nodes: [], edges: [] }); + // 清空容器 const container = document.getElementById('attack-chain-container'); if (container) { @@ -3331,16 +3382,35 @@ function getNodeTypeLabel(type) { return labels[type] || type; } -// 更新统计信息 +// 更新统计信息(使用 i18n,与 attackChainModal.nodesEdges 一致) function updateAttackChainStats(chainData) { const statsElement = document.getElementById('attack-chain-stats'); if (statsElement) { const nodeCount = chainData.nodes ? chainData.nodes.length : 0; const edgeCount = chainData.edges ? chainData.edges.length : 0; - statsElement.textContent = `节点: ${nodeCount} | 边: ${edgeCount}`; + if (typeof window.t === 'function') { + statsElement.textContent = window.t('attackChainModal.nodesEdges', { + nodes: nodeCount, + edges: edgeCount + }); + } else { + statsElement.textContent = `Nodes: ${nodeCount} | Edges: ${edgeCount}`; + } } } +// 语言切换时刷新攻击链统计文案(动态 textContent 不会随 applyTranslations 更新) +document.addEventListener('languagechange', function () { + if (window.attackChainOriginalData && typeof updateAttackChainStats === 'function') { + updateAttackChainStats(window.attackChainOriginalData); + } else { + const statsEl = document.getElementById('attack-chain-stats'); + if (statsEl && typeof window.t === 'function') { + statsEl.textContent = window.t('attackChainModal.nodesEdges', { nodes: 0, edges: 0 }); + } + } +}); + // 关闭节点详情 function closeNodeDetails() { const detailsPanel = document.getElementById('attack-chain-details'); @@ -5203,12 +5273,19 @@ function closeBatchManageModal() { allConversationsForBatch = []; } -// 语言切换时刷新批量管理模态框标题(若当前正在显示) +// 语言切换时刷新批量管理模态框标题(若当前正在显示);并刷新对话列表时间格式与系统就绪提示 document.addEventListener('languagechange', function () { + refreshSystemReadyMessageBubbles(); const modal = document.getElementById('batch-manage-modal'); if (modal && modal.style.display === 'flex') { updateBatchManageTitle(allConversationsForBatch.length); } + // 侧边栏最近对话等列表的时间戳会随语言变化(24h/12h 等),重新拉列表以统一格式 + if (typeof loadConversationsWithGroups === 'function') { + loadConversationsWithGroups(); + } else if (typeof loadConversations === 'function') { + loadConversations(); + } }); // 显示创建分组模态框 diff --git a/web/static/js/i18n.js b/web/static/js/i18n.js index a5fbf803..96a35694 100644 --- a/web/static/js/i18n.js +++ b/web/static/js/i18n.js @@ -6,6 +6,12 @@ const loadedLangs = {}; + // 供 bootstrap 等逻辑等待:避免 chat 在 t() 未就绪时用中文硬编码渲染,导致与语言标签不一致 + let i18nReadyResolve; + window.i18nReady = new Promise(function (resolve) { + i18nReadyResolve = resolve; + }); + function detectInitialLang() { try { const stored = localStorage.getItem(STORAGE_KEY); @@ -159,6 +165,7 @@ async function initI18n() { if (typeof i18next === 'undefined') { console.warn('i18next 未加载,跳过前端国际化初始化'); + if (typeof i18nReadyResolve === 'function') i18nReadyResolve(); return; } @@ -201,12 +208,22 @@ }; document.addEventListener('click', handleGlobalClickForLangDropdown); + + // 若 chat 已在 i18n 完成前用后备中文渲染了系统就绪消息,这里按当前语言纠正一次 + try { + if (typeof refreshSystemReadyMessageBubbles === 'function') { + refreshSystemReadyMessageBubbles(); + } + } catch (e) { /* ignore */ } + + if (typeof i18nReadyResolve === 'function') i18nReadyResolve(); } document.addEventListener('DOMContentLoaded', function () { // i18n 初始化在 DOM Ready 后执行 initI18n().catch(function (e) { console.error('初始化国际化失败:', e); + if (typeof i18nReadyResolve === 'function') i18nReadyResolve(); }); }); })(); diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 13bbb89b..b484abae 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -3,22 +3,55 @@ let activeTaskInterval = null; const ACTIVE_TASK_REFRESH_INTERVAL = 10000; // 10秒检查一次 const TASK_FINAL_STATUSES = new Set(['failed', 'timeout', 'cancelled', 'completed']); -// 将后端下发的进度文案转为当前语言的翻译(已知中文 key 映射) +// 当前界面语言对应的 BCP 47 标签(与时间格式化一致) +function getCurrentTimeLocale() { + if (typeof window.__locale === 'string' && window.__locale.length) { + return window.__locale.startsWith('zh') ? 'zh-CN' : 'en-US'; + } + if (typeof i18next !== 'undefined' && i18next.language) { + return (i18next.language || '').startsWith('zh') ? 'zh-CN' : 'en-US'; + } + return 'zh-CN'; +} + +// toLocaleTimeString 选项:中文用 24 小时制,避免仍显示 AM/PM +function getTimeFormatOptions() { + const loc = getCurrentTimeLocale(); + const base = { hour: '2-digit', minute: '2-digit', second: '2-digit' }; + if (loc === 'zh-CN') { + base.hour12 = false; + } + return base; +} + +// 将后端下发的进度文案转为当前语言的翻译(中英双向映射,切换语言后能跟上) function translateProgressMessage(message) { if (!message || typeof message !== 'string') return message; if (typeof window.t !== 'function') return message; const trim = message.trim(); const map = { + // 中文 '正在调用AI模型...': 'progress.callingAI', '最后一次迭代:正在生成总结和下一步计划...': 'progress.lastIterSummary', '总结生成完成': 'progress.summaryDone', '正在生成最终回复...': 'progress.generatingFinalReply', - '达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary' + '达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary', + // 英文(与 en-US.json 一致,避免后端/缓存已是英文时无法随语言切换) + 'Calling AI model...': 'progress.callingAI', + 'Last iteration: generating summary and next steps...': 'progress.lastIterSummary', + 'Summary complete': 'progress.summaryDone', + 'Generating final reply...': 'progress.generatingFinalReply', + 'Max iterations reached, generating summary...': 'progress.maxIterSummary' }; if (map[trim]) return window.t(map[trim]); - const callingToolPrefix = '正在调用工具: '; - if (trim.indexOf(callingToolPrefix) === 0) { - const name = trim.slice(callingToolPrefix.length); + const callingToolPrefixCn = '正在调用工具: '; + const callingToolPrefixEn = 'Calling tool: '; + if (trim.indexOf(callingToolPrefixCn) === 0) { + const name = trim.slice(callingToolPrefixCn.length); + return window.t('progress.callingTool', { name: name }); + } + if (trim.indexOf(callingToolPrefixEn) === 0) { + const name = trim.slice(callingToolPrefixEn.length); return window.t('progress.callingTool', { name: name }); } return message; @@ -497,11 +530,12 @@ function handleStreamEvent(event, progressElement, progressId, } break; case 'iteration': - // 添加迭代标记 + // 添加迭代标记(data 属性供语言切换时重算标题) addTimelineItem(timeline, 'iteration', { title: typeof window.t === 'function' ? window.t('chat.iterationRound', { n: event.data?.iteration || 1 }) : '第 ' + (event.data?.iteration || 1) + ' 轮迭代', message: event.message, - data: event.data + data: event.data, + iterationN: event.data?.iteration || 1 }); break; @@ -569,6 +603,11 @@ function handleStreamEvent(event, progressElement, progressId, case 'progress': const progressTitle = document.querySelector(`#${progressId} .progress-title`); if (progressTitle) { + // 保存原文,语言切换时可用 translateProgressMessage 重新套当前语言 + const progressEl = document.getElementById(progressId); + if (progressEl) { + progressEl.dataset.progressRawMessage = event.message || ''; + } const progressMsg = translateProgressMessage(event.message); progressTitle.textContent = '🔍 ' + progressMsg; } @@ -855,6 +894,18 @@ function addTimelineItem(timeline, type, options) { const itemId = 'timeline-item-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9); item.id = itemId; item.className = `timeline-item timeline-item-${type}`; + // 记录类型与参数,便于 languagechange 时刷新标题文案 + item.dataset.timelineType = type; + if (type === 'iteration' && options.iterationN != null) { + item.dataset.iterationN = String(options.iterationN); + } + if (type === 'tool_calls_detected' && options.data && options.data.count != null) { + item.dataset.toolCallsCount = String(options.data.count); + } + // 保存事件时间 ISO,语言切换时可重算时间格式 + try { + item.dataset.createdAtIso = eventTime.toISOString(); + } catch (e) { /* ignore */ } // 使用传入的createdAt时间,如果没有则使用当前时间(向后兼容) let eventTime; @@ -875,8 +926,9 @@ function addTimelineItem(timeline, type, options) { eventTime = new Date(); } - const timeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US'; - const time = eventTime.toLocaleTimeString(timeLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit' }); + const timeLocale = getCurrentTimeLocale(); + const timeOpts = getTimeFormatOptions(); + const time = eventTime.toLocaleTimeString(timeLocale, timeOpts); let content = `
@@ -987,9 +1039,10 @@ function renderActiveTasks(tasks) { item.className = 'active-task-item'; const startedTime = task.startedAt ? new Date(task.startedAt) : null; - const taskTimeLocale = (typeof window.__locale === 'string' && window.__locale.startsWith('zh')) ? 'zh-CN' : 'en-US'; + const taskTimeLocale = getCurrentTimeLocale(); + const timeOpts = getTimeFormatOptions(); const timeText = startedTime && !isNaN(startedTime.getTime()) - ? startedTime.toLocaleTimeString(taskTimeLocale, { hour: '2-digit', minute: '2-digit', second: '2-digit' }) + ? startedTime.toLocaleTimeString(taskTimeLocale, timeOpts) : ''; const _t = function (k) { return typeof window.t === 'function' ? window.t(k) : k; }; @@ -1686,6 +1739,76 @@ function formatExecutionDuration(start, end) { return typeof window.t === 'function' ? window.t('mcpMonitor.durationHoursOnly', { hours: hours }) : hours + ' 小时'; } +/** + * 语言切换后刷新对话页已渲染的进度条、时间线标题与时间格式(避免仍显示英文或 AM/PM) + */ +function refreshProgressAndTimelineI18n() { + const _t = function (k, o) { + return typeof window.t === 'function' ? window.t(k, o) : k; + }; + const timeLocale = getCurrentTimeLocale(); + const timeOpts = getTimeFormatOptions(); + + // 进度块内停止按钮:未禁用时统一为当前语言的「停止任务」(避免仍显示 Stop task) + document.querySelectorAll('.progress-message .progress-stop').forEach(function (btn) { + if (!btn.disabled && btn.id && btn.id.indexOf('-stop-btn') !== -1) { + const cancelling = _t('tasks.cancelling'); + if (btn.textContent !== cancelling) { + btn.textContent = _t('tasks.stopTask'); + } + } + }); + document.querySelectorAll('.progress-toggle').forEach(function (btn) { + const timeline = btn.closest('.progress-container, .message-bubble') && + btn.closest('.progress-container, .message-bubble').querySelector('.progress-timeline'); + const expanded = timeline && timeline.classList.contains('expanded'); + btn.textContent = expanded ? _t('tasks.collapseDetail') : _t('chat.expandDetail'); + }); + document.querySelectorAll('.progress-message').forEach(function (msgEl) { + const raw = msgEl.dataset.progressRawMessage; + const titleEl = msgEl.querySelector('.progress-title'); + if (titleEl && raw) { + titleEl.textContent = '\uD83D\uDD0D ' + translateProgressMessage(raw); + } + }); + + // 时间线项:按类型重算标题,并重绘时间戳 + document.querySelectorAll('.timeline-item').forEach(function (item) { + const type = item.dataset.timelineType; + const titleSpan = item.querySelector('.timeline-item-title'); + const timeSpan = item.querySelector('.timeline-item-time'); + if (!titleSpan) return; + if (type === 'iteration' && item.dataset.iterationN) { + const n = parseInt(item.dataset.iterationN, 10) || 1; + titleSpan.textContent = _t('chat.iterationRound', { n: n }); + } else if (type === 'thinking') { + titleSpan.textContent = '\uD83E\uDD14 ' + _t('chat.aiThinking'); + } else if (type === 'tool_calls_detected' && item.dataset.toolCallsCount != null) { + const count = parseInt(item.dataset.toolCallsCount, 10) || 0; + titleSpan.textContent = '\uD83D\uDD27 ' + _t('chat.toolCallsDetected', { count: count }); + } + if (timeSpan && item.dataset.createdAtIso) { + const d = new Date(item.dataset.createdAtIso); + if (!isNaN(d.getTime())) { + timeSpan.textContent = d.toLocaleTimeString(timeLocale, timeOpts); + } + } + }); + + // 详情区「展开/收起」按钮 + document.querySelectorAll('.process-detail-btn span').forEach(function (span) { + const btn = span.closest('.process-detail-btn'); + const assistantId = btn && btn.closest('.message.assistant') && btn.closest('.message.assistant').id; + if (!assistantId) return; + const detailsId = 'process-details-' + assistantId; + const timeline = document.getElementById(detailsId) && document.getElementById(detailsId).querySelector('.progress-timeline'); + const expanded = timeline && timeline.classList.contains('expanded'); + span.textContent = expanded ? _t('tasks.collapseDetail') : _t('chat.expandDetail'); + }); +} + document.addEventListener('languagechange', function () { updateBatchActionsState(); + loadActiveTasks(); + refreshProgressAndTimelineI18n(); }); diff --git a/web/static/js/terminal.js b/web/static/js/terminal.js index b09dcb64..e93adf5c 100644 --- a/web/static/js/terminal.js +++ b/web/static/js/terminal.js @@ -26,7 +26,33 @@ return terminals[0] || null; } - var WELCOME_LINE = 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏\r\n'; + function tr(key, opts) { + if (typeof window !== 'undefined' && typeof window.t === 'function') { + return window.t(key, opts); + } + // i18n 未就绪时的后备(与 zh-CN 一致) + var fallbacks = { + 'settingsTerminal.welcomeLine': 'CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏', + 'settingsTerminal.sessionClosed': '[会话已关闭]', + 'settingsTerminal.connectionError': '[终端连接出错]', + 'settingsTerminal.connectFailed': '[无法连接终端服务: {{msg}}]', + 'settingsTerminal.closeTabTitle': '关闭', + 'settingsTerminal.containerClickTitle': '点击此处后输入命令', + 'settingsTerminal.xtermNotLoaded': '未加载 xterm.js,请刷新页面或检查网络。', + 'settingsTerminal.terminalTab': '终端 {{n}}' + }; + var s = fallbacks[key] || key; + if (opts && typeof opts === 'object') { + Object.keys(opts).forEach(function (k) { + s = s.split('{{' + k + '}}').join(String(opts[k])); + }); + } + return s; + } + + function getWelcomeLine() { + return tr('settingsTerminal.welcomeLine') + '\r\n'; + } function writePrompt(tab) { // 提示符交由后端 Shell 自行输出,这里仅保留占位函数,避免旧代码报错 @@ -35,7 +61,7 @@ function redrawTabDisplay(t) { if (!t || !t.term) return; t.term.clear(); - t.term.write(WELCOME_LINE); + t.term.write(getWelcomeLine()); } function writeln(tabOrS, s) { @@ -121,19 +147,19 @@ ws.onclose = function () { tab.running = false; if (tab.term) { - tab.term.writeln('\r\n\x1b[2m[会话已关闭]\x1b[0m'); + tab.term.writeln('\r\n\x1b[2m' + tr('settingsTerminal.sessionClosed') + '\x1b[0m'); } }; ws.onerror = function () { tab.running = false; if (tab.term) { - tab.term.writeln('\r\n\x1b[31m[终端连接出错]\x1b[0m'); + tab.term.writeln('\r\n\x1b[31m' + tr('settingsTerminal.connectionError') + '\x1b[0m'); } }; } catch (e) { if (tab.term) { - tab.term.writeln('\r\n\x1b[31m[无法连接终端服务: ' + String(e) + ']\x1b[0m'); + tab.term.writeln('\r\n\x1b[31m' + tr('settingsTerminal.connectFailed', { msg: String(e) }) + '\x1b[0m'); } } } @@ -182,13 +208,13 @@ term.loadAddon(fitAddon); } term.open(container); - term.write(WELCOME_LINE); + term.write(getWelcomeLine()); container.addEventListener('click', function () { switchTerminalTab(tab.id); if (term) term.focus(); }); container.setAttribute('tabindex', '0'); - container.title = '点击此处后输入命令'; + container.title = tr('settingsTerminal.containerClickTitle'); function sendToWS(data) { ensureTerminalWS(tab); @@ -211,6 +237,9 @@ tab.term = term; tab.fitAddon = fitAddon; + // 立即建立 WebSocket,让后端 PTY/Shell 马上启动并输出提示符; + // 若等到首次按键才 connect,用户会感觉必须先按回车才能输入(实为连接尚未建立)。 + ensureTerminalWS(tab); return term; } @@ -253,12 +282,12 @@ tabDiv.setAttribute('data-tab-id', String(id)); var label = document.createElement('span'); label.className = 'terminal-tab-label'; - label.textContent = '终端 ' + id; + label.textContent = tr('settingsTerminal.terminalTab', { n: id }); label.onclick = function () { switchTerminalTab(id); }; var closeBtn = document.createElement('button'); closeBtn.type = 'button'; closeBtn.className = 'terminal-tab-close'; - closeBtn.title = '关闭'; + closeBtn.title = tr('settingsTerminal.closeTabTitle'); closeBtn.textContent = '×'; closeBtn.onclick = function (e) { e.stopPropagation(); removeTerminalTab(id); }; tabDiv.appendChild(label); @@ -340,7 +369,7 @@ var t = terminals[i]; tabDivs[i].setAttribute('data-tab-id', String(t.id)); var lbl = tabDivs[i].querySelector('.terminal-tab-label'); - if (lbl) lbl.textContent = '终端 ' + t.id; + if (lbl) lbl.textContent = tr('settingsTerminal.terminalTab', { n: t.id }); if (lbl) lbl.onclick = (function (tid) { return function () { switchTerminalTab(tid); }; })(t.id); var cb = tabDivs[i].querySelector('.terminal-tab-close'); if (cb) cb.onclick = (function (tid) { return function (e) { e.stopPropagation(); removeTerminalTab(tid); }; })(t.id); @@ -364,6 +393,40 @@ } } + function escapeHtml(s) { + return String(s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + } + + function refreshTerminalI18n() { + // 语言切换后更新标签与容器 title;已打开的终端内容不强制清屏,以免丢失会话输出 + try { + var tabsEl = document.querySelector('.terminal-tabs'); + if (tabsEl) { + var tabDivs = tabsEl.querySelectorAll('.terminal-tab'); + for (var i = 0; i < tabDivs.length && i < terminals.length; i++) { + var tid = terminals[i].id; + var lbl = tabDivs[i].querySelector('.terminal-tab-label'); + if (lbl) lbl.textContent = tr('settingsTerminal.terminalTab', { n: tid }); + var cb = tabDivs[i].querySelector('.terminal-tab-close'); + if (cb) cb.title = tr('settingsTerminal.closeTabTitle'); + } + } + terminals.forEach(function (tab) { + if (!tab || !tab.term) return; + var cont = document.getElementById(tab.containerId); + if (cont) cont.title = tr('settingsTerminal.containerClickTitle'); + }); + } catch (e) { /* ignore */ } + } + + document.addEventListener('languagechange', function () { + refreshTerminalI18n(); + }); + function initTerminal() { var pane1 = document.getElementById('terminal-pane-1'); var container1 = document.getElementById('terminal-container-1'); @@ -377,7 +440,7 @@ inited = true; if (typeof Terminal === 'undefined') { - container1.innerHTML = '

未加载 xterm.js,请刷新页面或检查网络。

'; + container1.innerHTML = '

' + escapeHtml(tr('settingsTerminal.xtermNotLoaded')) + '

'; return; } @@ -388,6 +451,8 @@ updateTerminalTabCloseVisibility(); + refreshTerminalI18n(); + setTimeout(function () { try { if (tab.fitAddon) tab.fitAddon.fit(); if (tab.term) tab.term.focus(); } catch (e) {} }, 100); diff --git a/web/templates/index.html b/web/templates/index.html index 9669f66f..11c9af0b 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1587,7 +1587,7 @@
- 节点: 0 | 边: 0 + Nodes: 0 | Edges: 0