diff --git a/internal/database/conversation.go b/internal/database/conversation.go index 432f870d..61c9fc79 100644 --- a/internal/database/conversation.go +++ b/internal/database/conversation.go @@ -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 } diff --git a/web/static/css/style.css b/web/static/css/style.css index a92a1acf..b4862a92 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -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; diff --git a/web/static/i18n/en-US.json b/web/static/i18n/en-US.json index 68dda4ac..401a3a78 100644 --- a/web/static/i18n/en-US.json +++ b/web/static/i18n/en-US.json @@ -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:", diff --git a/web/static/i18n/zh-CN.json b/web/static/i18n/zh-CN.json index 33af68e9..f20a110d 100644 --- a/web/static/i18n/zh-CN.json +++ b/web/static/i18n/zh-CN.json @@ -171,7 +171,9 @@ "lastIterSummary": "最后一次迭代:正在生成总结和下一步计划...", "summaryDone": "总结生成完成", "generatingFinalReply": "正在生成最终回复...", - "maxIterSummary": "达到最大迭代次数,正在生成总结..." + "maxIterSummary": "达到最大迭代次数,正在生成总结...", + "analyzingRequestShort": "正在分析您的请求...", + "analyzingRequestPlanning": "开始分析请求并制定测试策略" }, "timeline": { "params": "参数:", diff --git a/web/static/js/chat.js b/web/static/js/chat.js index 99caad01..f50870b6 100644 --- a/web/static/js/chat.js +++ b/web/static/js/chat.js @@ -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(); + } + }); }); diff --git a/web/static/js/monitor.js b/web/static/js/monitor.js index 7843ec0d..9a04cb88 100644 --- a/web/static/js/monitor.js +++ b/web/static/js/monitor.js @@ -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 = '正在调用工具: '; diff --git a/web/static/js/roles.js b/web/static/js/roles.js index 129343c8..65d0e65b 100644 --- a/web/static/js/roles.js +++ b/web/static/js/roles.js @@ -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 []; } } diff --git a/web/static/js/webshell.js b/web/static/js/webshell.js index 7642922b..fe615c1c 100644 --- a/web/static/js/webshell.js +++ b/web/static/js/webshell.js @@ -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 = '' + escapeHtml(title || '') + ''; + 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 += '
' + escapeHtml(JSON.stringify(args, null, 2)) + '
' + escapeHtml(resultStr) + '' + (data.executionId ? '
' + escapeHtml(String(data.executionId)) + '