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 = `
未加载 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 @@