Add files via upload

This commit is contained in:
公明
2026-03-12 21:17:22 +08:00
committed by GitHub
parent 872e570518
commit ee9559e074
8 changed files with 359 additions and 55 deletions
+7
View File
@@ -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": "+"
},
+7
View File
@@ -1028,6 +1028,13 @@
"title": "终端",
"description": "在服务器上执行命令,便于运维与调试。命令在服务端执行,请勿执行敏感或破坏性操作。",
"terminalTab": "终端 {{n}}",
"welcomeLine": "CyberStrikeAI 终端 - 真实 Shell 会话,直接输入命令;Ctrl+L 清屏",
"sessionClosed": "[会话已关闭]",
"connectionError": "[终端连接出错]",
"connectFailed": "[无法连接终端服务: {{msg}}]",
"closeTabTitle": "关闭",
"containerClickTitle": "点击此处后输入命令",
"xtermNotLoaded": "未加载 xterm.js,请刷新页面或检查网络。",
"close": "×",
"newTerminal": "+"
},
+8
View File
@@ -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;
}
+109 -32
View File
@@ -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, '<br>');
}
} else {
formattedContent = escapeHtmlLocal(text).replace(/\n/g, '<br>');
}
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 = '<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>' + window.t('common.copy') + '</span>';
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();
}
});
// 显示创建分组模态框
+17
View File
@@ -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();
});
});
})();
+134 -11
View File
@@ -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 = `
<div class="timeline-item-header">
@@ -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();
});
+76 -11
View File
@@ -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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
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 = '<p class="terminal-error">未加载 xterm.js,请刷新页面或检查网络。</p>';
container1.innerHTML = '<p class="terminal-error">' + escapeHtml(tr('settingsTerminal.xtermNotLoaded')) + '</p>';
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);
+1 -1
View File
@@ -1587,7 +1587,7 @@
<div class="attack-chain-visualization-area">
<div class="attack-chain-toolbar">
<div class="attack-chain-info">
<span id="attack-chain-stats">节点: 0 | : 0</span>
<span id="attack-chain-stats">Nodes: 0 | Edges: 0</span>
</div>
<div class="attack-chain-filters">
<input type="text" id="attack-chain-search" data-i18n="attackChainModal.searchPlaceholder" data-i18n-attr="placeholder" placeholder="搜索节点..."