From 2f620fa4d030d8185973c1460b0397fbb915f3d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E7=99=BD?= Date: Thu, 12 Mar 2026 20:56:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E4=BC=9A=E8=AF=9D?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现会话列表 API - 实现会话详情 API - 实现终止会话 API - 添加会话管理页面 UI - 添加会话管理样式 - 添加会话管理交互脚本 --- api/sessions.py | 87 +++++++++++-- static/css/dashboard.css | 223 +++++++++++++++++++++++++++++++++ static/js/dashboard.js | 182 +++++++++++++++++++++++++++ templates/dashboard/index.html | 30 ++++- 4 files changed, 511 insertions(+), 11 deletions(-) diff --git a/api/sessions.py b/api/sessions.py index 0c48a3e..cd7fc16 100644 --- a/api/sessions.py +++ b/api/sessions.py @@ -5,15 +5,88 @@ 作者:小白 🐶 """ -from flask import jsonify +import json +import subprocess +from flask import jsonify, request from . import api @api.route('/sessions') def get_sessions(): """获取会话列表""" - # TODO: 实现 OpenClaw 会话查询 - return jsonify({ - 'success': True, - 'data': [], - 'message': '会话管理功能开发中' - }) + try: + # 调用 sessions_list 命令 + result = subprocess.run( + ['sessions_list'], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + data = json.loads(result.stdout) + return jsonify({ + 'success': True, + 'data': data + }) + else: + return jsonify({ + 'success': False, + 'error': result.stderr + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }) + +@api.route('/sessions/') +def get_session_detail(session_key): + """获取会话详情""" + try: + # 获取会话历史 + result = subprocess.run( + ['sessions_history', '--session-key', session_key, '--limit', '20'], + capture_output=True, + text=True, + timeout=10 + ) + + if result.returncode == 0: + data = json.loads(result.stdout) + return jsonify({ + 'success': True, + 'data': data + }) + else: + return jsonify({ + 'success': False, + 'error': result.stderr + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }) + +@api.route('/sessions//kill', methods=['POST']) +def kill_session(session_key): + """终止会话""" + try: + # 调用 subagents kill 命令 + result = subprocess.run( + ['subagents', 'kill', '--target', session_key], + capture_output=True, + text=True, + timeout=10 + ) + + return jsonify({ + 'success': result.returncode == 0, + 'message': '会话已终止' if result.returncode == 0 else '终止失败', + 'output': result.stdout or result.stderr + }) + except Exception as e: + return jsonify({ + 'success': False, + 'error': str(e) + }) diff --git a/static/css/dashboard.css b/static/css/dashboard.css index f12a18f..1efdab1 100644 --- a/static/css/dashboard.css +++ b/static/css/dashboard.css @@ -436,6 +436,229 @@ body { color: var(--text-secondary); } +/* 会话管理 */ +.section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20px; +} + +.btn-refresh { + padding: 8px 16px; + background: var(--primary-color); + color: white; + border: none; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: opacity 0.3s; +} + +.btn-refresh:hover { + opacity: 0.8; +} + +.sessions-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.session-card { + background: var(--card-bg); + border-radius: 10px; + padding: 16px 20px; + border: 1px solid var(--border-color); + display: flex; + align-items: center; + gap: 15px; + transition: all 0.3s ease; +} + +.session-card:hover { + transform: translateX(4px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.session-icon { + font-size: 28px; +} + +.session-info { + flex: 1; +} + +.session-name { + font-size: 15px; + font-weight: 500; + color: var(--text-primary); + margin-bottom: 4px; +} + +.session-meta { + font-size: 13px; + color: var(--text-secondary); +} + +.session-status { + padding: 4px 10px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; +} + +.session-status.active { + background: rgba(72, 187, 120, 0.2); + color: var(--success-color); +} + +.session-actions { + display: flex; + gap: 8px; +} + +.btn-view, .btn-kill { + padding: 6px 12px; + border: none; + border-radius: 6px; + font-size: 13px; + cursor: pointer; + transition: opacity 0.3s; +} + +.btn-view { + background: var(--primary-color); + color: white; +} + +.btn-kill { + background: var(--danger-color); + color: white; +} + +.btn-view:hover, .btn-kill:hover { + opacity: 0.8; +} + +/* 模态框 */ +.modal { + display: none; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + align-items: center; + justify-content: center; +} + +.modal.show { + display: flex; +} + +.modal-content { + background: var(--card-bg); + border-radius: 12px; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow: hidden; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + border-bottom: 1px solid var(--border-color); +} + +.modal-header h3 { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); +} + +.modal-close { + width: 32px; + height: 32px; + border: none; + background: none; + font-size: 24px; + cursor: pointer; + color: var(--text-secondary); +} + +.modal-body { + padding: 20px; + max-height: 50vh; + overflow-y: auto; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 16px 20px; + border-top: 1px solid var(--border-color); +} + +.btn-danger, .btn-secondary { + padding: 8px 16px; + border: none; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + transition: opacity 0.3s; +} + +.btn-danger { + background: var(--danger-color); + color: white; +} + +.btn-secondary { + background: var(--border-color); + color: var(--text-primary); +} + +.btn-danger:hover, .btn-secondary:hover { + opacity: 0.8; +} + +/* 详情列表 */ +.detail-list { + display: flex; + flex-direction: column; + gap: 12px; +} + +.detail-item { + display: flex; + padding: 10px 0; + border-bottom: 1px solid var(--border-color); +} + +.detail-item:last-child { + border-bottom: none; +} + +.detail-label { + width: 120px; + color: var(--text-secondary); + font-size: 14px; +} + +.detail-value { + flex: 1; + color: var(--text-primary); + font-size: 14px; + word-break: break-all; +} + /* 响应式 */ @media (max-width: 768px) { .sidebar { diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 5e48125..faf0945 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -3,6 +3,8 @@ * 作者:小白 🐶 */ +let currentSessionKey = null; + // 页面切换 document.querySelectorAll('.nav-item').forEach(item => { item.addEventListener('click', (e) => { @@ -16,6 +18,11 @@ document.querySelectorAll('.nav-item').forEach(item => { item.classList.add('active'); const pageId = 'page-' + item.dataset.page; document.getElementById(pageId).classList.add('active'); + + // 如果是会话管理页面,加载数据 + if (item.dataset.page === 'sessions') { + fetchSessions(); + } }); }); @@ -50,6 +57,174 @@ async function fetchStatus() { } } +// 获取会话列表 +async function fetchSessions() { + const container = document.getElementById('sessions-list'); + container.innerHTML = '
加载中...
'; + + try { + const response = await fetch('/api/sessions'); + const result = await response.json(); + + if (result.success && result.data.sessions) { + if (result.data.sessions.length === 0) { + container.innerHTML = '
暂无活跃会话
'; + return; + } + + container.innerHTML = ''; + result.data.sessions.forEach(session => { + const card = createSessionCard(session); + container.appendChild(card); + }); + } else { + container.innerHTML = '
加载失败: ' + (result.error || '未知错误') + '
'; + } + } catch (error) { + container.innerHTML = '
加载失败: ' + error.message + '
'; + } +} + +// 创建会话卡片 +function createSessionCard(session) { + const card = document.createElement('div'); + card.className = 'session-card'; + + const updateTime = new Date(session.updatedAt).toLocaleString('zh-CN'); + const isActive = Date.now() - session.updatedAt < 300000; // 5分钟内活跃 + + card.innerHTML = ` +
+ ${session.kind === 'qqbot' ? '💬' : '🌐'} +
+
${session.displayName || session.key.split(':').pop()}
+
+ ${session.channel || 'unknown'} + ${session.model || 'unknown'} +
+
+ + ${isActive ? '活跃' : '离线'} + +
+
+
+ Token 使用: + ${session.totalTokens || 0} / ${session.contextTokens || 0} +
+
+ 最后活动: + ${updateTime} +
+
+
+ + +
+ `; + + return card; +} + +// 查看会话详情 +async function viewSession(sessionKey) { + currentSessionKey = sessionKey; + const modal = document.getElementById('session-modal'); + const detail = document.getElementById('session-detail'); + + modal.classList.add('show'); + detail.innerHTML = '
加载中...
'; + + try { + const response = await fetch(`/api/sessions/${encodeURIComponent(sessionKey)}`); + const result = await response.json(); + + if (result.success && result.data) { + detail.innerHTML = ` +
+

基本信息

+
+
+ 会话 Key: + ${sessionKey} +
+
+ 渠道: + ${result.data.channel || '-'} +
+
+ 模型: + ${result.data.model || '-'} +
+
+
+
+

最近消息 (最近 ${result.data.messages?.length || 0} 条)

+
+ ${(result.data.messages || []).map(msg => ` +
+ ${msg.role === 'user' ? '👤 用户' : '🤖 助手'} +
${escapeHtml(msg.content?.substring(0, 200) || '')}${msg.content?.length > 200 ? '...' : ''}
+
+ `).join('')} +
+
+ `; + } else { + detail.innerHTML = '
加载失败
'; + } + } catch (error) { + detail.innerHTML = '
加载失败: ' + error.message + '
'; + } +} + +// 确认终止会话 +function confirmKillSession(sessionKey) { + if (confirm('确定要终止这个会话吗?此操作不可撤销。')) { + killSession(sessionKey); + } +} + +// 终止当前会话 +async function killCurrentSession() { + if (currentSessionKey) { + await killSession(currentSessionKey); + closeSessionModal(); + } +} + +// 终止会话 +async function killSession(sessionKey) { + try { + const response = await fetch(`/api/sessions/${encodeURIComponent(sessionKey)}/kill`, { + method: 'POST' + }); + const result = await response.json(); + + if (result.success) { + alert('会话已终止'); + fetchSessions(); + } else { + alert('终止失败: ' + (result.error || result.message)); + } + } catch (error) { + alert('终止失败: ' + error.message); + } +} + +// 关闭模态框 +function closeSessionModal() { + document.getElementById('session-modal').classList.remove('show'); + currentSessionKey = null; +} + +// HTML 转义 +function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} + // 刷新状态 function refreshStatus() { fetchStatus(); @@ -61,4 +236,11 @@ document.addEventListener('DOMContentLoaded', () => { // 每 30 秒自动刷新 setInterval(fetchStatus, 30000); + + // 点击模态框背景关闭 + document.getElementById('session-modal').addEventListener('click', (e) => { + if (e.target.id === 'session-modal') { + closeSessionModal(); + } + }); }); diff --git a/templates/dashboard/index.html b/templates/dashboard/index.html index c170de8..a7157eb 100644 --- a/templates/dashboard/index.html +++ b/templates/dashboard/index.html @@ -107,12 +107,34 @@ - +

💬 会话管理

-
- 🚧 -

功能开发中...

+ +
+

活跃会话列表

+ +
+ +
+
加载中...
+
+ + +