mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-03 03:05:57 +02:00
Add files via upload
This commit is contained in:
@@ -9092,6 +9092,247 @@ header {
|
||||
min-width: 72px;
|
||||
}
|
||||
|
||||
/* WebShell AI 助手 Tab:仅在此 pane 激活时显示;左右布局 = 左侧栏 + 右侧主区(与对话页一致) */
|
||||
#webshell-pane-ai {
|
||||
flex-direction: row;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* 不设 display,避免覆盖 .webshell-pane 的 display:none(否则终端/文件管理页会露出 AI 面板)。左右布局由 #webshell-pane-ai 的 flex-direction:row 提供 */
|
||||
.webshell-pane-ai-with-sidebar {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.webshell-ai-sidebar {
|
||||
flex-shrink: 0;
|
||||
width: 280px;
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: 1px solid var(--border-color);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
.webshell-ai-new-btn {
|
||||
margin: 10px 12px;
|
||||
width: calc(100% - 24px);
|
||||
}
|
||||
.webshell-ai-conv-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.webshell-ai-conv-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
border-left: 3px solid transparent;
|
||||
}
|
||||
.webshell-ai-conv-item:hover {
|
||||
background: var(--border-color);
|
||||
}
|
||||
.webshell-ai-conv-item.active {
|
||||
background: var(--accent-light, rgba(59, 130, 246, 0.1));
|
||||
border-left-color: var(--accent-color);
|
||||
}
|
||||
.webshell-ai-conv-item-title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.webshell-ai-conv-item-date {
|
||||
flex-shrink: 0;
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.webshell-ai-conv-del {
|
||||
flex-shrink: 0;
|
||||
padding: 2px 6px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.webshell-ai-conv-item:hover .webshell-ai-conv-del {
|
||||
opacity: 1;
|
||||
}
|
||||
.webshell-ai-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
.webshell-ai-hint {
|
||||
flex-shrink: 0;
|
||||
padding: 10px 14px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.webshell-ai-toolbar {
|
||||
flex-shrink: 0;
|
||||
padding: 6px 14px 8px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.webshell-ai-progress {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
padding: 6px 12px;
|
||||
margin: 0 0 4px 0;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
align-self: flex-start;
|
||||
}
|
||||
.webshell-ai-timeline {
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.webshell-ai-timeline.has-items {
|
||||
display: flex;
|
||||
}
|
||||
.webshell-ai-timeline-item {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-primary);
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
.webshell-ai-timeline-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.webshell-ai-timeline-title {
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
.webshell-ai-timeline-msg {
|
||||
margin-top: 4px;
|
||||
padding-left: 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.webshell-ai-old-conv {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-secondary);
|
||||
overflow: hidden;
|
||||
}
|
||||
.webshell-ai-old-conv-label {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.webshell-ai-old-conv-label:hover {
|
||||
background: var(--border-color);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.webshell-ai-old-conv-body {
|
||||
padding: 0 12px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.webshell-ai-messages {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 12px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
.webshell-ai-msg {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
max-width: 90%;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.webshell-ai-msg.user {
|
||||
align-self: flex-end;
|
||||
background: var(--accent-color);
|
||||
color: #fff;
|
||||
}
|
||||
.webshell-ai-msg.assistant {
|
||||
align-self: flex-start;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
.webshell-ai-input-row {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 8px 14px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
align-items: center;
|
||||
}
|
||||
.webshell-ai-input {
|
||||
flex: 1;
|
||||
height: 36px;
|
||||
min-height: 36px;
|
||||
resize: none;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.95rem;
|
||||
/* 更柔和的滚动条样式 */
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: rgba(15, 23, 42, 0.25) transparent;
|
||||
}
|
||||
.webshell-ai-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent-color);
|
||||
}
|
||||
.webshell-ai-input::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.webshell-ai-input::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.webshell-ai-input::-webkit-scrollbar-thumb {
|
||||
background: rgba(15, 23, 42, 0.25);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.webshell-ai-input::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
}
|
||||
.webshell-ai-input-row .btn-primary {
|
||||
flex-shrink: 0;
|
||||
height: 36px;
|
||||
min-width: 72px;
|
||||
padding: 0 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 仪表盘页面样式(最佳实践布局 + 视觉增强) */
|
||||
.dashboard-page {
|
||||
height: 100%;
|
||||
|
||||
@@ -355,6 +355,14 @@
|
||||
"editConnectionTitle": "Edit connection",
|
||||
"tabTerminal": "Virtual terminal",
|
||||
"tabFileManager": "File manager",
|
||||
"tabAiAssistant": "AI Assistant",
|
||||
"aiSystemReadyMessage": "System is ready. Please enter your test requirements, and the system will automatically perform the corresponding security tests.",
|
||||
"aiNewConversation": "New conversation",
|
||||
"aiPreviousConversation": "Previous conversation",
|
||||
"aiDeleteConversation": "Delete conversation",
|
||||
"aiDeleteConversationConfirm": "Delete this conversation?",
|
||||
"aiPlaceholder": "e.g. List files in the current directory",
|
||||
"aiSend": "Send",
|
||||
"quickCommands": "Quick commands",
|
||||
"downloadFile": "Download",
|
||||
"terminalWelcome": "WebShell virtual terminal — type a command and press Enter (Ctrl+L clear)",
|
||||
|
||||
@@ -355,6 +355,14 @@
|
||||
"editConnectionTitle": "编辑连接",
|
||||
"tabTerminal": "虚拟终端",
|
||||
"tabFileManager": "文件管理",
|
||||
"tabAiAssistant": "AI 助手",
|
||||
"aiSystemReadyMessage": "系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。",
|
||||
"aiNewConversation": "新对话",
|
||||
"aiPreviousConversation": "之前的对话",
|
||||
"aiDeleteConversation": "删除对话",
|
||||
"aiDeleteConversationConfirm": "确定删除当前对话记录?",
|
||||
"aiPlaceholder": "例如:列出当前目录下的文件",
|
||||
"aiSend": "发送",
|
||||
"quickCommands": "快捷命令",
|
||||
"downloadFile": "下载",
|
||||
"terminalWelcome": "WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)",
|
||||
|
||||
+379
-1
@@ -20,6 +20,11 @@ let webshellHistoryIndex = -1;
|
||||
const WEBSHELL_HISTORY_MAX = 100;
|
||||
// 清屏防重入:一次点击只执行一次(避免多次绑定或重复触发导致多个 shell>)
|
||||
let webshellClearInProgress = false;
|
||||
// AI 助手:按连接 ID 保存对话 ID,便于多轮对话
|
||||
let webshellAiConvMap = {};
|
||||
let webshellAiSending = false;
|
||||
// 流式打字机效果:当前会话的 response 序号,用于中止过期的打字
|
||||
let webshellStreamingTypingId = 0;
|
||||
|
||||
// 从服务端(SQLite)拉取连接列表
|
||||
function getWebshellConnections() {
|
||||
@@ -61,6 +66,10 @@ function wsT(key) {
|
||||
'webshell.editConnectionTitle': '编辑连接',
|
||||
'webshell.tabTerminal': '虚拟终端',
|
||||
'webshell.tabFileManager': '文件管理',
|
||||
'webshell.tabAiAssistant': 'AI 助手',
|
||||
'webshell.aiSystemReadyMessage': '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。',
|
||||
'webshell.aiPlaceholder': '例如:列出当前目录下的文件',
|
||||
'webshell.aiSend': '发送',
|
||||
'webshell.terminalWelcome': 'WebShell 虚拟终端 — 输入命令后按回车执行(Ctrl+L 清屏)',
|
||||
'webshell.quickCommands': '快捷命令',
|
||||
'webshell.downloadFile': '下载',
|
||||
@@ -268,6 +277,95 @@ function escapeHtml(s) {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
function formatWebshellAiConvDate(updatedAt) {
|
||||
if (!updatedAt) return '';
|
||||
var d = typeof updatedAt === 'string' ? new Date(updatedAt) : updatedAt;
|
||||
if (isNaN(d.getTime())) return '';
|
||||
var now = new Date();
|
||||
var sameDay = d.getDate() === now.getDate() && d.getMonth() === now.getMonth() && d.getFullYear() === now.getFullYear();
|
||||
if (sameDay) return d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0');
|
||||
return (d.getMonth() + 1) + '/' + d.getDate();
|
||||
}
|
||||
|
||||
function fetchAndRenderWebshellAiConvList(conn, listEl) {
|
||||
if (!conn || !conn.id || !listEl || typeof apiFetch !== 'function') return Promise.resolve();
|
||||
return apiFetch('/api/webshell/connections/' + encodeURIComponent(conn.id) + '/ai-conversations', { method: 'GET' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (list) {
|
||||
if (!Array.isArray(list)) list = [];
|
||||
listEl.innerHTML = '';
|
||||
list.forEach(function (item) {
|
||||
var row = document.createElement('div');
|
||||
row.className = 'webshell-ai-conv-item';
|
||||
row.dataset.convId = item.id;
|
||||
var title = (item.title || '').trim() || item.id.slice(0, 8);
|
||||
var dateStr = item.updatedAt ? formatWebshellAiConvDate(item.updatedAt) : '';
|
||||
row.innerHTML = '<span class="webshell-ai-conv-item-title">' + escapeHtml(title) + '</span><span class="webshell-ai-conv-item-date">' + escapeHtml(dateStr) + '</span>';
|
||||
if (webshellAiConvMap[conn.id] === item.id) row.classList.add('active');
|
||||
row.addEventListener('click', function () {
|
||||
webshellAiConvListSelect(conn, item.id, document.getElementById('webshell-ai-messages'), listEl);
|
||||
});
|
||||
var delBtn = document.createElement('button');
|
||||
delBtn.type = 'button';
|
||||
delBtn.className = 'btn-ghost btn-sm webshell-ai-conv-del';
|
||||
delBtn.textContent = '×';
|
||||
delBtn.title = wsT('webshell.aiDeleteConversation') || '删除对话';
|
||||
delBtn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (!confirm(wsT('webshell.aiDeleteConversationConfirm') || '确定删除该对话?')) return;
|
||||
apiFetch('/api/conversations/' + encodeURIComponent(item.id), { method: 'DELETE' })
|
||||
.then(function (r) {
|
||||
if (r.ok) {
|
||||
if (webshellAiConvMap[conn.id] === item.id) {
|
||||
delete webshellAiConvMap[conn.id];
|
||||
var msgs = document.getElementById('webshell-ai-messages');
|
||||
if (msgs) msgs.innerHTML = '';
|
||||
}
|
||||
fetchAndRenderWebshellAiConvList(conn, listEl);
|
||||
}
|
||||
})
|
||||
.catch(function (e) { console.warn('删除对话失败', e); });
|
||||
});
|
||||
row.appendChild(delBtn);
|
||||
listEl.appendChild(row);
|
||||
});
|
||||
})
|
||||
.catch(function (e) { console.warn('加载对话列表失败', e); });
|
||||
}
|
||||
|
||||
function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
|
||||
if (!conn || !convId || !messagesContainer) return;
|
||||
webshellAiConvMap[conn.id] = convId;
|
||||
if (listEl) listEl.querySelectorAll('.webshell-ai-conv-item').forEach(function (el) {
|
||||
el.classList.toggle('active', el.dataset.convId === convId);
|
||||
});
|
||||
if (typeof apiFetch !== 'function') return;
|
||||
apiFetch('/api/conversations/' + encodeURIComponent(convId), { method: 'GET' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
messagesContainer.innerHTML = '';
|
||||
var list = data.messages || [];
|
||||
list.forEach(function (msg) {
|
||||
var role = (msg.role || '').toLowerCase();
|
||||
var content = (msg.content || '').trim();
|
||||
if (!content && role !== 'assistant') return;
|
||||
var div = document.createElement('div');
|
||||
div.className = 'webshell-ai-msg ' + (role === 'user' ? 'user' : 'assistant');
|
||||
div.textContent = content;
|
||||
messagesContainer.appendChild(div);
|
||||
});
|
||||
if (list.length === 0) {
|
||||
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
var readyDiv = document.createElement('div');
|
||||
readyDiv.className = 'webshell-ai-msg assistant';
|
||||
readyDiv.textContent = readyMsg;
|
||||
messagesContainer.appendChild(readyDiv);
|
||||
}
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
})
|
||||
.catch(function (e) { console.warn('加载对话失败', e); });
|
||||
}
|
||||
|
||||
// 选择连接:渲染终端 + 文件管理 Tab,并初始化终端
|
||||
function selectWebshell(id) {
|
||||
currentWebshellId = id;
|
||||
@@ -287,6 +385,7 @@ function selectWebshell(id) {
|
||||
'<div class="webshell-tabs">' +
|
||||
'<button type="button" class="webshell-tab active" data-tab="terminal">' + wsT('webshell.tabTerminal') + '</button>' +
|
||||
'<button type="button" class="webshell-tab" data-tab="file">' + wsT('webshell.tabFileManager') + '</button>' +
|
||||
'<button type="button" class="webshell-tab" data-tab="ai">' + (wsT('webshell.tabAiAssistant') || 'AI 助手') + '</button>' +
|
||||
'</div>' +
|
||||
'<div id="webshell-pane-terminal" class="webshell-pane active">' +
|
||||
'<div class="webshell-terminal-toolbar">' +
|
||||
@@ -321,6 +420,19 @@ function selectWebshell(id) {
|
||||
'<button type="button" class="btn-ghost" id="webshell-batch-download-btn">' + (wsT('webshell.batchDownload') || '批量下载') + '</button>' +
|
||||
'</div>' +
|
||||
'<div id="webshell-file-list" class="webshell-file-list"></div>' +
|
||||
'</div>' +
|
||||
'<div id="webshell-pane-ai" class="webshell-pane webshell-pane-ai-with-sidebar">' +
|
||||
'<div class="webshell-ai-sidebar">' +
|
||||
'<button type="button" class="btn-primary btn-sm webshell-ai-new-btn" id="webshell-ai-new-conv">' + (wsT('webshell.aiNewConversation') || '新对话') + '</button>' +
|
||||
'<div class="webshell-ai-conv-list" id="webshell-ai-conv-list"></div>' +
|
||||
'</div>' +
|
||||
'<div class="webshell-ai-main">' +
|
||||
'<div id="webshell-ai-messages" class="webshell-ai-messages"></div>' +
|
||||
'<div class="webshell-ai-input-row">' +
|
||||
'<textarea id="webshell-ai-input" class="webshell-ai-input form-control" rows="2" placeholder="' + (wsT('webshell.aiPlaceholder') || '例如:列出当前目录下的文件') + '"></textarea>' +
|
||||
'<button type="button" class="btn-primary" id="webshell-ai-send">' + (wsT('webshell.aiSend') || '发送') + '</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
// Tab 切换
|
||||
@@ -376,9 +488,251 @@ function selectWebshell(id) {
|
||||
document.getElementById('webshell-batch-delete-btn').addEventListener('click', function () { webshellBatchDelete(webshellCurrentConn, pathInput); });
|
||||
document.getElementById('webshell-batch-download-btn').addEventListener('click', function () { webshellBatchDownload(webshellCurrentConn, pathInput); });
|
||||
|
||||
// AI 助手:侧边栏对话列表 + 主区消息
|
||||
var aiInput = document.getElementById('webshell-ai-input');
|
||||
var aiSendBtn = document.getElementById('webshell-ai-send');
|
||||
var aiMessages = document.getElementById('webshell-ai-messages');
|
||||
var aiNewConvBtn = document.getElementById('webshell-ai-new-conv');
|
||||
var aiConvListEl = document.getElementById('webshell-ai-conv-list');
|
||||
|
||||
if (aiNewConvBtn) {
|
||||
aiNewConvBtn.addEventListener('click', function () {
|
||||
delete webshellAiConvMap[conn.id];
|
||||
if (aiMessages) {
|
||||
aiMessages.innerHTML = '';
|
||||
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
var div = document.createElement('div');
|
||||
div.className = 'webshell-ai-msg assistant';
|
||||
div.textContent = readyMsg;
|
||||
aiMessages.appendChild(div);
|
||||
}
|
||||
if (aiConvListEl) aiConvListEl.querySelectorAll('.webshell-ai-conv-item').forEach(function (el) { el.classList.remove('active'); });
|
||||
});
|
||||
}
|
||||
if (aiSendBtn && aiInput && aiMessages) {
|
||||
aiSendBtn.addEventListener('click', function () { runWebshellAiSend(conn, aiInput, aiSendBtn, aiMessages); });
|
||||
aiInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
runWebshellAiSend(conn, aiInput, aiSendBtn, aiMessages);
|
||||
}
|
||||
});
|
||||
fetchAndRenderWebshellAiConvList(conn, aiConvListEl).then(function () {
|
||||
loadWebshellAiHistory(conn, aiMessages).then(function () {
|
||||
if (webshellAiConvMap[conn.id] && aiConvListEl) {
|
||||
aiConvListEl.querySelectorAll('.webshell-ai-conv-item').forEach(function (el) {
|
||||
el.classList.toggle('active', el.dataset.convId === webshellAiConvMap[conn.id]);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initWebshellTerminal(conn);
|
||||
}
|
||||
|
||||
// 加载 WebShell 连接的 AI 助手对话历史(持久化展示),返回 Promise 供 .then 更新工具栏等
|
||||
function loadWebshellAiHistory(conn, messagesContainer) {
|
||||
if (!conn || !conn.id || !messagesContainer) return Promise.resolve();
|
||||
if (typeof apiFetch !== 'function') return Promise.resolve();
|
||||
return apiFetch('/api/webshell/connections/' + encodeURIComponent(conn.id) + '/ai-history', { method: 'GET' })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
if (data.conversationId) webshellAiConvMap[conn.id] = data.conversationId;
|
||||
var list = Array.isArray(data.messages) ? data.messages : [];
|
||||
list.forEach(function (msg) {
|
||||
var role = (msg.role || '').toLowerCase();
|
||||
var content = (msg.content || '').trim();
|
||||
if (!content && role !== 'assistant') return;
|
||||
var div = document.createElement('div');
|
||||
div.className = 'webshell-ai-msg ' + (role === 'user' ? 'user' : 'assistant');
|
||||
div.textContent = content;
|
||||
messagesContainer.appendChild(div);
|
||||
});
|
||||
if (list.length === 0) {
|
||||
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
var readyDiv = document.createElement('div');
|
||||
readyDiv.className = 'webshell-ai-msg assistant';
|
||||
readyDiv.textContent = readyMsg;
|
||||
messagesContainer.appendChild(readyDiv);
|
||||
}
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
})
|
||||
.catch(function (e) {
|
||||
console.warn('加载 WebShell AI 历史失败', conn.id, e);
|
||||
});
|
||||
}
|
||||
|
||||
function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
if (!conn || !conn.id) return;
|
||||
var message = (inputEl && inputEl.value || '').trim();
|
||||
if (!message) return;
|
||||
if (webshellAiSending) return;
|
||||
if (typeof apiFetch !== 'function') {
|
||||
if (messagesContainer) {
|
||||
var errDiv = document.createElement('div');
|
||||
errDiv.className = 'webshell-ai-msg assistant';
|
||||
errDiv.textContent = '无法发送:未登录或 apiFetch 不可用';
|
||||
messagesContainer.appendChild(errDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
webshellAiSending = true;
|
||||
if (sendBtn) sendBtn.disabled = true;
|
||||
|
||||
var userDiv = document.createElement('div');
|
||||
userDiv.className = 'webshell-ai-msg user';
|
||||
userDiv.textContent = message;
|
||||
messagesContainer.appendChild(userDiv);
|
||||
|
||||
var timelineContainer = document.createElement('div');
|
||||
timelineContainer.className = 'webshell-ai-timeline';
|
||||
timelineContainer.setAttribute('aria-live', 'polite');
|
||||
|
||||
var assistantDiv = document.createElement('div');
|
||||
assistantDiv.className = 'webshell-ai-msg assistant';
|
||||
assistantDiv.textContent = '…';
|
||||
messagesContainer.appendChild(timelineContainer);
|
||||
messagesContainer.appendChild(assistantDiv);
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
|
||||
function appendTimelineItem(type, title, message) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'webshell-ai-timeline-item webshell-ai-timeline-' + type;
|
||||
item.innerHTML = '<span class="webshell-ai-timeline-title">' + escapeHtml(title || message || '') + '</span>';
|
||||
if (message && message !== title) item.innerHTML += '<div class="webshell-ai-timeline-msg">' + escapeHtml(message) + '</div>';
|
||||
timelineContainer.appendChild(item);
|
||||
timelineContainer.classList.add('has-items');
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
}
|
||||
|
||||
if (inputEl) inputEl.value = '';
|
||||
|
||||
var convId = webshellAiConvMap[conn.id] || '';
|
||||
var body = {
|
||||
message: message,
|
||||
webshellConnectionId: conn.id,
|
||||
conversationId: convId
|
||||
};
|
||||
|
||||
// 流式输出:支持 progress 实时更新、response 打字机效果;若后端发送多段 response 则追加
|
||||
var streamingTarget = ''; // 当前要打字显示的目标全文(用于打字机效果)
|
||||
var streamingTypingId = 0; // 防重入,每次新 response 自增
|
||||
|
||||
apiFetch('/api/agent-loop/stream', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
}).then(function (response) {
|
||||
if (!response.ok) {
|
||||
assistantDiv.textContent = '请求失败: ' + response.status;
|
||||
return;
|
||||
}
|
||||
return response.body.getReader();
|
||||
}).then(function (reader) {
|
||||
if (!reader) return;
|
||||
var decoder = new TextDecoder();
|
||||
var buffer = '';
|
||||
return reader.read().then(function processChunk(result) {
|
||||
if (result.done) return;
|
||||
buffer += decoder.decode(result.value, { stream: true });
|
||||
var lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
var line = lines[i];
|
||||
if (line.indexOf('data: ') !== 0) continue;
|
||||
try {
|
||||
var eventData = JSON.parse(line.slice(6));
|
||||
if (eventData.type === 'conversation' && eventData.data && eventData.data.conversationId) {
|
||||
webshellAiConvMap[conn.id] = eventData.data.conversationId;
|
||||
var listEl = document.getElementById('webshell-ai-conv-list');
|
||||
if (listEl) fetchAndRenderWebshellAiConvList(conn, listEl).then(function () {
|
||||
listEl.querySelectorAll('.webshell-ai-conv-item').forEach(function (el) {
|
||||
el.classList.toggle('active', el.dataset.convId === eventData.data.conversationId);
|
||||
});
|
||||
});
|
||||
} else if (eventData.type === 'response') {
|
||||
var text = (eventData.message != null && eventData.message !== '') ? eventData.message : (eventData.data && typeof eventData.data === 'string' ? eventData.data : '');
|
||||
if (text) {
|
||||
streamingTarget += text;
|
||||
webshellStreamingTypingId += 1;
|
||||
streamingTypingId = webshellStreamingTypingId;
|
||||
runWebshellAiStreamingTyping(assistantDiv, streamingTarget, streamingTypingId, messagesContainer);
|
||||
}
|
||||
} else if (eventData.type === 'error' && eventData.message) {
|
||||
streamingTypingId += 1;
|
||||
appendTimelineItem('error', '❌ 错误', eventData.message);
|
||||
assistantDiv.textContent = '错误: ' + eventData.message;
|
||||
} else if (eventData.type === 'progress' && eventData.message) {
|
||||
appendTimelineItem('progress', '🔍 ' + eventData.message, '');
|
||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||
} else if (eventData.type === 'iteration') {
|
||||
var iterN = (eventData.data && eventData.data.iteration) || 0;
|
||||
var iterTitle = iterN ? '🔍 第 ' + iterN + ' 轮迭代' : ('🔍 ' + (eventData.message || '迭代'));
|
||||
appendTimelineItem('iteration', iterTitle, eventData.message || '');
|
||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||
} else if (eventData.type === 'thinking' && eventData.message) {
|
||||
appendTimelineItem('thinking', '🤔 AI 思考', eventData.message);
|
||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||
} else if (eventData.type === 'tool_calls_detected' && eventData.data) {
|
||||
var count = eventData.data.count || 0;
|
||||
appendTimelineItem('tool_calls_detected', '🔧 检测到 ' + count + ' 个工具调用', eventData.message || '');
|
||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||
} else if (eventData.type === 'tool_call' && eventData.data) {
|
||||
var d = eventData.data;
|
||||
var tn = d.toolName || '未知工具';
|
||||
var idx = d.index || 0;
|
||||
var total = d.total || 0;
|
||||
var title = '🔧 调用: ' + tn + (total ? ' (' + idx + '/' + total + ')' : '');
|
||||
appendTimelineItem('tool_call', title, eventData.message || '');
|
||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||
} else if (eventData.type === 'tool_result' && eventData.data) {
|
||||
var dr = eventData.data;
|
||||
var success = dr.success !== false;
|
||||
var tname = dr.toolName || '工具';
|
||||
var title = (success ? '✅ ' : '❌ ') + tname + (success ? ' 执行完成' : ' 执行失败');
|
||||
var sub = eventData.message || (dr.result ? String(dr.result).slice(0, 300) : '');
|
||||
appendTimelineItem('tool_result', title, sub);
|
||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||
}
|
||||
} catch (e) { /* ignore parse error */ }
|
||||
}
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
return reader.read().then(processChunk);
|
||||
});
|
||||
}).catch(function (err) {
|
||||
assistantDiv.textContent = '请求异常: ' + (err && err.message ? err.message : String(err));
|
||||
}).then(function () {
|
||||
webshellAiSending = false;
|
||||
if (sendBtn) sendBtn.disabled = false;
|
||||
if (assistantDiv.textContent === '…' && !streamingTarget) assistantDiv.textContent = '无回复内容';
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
});
|
||||
}
|
||||
|
||||
// 打字机效果:将 target 逐字/逐段写入 el,保证只生效于当前 id 的调用
|
||||
function runWebshellAiStreamingTyping(el, target, id, scrollContainer) {
|
||||
if (!el || id === undefined) return;
|
||||
var chunkSize = 3;
|
||||
var delayMs = 24;
|
||||
function tick() {
|
||||
if (id !== webshellStreamingTypingId) return;
|
||||
var cur = el.textContent || '';
|
||||
if (cur.length >= target.length) {
|
||||
el.textContent = target;
|
||||
if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
||||
return;
|
||||
}
|
||||
var next = target.slice(0, cur.length + chunkSize);
|
||||
el.textContent = next;
|
||||
if (scrollContainer) scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
||||
setTimeout(tick, delayMs);
|
||||
}
|
||||
if (el.textContent.length < target.length) setTimeout(tick, delayMs);
|
||||
}
|
||||
|
||||
function getWebshellHistory(connId) {
|
||||
if (!connId) return [];
|
||||
if (!webshellHistoryByConn[connId]) webshellHistoryByConn[connId] = [];
|
||||
@@ -1058,7 +1412,6 @@ function refreshWebshellUIOnLanguageChange() {
|
||||
if (page !== 'webshell') return;
|
||||
|
||||
renderWebshellList();
|
||||
|
||||
var workspace = document.getElementById('webshell-workspace');
|
||||
if (workspace) {
|
||||
if (!currentWebshellId || !webshellCurrentConn) {
|
||||
@@ -1067,8 +1420,10 @@ function refreshWebshellUIOnLanguageChange() {
|
||||
// 只更新标签文案,不重建终端
|
||||
var tabTerminal = workspace.querySelector('.webshell-tab[data-tab="terminal"]');
|
||||
var tabFile = workspace.querySelector('.webshell-tab[data-tab="file"]');
|
||||
var tabAi = workspace.querySelector('.webshell-tab[data-tab="ai"]');
|
||||
if (tabTerminal) tabTerminal.textContent = wsT('webshell.tabTerminal');
|
||||
if (tabFile) tabFile.textContent = wsT('webshell.tabFileManager');
|
||||
if (tabAi) tabAi.textContent = wsT('webshell.tabAiAssistant') || 'AI 助手';
|
||||
|
||||
var quickLabel = workspace.querySelector('.webshell-quick-label');
|
||||
if (quickLabel) quickLabel.textContent = (wsT('webshell.quickCommands') || '快捷命令') + ':';
|
||||
@@ -1094,6 +1449,29 @@ function refreshWebshellUIOnLanguageChange() {
|
||||
if (batchDownloadBtn) batchDownloadBtn.textContent = wsT('webshell.batchDownload') || '批量下载';
|
||||
if (filterInput) filterInput.placeholder = wsT('webshell.filterPlaceholder') || '过滤文件名';
|
||||
|
||||
// AI 助手区域文案:Tab 内按钮、占位符、系统就绪提示
|
||||
var aiNewConvBtn = document.getElementById('webshell-ai-new-conv');
|
||||
if (aiNewConvBtn) aiNewConvBtn.textContent = wsT('webshell.aiNewConversation') || '新对话';
|
||||
var aiInput = document.getElementById('webshell-ai-input');
|
||||
if (aiInput) aiInput.placeholder = wsT('webshell.aiPlaceholder') || '例如:列出当前目录下的文件';
|
||||
var aiSendBtn = document.getElementById('webshell-ai-send');
|
||||
if (aiSendBtn) aiSendBtn.textContent = wsT('webshell.aiSend') || '发送';
|
||||
|
||||
// 如果当前 AI 对话区只有系统就绪提示(没有用户消息),用当前语言重置这条提示
|
||||
var aiMessages = document.getElementById('webshell-ai-messages');
|
||||
if (aiMessages) {
|
||||
var hasUserMsg = !!aiMessages.querySelector('.webshell-ai-msg.user');
|
||||
var msgNodes = aiMessages.querySelectorAll('.webshell-ai-msg');
|
||||
if (!hasUserMsg && msgNodes.length <= 1) {
|
||||
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
aiMessages.innerHTML = '';
|
||||
var readyDiv = document.createElement('div');
|
||||
readyDiv.className = 'webshell-ai-msg assistant';
|
||||
readyDiv.textContent = readyMsg;
|
||||
aiMessages.appendChild(readyDiv);
|
||||
}
|
||||
}
|
||||
|
||||
var pathInput = document.getElementById('webshell-file-path');
|
||||
var fileListEl = document.getElementById('webshell-file-list');
|
||||
if (fileListEl && webshellCurrentConn && pathInput) {
|
||||
|
||||
Reference in New Issue
Block a user