Add files via upload

This commit is contained in:
公明
2026-03-14 02:32:23 +08:00
committed by GitHub
parent 0809be60fa
commit 797b10b176
8 changed files with 257 additions and 15 deletions
+31
View File
@@ -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
}
+38 -2
View File
@@ -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;
+3 -1
View File
@@ -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:",
+3 -1
View File
@@ -171,7 +171,9 @@
"lastIterSummary": "最后一次迭代:正在生成总结和下一步计划...",
"summaryDone": "总结生成完成",
"generatingFinalReply": "正在生成最终回复...",
"maxIterSummary": "达到最大迭代次数,正在生成总结..."
"maxIterSummary": "达到最大迭代次数,正在生成总结...",
"analyzingRequestShort": "正在分析您的请求...",
"analyzingRequestPlanning": "开始分析请求并制定测试策略"
},
"timeline": {
"params": "参数:",
+29 -2
View File
@@ -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();
}
});
});
+5 -1
View File
@@ -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 = '正在调用工具: ';
+5 -1
View File
@@ -57,7 +57,11 @@ async function loadRoles() {
return roles;
} catch (error) {
console.error('加载角色失败:', error);
showNotification(_t('roles.loadFailed') + ': ' + error.message, 'error');
// 提示文案使用 i18n;若此时 i18n 尚未初始化,则回退为可读中文,而不是暴露 keyroles.loadFailed
var loadFailedLabel = (typeof window !== 'undefined' && typeof window.t === 'function')
? window.t('roles.loadFailed')
: '加载角色失败';
showNotification(loadFailedLabel + ': ' + error.message, 'error');
return [];
}
}
+143 -7
View File
@@ -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;