mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-05-15 04:51:01 +02:00
Add files via upload
This commit is contained in:
@@ -103,6 +103,37 @@ func (db *DB) GetConversationByWebshellConnectionID(connectionID string) (*Conve
|
||||
return nil, fmt.Errorf("加载消息失败: %w", err)
|
||||
}
|
||||
conv.Messages = messages
|
||||
|
||||
// 加载过程详情并附加到对应消息(与 GetConversation 一致,便于刷新后仍可查看执行过程)
|
||||
processDetailsMap, err := db.GetProcessDetailsByConversation(conv.ID)
|
||||
if err != nil {
|
||||
db.logger.Warn("加载过程详情失败", zap.Error(err))
|
||||
processDetailsMap = make(map[string][]ProcessDetail)
|
||||
}
|
||||
for i := range conv.Messages {
|
||||
if details, ok := processDetailsMap[conv.Messages[i].ID]; ok {
|
||||
detailsJSON := make([]map[string]interface{}, len(details))
|
||||
for j, detail := range details {
|
||||
var data interface{}
|
||||
if detail.Data != "" {
|
||||
if err := json.Unmarshal([]byte(detail.Data), &data); err != nil {
|
||||
db.logger.Warn("解析过程详情数据失败", zap.Error(err))
|
||||
}
|
||||
}
|
||||
detailsJSON[j] = map[string]interface{}{
|
||||
"id": detail.ID,
|
||||
"messageId": detail.MessageID,
|
||||
"conversationId": detail.ConversationID,
|
||||
"eventType": detail.EventType,
|
||||
"message": detail.Message,
|
||||
"data": data,
|
||||
"createdAt": detail.CreatedAt,
|
||||
}
|
||||
}
|
||||
conv.Messages[i].ProcessDetails = detailsJSON
|
||||
}
|
||||
}
|
||||
|
||||
return &conv, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -51,11 +51,14 @@ body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
/* 主侧边栏与右侧内容之间预留水平间距,避免导航项文字贴到内容边框 */
|
||||
column-gap: 12px;
|
||||
}
|
||||
|
||||
/* 主侧边栏样式 - 紧凑宽度,参考常见后台 200~220px */
|
||||
.main-sidebar {
|
||||
width: 208px;
|
||||
/* 稍微拉宽侧边栏,给多语言菜单文案更多缓冲空间 */
|
||||
width: 224px;
|
||||
background: linear-gradient(180deg, #fafbfc 0%, #f5f7fa 100%);
|
||||
color: var(--text-primary);
|
||||
display: flex;
|
||||
@@ -164,7 +167,8 @@ body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 16px;
|
||||
/* 侧边栏导航项:左 16px 对齐图标,右 32px 预留更大安全间距,避免长文案贴边 */
|
||||
padding: 10px 32px 10px 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
color: var(--text-primary);
|
||||
@@ -240,6 +244,9 @@ body {
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 400;
|
||||
white-space: nowrap;
|
||||
/* 防止长标题顶到边界:在右侧内边距内做省略而不是越界 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
@@ -9230,6 +9237,35 @@ header {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.webshell-ai-process-block.process-details-container {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.webshell-ai-process-toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.webshell-ai-process-toggle:hover {
|
||||
color: var(--text-primary);
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
.webshell-ai-process-block .process-details-content .progress-timeline {
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
.webshell-ai-process-block .process-details-content .progress-timeline.expanded {
|
||||
max-height: 2000px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.webshell-ai-old-conv {
|
||||
width: 100%;
|
||||
margin-bottom: 8px;
|
||||
|
||||
@@ -171,7 +171,9 @@
|
||||
"lastIterSummary": "Last iteration: generating summary and next steps...",
|
||||
"summaryDone": "Summary complete",
|
||||
"generatingFinalReply": "Generating final reply...",
|
||||
"maxIterSummary": "Max iterations reached, generating summary..."
|
||||
"maxIterSummary": "Max iterations reached, generating summary...",
|
||||
"analyzingRequestShort": "Analyzing your request...",
|
||||
"analyzingRequestPlanning": "Analyzing your request and planning test strategy..."
|
||||
},
|
||||
"timeline": {
|
||||
"params": "Parameters:",
|
||||
|
||||
@@ -171,7 +171,9 @@
|
||||
"lastIterSummary": "最后一次迭代:正在生成总结和下一步计划...",
|
||||
"summaryDone": "总结生成完成",
|
||||
"generatingFinalReply": "正在生成最终回复...",
|
||||
"maxIterSummary": "达到最大迭代次数,正在生成总结..."
|
||||
"maxIterSummary": "达到最大迭代次数,正在生成总结...",
|
||||
"analyzingRequestShort": "正在分析您的请求...",
|
||||
"analyzingRequestPlanning": "开始分析请求并制定测试策略"
|
||||
},
|
||||
"timeline": {
|
||||
"params": "参数:",
|
||||
|
||||
+29
-2
@@ -2243,8 +2243,16 @@ async function deleteConversation(conversationId, skipConfirm = false) {
|
||||
await loadGroupConversations(currentGroupId);
|
||||
}
|
||||
|
||||
// 刷新对话列表
|
||||
loadConversations();
|
||||
// 刷新对话列表(使用分组接口以与其他入口一致)
|
||||
if (typeof loadConversationsWithGroups === 'function') {
|
||||
loadConversationsWithGroups();
|
||||
} else if (typeof loadConversations === 'function') {
|
||||
loadConversations();
|
||||
}
|
||||
// 通知其他模块(如 WebShell AI 助手)同步删除,保持列表一致
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('conversation-deleted', { detail: { conversationId } }));
|
||||
} catch (e) { /* ignore */ }
|
||||
} catch (error) {
|
||||
console.error('删除对话失败:', error);
|
||||
alert('删除对话失败: ' + error.message);
|
||||
@@ -6284,4 +6292,23 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 任意入口删除对话后同步:若删除的是当前对话则清空主区,并刷新侧边栏列表(如从 WebShell AI 助手删除)
|
||||
document.addEventListener('conversation-deleted', (e) => {
|
||||
const id = e.detail && e.detail.conversationId;
|
||||
if (!id) return;
|
||||
if (id === currentConversationId) {
|
||||
currentConversationId = null;
|
||||
const messagesDiv = document.getElementById('chat-messages');
|
||||
if (messagesDiv) messagesDiv.innerHTML = '';
|
||||
const readyMsg = typeof window.t === 'function' ? window.t('chat.systemReadyMessage') : '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
addMessage('assistant', readyMsg, null, null, null, { systemReadyMessage: true });
|
||||
addAttackChainButton(null);
|
||||
}
|
||||
if (typeof loadConversationsWithGroups === 'function') {
|
||||
loadConversationsWithGroups();
|
||||
} else if (typeof loadConversations === 'function') {
|
||||
loadConversations();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,12 +36,16 @@ function translateProgressMessage(message) {
|
||||
'总结生成完成': 'progress.summaryDone',
|
||||
'正在生成最终回复...': 'progress.generatingFinalReply',
|
||||
'达到最大迭代次数,正在生成总结...': 'progress.maxIterSummary',
|
||||
'正在分析您的请求...': 'progress.analyzingRequestShort',
|
||||
'开始分析请求并制定测试策略': 'progress.analyzingRequestPlanning',
|
||||
// 英文(与 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'
|
||||
'Max iterations reached, generating summary...': 'progress.maxIterSummary',
|
||||
'Analyzing your request...': 'progress.analyzingRequestShort',
|
||||
'Analyzing your request and planning test strategy...': 'progress.analyzingRequestPlanning'
|
||||
};
|
||||
if (map[trim]) return window.t(map[trim]);
|
||||
const callingToolPrefixCn = '正在调用工具: ';
|
||||
|
||||
@@ -57,7 +57,11 @@ async function loadRoles() {
|
||||
return roles;
|
||||
} catch (error) {
|
||||
console.error('加载角色失败:', error);
|
||||
showNotification(_t('roles.loadFailed') + ': ' + error.message, 'error');
|
||||
// 提示文案使用 i18n;若此时 i18n 尚未初始化,则回退为可读中文,而不是暴露 key(roles.loadFailed)
|
||||
var loadFailedLabel = (typeof window !== 'undefined' && typeof window.t === 'function')
|
||||
? window.t('roles.loadFailed')
|
||||
: '加载角色失败';
|
||||
showNotification(loadFailedLabel + ': ' + error.message, 'error');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
+143
-7
@@ -287,6 +287,80 @@ function formatWebshellAiConvDate(updatedAt) {
|
||||
return (d.getMonth() + 1) + '/' + d.getDate();
|
||||
}
|
||||
|
||||
// 根据后端保存的 processDetail 构建一条时间线项的 HTML(与 appendTimelineItem 展示一致)
|
||||
function buildWebshellTimelineItemFromDetail(detail) {
|
||||
var eventType = detail.eventType || '';
|
||||
var title = detail.message || '';
|
||||
var data = detail.data || {};
|
||||
if (eventType === 'iteration') {
|
||||
title = (typeof window.t === 'function') ? window.t('chat.iterationRound', { n: data.iteration || 1 }) : ('第 ' + (data.iteration || 1) + ' 轮迭代');
|
||||
} else if (eventType === 'thinking') {
|
||||
title = '🤔 ' + ((typeof window.t === 'function') ? window.t('chat.aiThinking') : 'AI 思考');
|
||||
} else if (eventType === 'tool_calls_detected') {
|
||||
title = '🔧 ' + ((typeof window.t === 'function') ? window.t('chat.toolCallsDetected', { count: data.count || 0 }) : ('检测到 ' + (data.count || 0) + ' 个工具调用'));
|
||||
} else if (eventType === 'tool_call') {
|
||||
var tn = data.toolName || ((typeof window.t === 'function') ? window.t('chat.unknownTool') : '未知工具');
|
||||
var idx = data.index || 0;
|
||||
var total = data.total || 0;
|
||||
title = '🔧 ' + ((typeof window.t === 'function') ? window.t('chat.callTool', { name: tn, index: idx, total: total }) : ('调用: ' + tn + (total ? ' (' + idx + '/' + total + ')' : '')));
|
||||
} else if (eventType === 'tool_result') {
|
||||
var success = data.success !== false;
|
||||
var tname = data.toolName || '工具';
|
||||
title = (success ? '✅ ' : '❌ ') + ((typeof window.t === 'function') ? (success ? window.t('chat.toolExecComplete', { name: tname }) : window.t('chat.toolExecFailed', { name: tname })) : (tname + (success ? ' 执行完成' : ' 执行失败')));
|
||||
} else if (eventType === 'progress') {
|
||||
title = (typeof window.translateProgressMessage === 'function') ? window.translateProgressMessage(detail.message || '') : (detail.message || '');
|
||||
}
|
||||
var html = '<span class="webshell-ai-timeline-title">' + escapeHtml(title || '') + '</span>';
|
||||
if (eventType === 'tool_call' && data && (data.argumentsObj || data.arguments)) {
|
||||
try {
|
||||
var args = data.argumentsObj || (data.arguments ? JSON.parse(data.arguments) : null);
|
||||
if (args && typeof args === 'object') {
|
||||
var paramsLabel = (typeof window.t === 'function') ? window.t('timeline.params') : '参数:';
|
||||
html += '<div class="webshell-ai-timeline-msg"><div class="tool-arg-section"><strong>' + escapeHtml(paramsLabel) + '</strong><pre class="tool-args">' + escapeHtml(JSON.stringify(args, null, 2)) + '</pre></div></div>';
|
||||
}
|
||||
} catch (e) {}
|
||||
} else if (eventType === 'tool_result' && data) {
|
||||
var isError = data.isError || data.success === false;
|
||||
var noResultText = (typeof window.t === 'function') ? window.t('timeline.noResult') : '无结果';
|
||||
var result = data.result != null ? data.result : (data.error != null ? data.error : noResultText);
|
||||
var resultStr = (typeof result === 'string') ? result : JSON.stringify(result);
|
||||
var execResultLabel = (typeof window.t === 'function') ? window.t('timeline.executionResult') : '执行结果:';
|
||||
var execIdLabel = (typeof window.t === 'function') ? window.t('timeline.executionId') : '执行ID:';
|
||||
html += '<div class="webshell-ai-timeline-msg"><div class="tool-result-section ' + (isError ? 'error' : 'success') + '"><strong>' + escapeHtml(execResultLabel) + '</strong><pre class="tool-result">' + escapeHtml(resultStr) + '</pre>' + (data.executionId ? '<div class="tool-execution-id"><span>' + escapeHtml(execIdLabel) + '</span> <code>' + escapeHtml(String(data.executionId)) + '</code></div>' : '') + '</div></div>';
|
||||
} else if (detail.message && detail.message !== title) {
|
||||
html += '<div class="webshell-ai-timeline-msg">' + escapeHtml(detail.message) + '</div>';
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// 渲染「执行过程及调用工具」折叠块(默认折叠,刷新后加载历史时保留并可展开)
|
||||
function renderWebshellProcessDetailsBlock(processDetails, defaultCollapsed) {
|
||||
if (!processDetails || processDetails.length === 0) return null;
|
||||
var expandLabel = (typeof window.t === 'function') ? window.t('chat.expandDetail') : '展开详情';
|
||||
var collapseLabel = (typeof window.t === 'function') ? window.t('tasks.collapseDetail') : '收起详情';
|
||||
var headerLabel = (typeof window.t === 'function') ? (window.t('chat.penetrationTestDetail') || '执行过程及调用工具') : '执行过程及调用工具';
|
||||
var wrapper = document.createElement('div');
|
||||
wrapper.className = 'process-details-container webshell-ai-process-block';
|
||||
var collapsed = defaultCollapsed !== false;
|
||||
wrapper.innerHTML = '<button type="button" class="webshell-ai-process-toggle" aria-expanded="' + (!collapsed) + '">' + escapeHtml(headerLabel) + ' <span class="ws-toggle-icon">' + (collapsed ? '▶' : '▼') + '</span></button><div class="process-details-content"><div class="progress-timeline webshell-ai-timeline has-items' + (collapsed ? '' : ' expanded') + '"></div></div>';
|
||||
var timeline = wrapper.querySelector('.progress-timeline');
|
||||
processDetails.forEach(function (d) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'webshell-ai-timeline-item webshell-ai-timeline-' + (d.eventType || '');
|
||||
item.innerHTML = buildWebshellTimelineItemFromDetail(d);
|
||||
timeline.appendChild(item);
|
||||
});
|
||||
var toggleBtn = wrapper.querySelector('.webshell-ai-process-toggle');
|
||||
var toggleIcon = wrapper.querySelector('.ws-toggle-icon');
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
var isExpanded = timeline.classList.contains('expanded');
|
||||
timeline.classList.toggle('expanded');
|
||||
toggleBtn.setAttribute('aria-expanded', !isExpanded);
|
||||
if (toggleIcon) toggleIcon.textContent = isExpanded ? '▶' : '▼';
|
||||
});
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
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' })
|
||||
@@ -313,15 +387,19 @@ function fetchAndRenderWebshellAiConvList(conn, listEl) {
|
||||
delBtn.addEventListener('click', function (e) {
|
||||
e.stopPropagation();
|
||||
if (!confirm(wsT('webshell.aiDeleteConversationConfirm') || '确定删除该对话?')) return;
|
||||
apiFetch('/api/conversations/' + encodeURIComponent(item.id), { method: 'DELETE' })
|
||||
var deletedId = item.id;
|
||||
apiFetch('/api/conversations/' + encodeURIComponent(deletedId), { method: 'DELETE' })
|
||||
.then(function (r) {
|
||||
if (r.ok) {
|
||||
if (webshellAiConvMap[conn.id] === item.id) {
|
||||
if (webshellAiConvMap[conn.id] === deletedId) {
|
||||
delete webshellAiConvMap[conn.id];
|
||||
var msgs = document.getElementById('webshell-ai-messages');
|
||||
if (msgs) msgs.innerHTML = '';
|
||||
}
|
||||
fetchAndRenderWebshellAiConvList(conn, listEl);
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('conversation-deleted', { detail: { conversationId: deletedId } }));
|
||||
} catch (err) { /* ignore */ }
|
||||
}
|
||||
})
|
||||
.catch(function (e) { console.warn('删除对话失败', e); });
|
||||
@@ -361,6 +439,10 @@ function webshellAiConvListSelect(conn, convId, messagesContainer, listEl) {
|
||||
}
|
||||
}
|
||||
messagesContainer.appendChild(div);
|
||||
if (role === 'assistant' && msg.processDetails && msg.processDetails.length > 0) {
|
||||
var block = renderWebshellProcessDetailsBlock(msg.processDetails, true);
|
||||
if (block) messagesContainer.appendChild(block);
|
||||
}
|
||||
});
|
||||
if (list.length === 0) {
|
||||
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
@@ -539,7 +621,7 @@ function selectWebshell(id) {
|
||||
initWebshellTerminal(conn);
|
||||
}
|
||||
|
||||
// 加载 WebShell 连接的 AI 助手对话历史(持久化展示),返回 Promise 供 .then 更新工具栏等
|
||||
// 加载 WebShell 连接的 AI 助手对话历史(持久化展示),返回 Promise 供 .then 更新工具栏等;含 processDetails 时渲染折叠的「执行过程及调用工具」
|
||||
function loadWebshellAiHistory(conn, messagesContainer) {
|
||||
if (!conn || !conn.id || !messagesContainer) return Promise.resolve();
|
||||
if (typeof apiFetch !== 'function') return Promise.resolve();
|
||||
@@ -564,6 +646,10 @@ function loadWebshellAiHistory(conn, messagesContainer) {
|
||||
}
|
||||
}
|
||||
messagesContainer.appendChild(div);
|
||||
if (role === 'assistant' && msg.processDetails && msg.processDetails.length > 0) {
|
||||
var block = renderWebshellProcessDetailsBlock(msg.processDetails, true);
|
||||
if (block) messagesContainer.appendChild(block);
|
||||
}
|
||||
});
|
||||
if (list.length === 0) {
|
||||
var readyMsg = wsT('webshell.aiSystemReadyMessage') || '系统已就绪。请输入您的测试需求,系统将自动执行相应的安全测试。';
|
||||
@@ -702,11 +788,13 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
try {
|
||||
var eventData = JSON.parse(line.slice(6));
|
||||
if (eventData.type === 'conversation' && eventData.data && eventData.data.conversationId) {
|
||||
webshellAiConvMap[conn.id] = eventData.data.conversationId;
|
||||
// 先把 conversationId 拿出来,避免后续异步回调里 eventData 被后续事件覆盖导致 undefined 报错
|
||||
var convId = eventData.data.conversationId;
|
||||
webshellAiConvMap[conn.id] = convId;
|
||||
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);
|
||||
el.classList.toggle('active', el.dataset.convId === convId);
|
||||
});
|
||||
});
|
||||
} else if (eventData.type === 'response') {
|
||||
@@ -733,7 +821,11 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
var iterTitle = (typeof window.t === 'function')
|
||||
? window.t('chat.iterationRound', { n: iterN || 1 })
|
||||
: (iterN ? ('第 ' + iterN + ' 轮迭代') : (eventData.message || '迭代'));
|
||||
appendTimelineItem('iteration', '🔍 ' + iterTitle, eventData.message || '', eventData.data);
|
||||
var iterMessage = eventData.message || '';
|
||||
if (iterMessage && typeof window.translateProgressMessage === 'function') {
|
||||
iterMessage = window.translateProgressMessage(iterMessage);
|
||||
}
|
||||
appendTimelineItem('iteration', '🔍 ' + iterTitle, iterMessage, eventData.data);
|
||||
if (!streamingTarget) assistantDiv.textContent = '…';
|
||||
} else if (eventData.type === 'thinking' && eventData.message) {
|
||||
var thinkLabel = (typeof window.t === 'function') ? window.t('chat.aiThinking') : 'AI 思考';
|
||||
@@ -779,7 +871,38 @@ function runWebshellAiSend(conn, inputEl, sendBtn, messagesContainer) {
|
||||
}).then(function () {
|
||||
webshellAiSending = false;
|
||||
if (sendBtn) sendBtn.disabled = false;
|
||||
if (assistantDiv.textContent === '…' && !streamingTarget) assistantDiv.textContent = '无回复内容';
|
||||
if (assistantDiv.textContent === '…' && !streamingTarget) {
|
||||
// 没有任何 response 内容,保持纯文本提示
|
||||
assistantDiv.textContent = '无回复内容';
|
||||
} else if (streamingTarget) {
|
||||
// 流式结束:先终止当前打字机循环,避免后续 tick 把 HTML 覆盖回纯文本
|
||||
webshellStreamingTypingId += 1;
|
||||
// 再使用 Markdown 渲染完整内容
|
||||
if (typeof formatMarkdown === 'function') {
|
||||
assistantDiv.innerHTML = formatMarkdown(streamingTarget);
|
||||
} else {
|
||||
assistantDiv.textContent = streamingTarget;
|
||||
}
|
||||
}
|
||||
// 生成结果后:将执行过程折叠并保留,供后续查看;统一放在「助手回复下方」(与刷新后加载历史一致,最佳实践)
|
||||
if (timelineContainer && timelineContainer.classList.contains('has-items') && !timelineContainer.closest('.webshell-ai-process-block')) {
|
||||
var headerLabel = (typeof window.t === 'function') ? (window.t('chat.penetrationTestDetail') || '执行过程及调用工具') : '执行过程及调用工具';
|
||||
var wrap = document.createElement('div');
|
||||
wrap.className = 'process-details-container webshell-ai-process-block';
|
||||
wrap.innerHTML = '<button type="button" class="webshell-ai-process-toggle" aria-expanded="false">' + escapeHtml(headerLabel) + ' <span class="ws-toggle-icon">▶</span></button><div class="process-details-content"></div>';
|
||||
var contentDiv = wrap.querySelector('.process-details-content');
|
||||
contentDiv.appendChild(timelineContainer);
|
||||
timelineContainer.classList.add('progress-timeline');
|
||||
messagesContainer.insertBefore(wrap, assistantDiv.nextSibling);
|
||||
var toggleBtn = wrap.querySelector('.webshell-ai-process-toggle');
|
||||
var toggleIcon = wrap.querySelector('.ws-toggle-icon');
|
||||
toggleBtn.addEventListener('click', function () {
|
||||
var isExpanded = timelineContainer.classList.contains('expanded');
|
||||
timelineContainer.classList.toggle('expanded');
|
||||
toggleBtn.setAttribute('aria-expanded', !isExpanded);
|
||||
if (toggleIcon) toggleIcon.textContent = isExpanded ? '▶' : '▼';
|
||||
});
|
||||
}
|
||||
messagesContainer.scrollTop = messagesContainer.scrollHeight;
|
||||
});
|
||||
}
|
||||
@@ -1569,6 +1692,19 @@ document.addEventListener('languagechange', function () {
|
||||
refreshWebshellUIOnLanguageChange();
|
||||
});
|
||||
|
||||
// 任意入口删除对话后同步:若当前在 WebShell AI 助手且已选连接,则刷新对话列表(与 Chat 侧边栏删除保持一致)
|
||||
document.addEventListener('conversation-deleted', function (e) {
|
||||
var id = e.detail && e.detail.conversationId;
|
||||
if (!id || !currentWebshellId || !webshellCurrentConn) return;
|
||||
var listEl = document.getElementById('webshell-ai-conv-list');
|
||||
if (listEl) fetchAndRenderWebshellAiConvList(webshellCurrentConn, listEl);
|
||||
if (webshellAiConvMap[webshellCurrentConn.id] === id) {
|
||||
delete webshellAiConvMap[webshellCurrentConn.id];
|
||||
var msgs = document.getElementById('webshell-ai-messages');
|
||||
if (msgs) msgs.innerHTML = '';
|
||||
}
|
||||
});
|
||||
|
||||
// 测试连通性(不保存,仅用当前表单参数请求 Shell 执行 echo 1)
|
||||
function testWebshellConnection() {
|
||||
var url = (document.getElementById('webshell-url') || {}).value;
|
||||
|
||||
Reference in New Issue
Block a user