init: DeepSeek Agent Web 控制台

This commit is contained in:
admin
2026-04-17 09:12:38 +08:00
commit 898f180361
5 changed files with 2121 additions and 0 deletions
+869
View File
@@ -0,0 +1,869 @@
"""
Web 版 Agent 界面 - 完整显示所有信息
Flask + SocketIO 实现实时双向通信
"""
import asyncio
import json
import sys
import threading
import queue
from pathlib import Path
from datetime import datetime
from flask import Flask, render_template_string
from flask_socketio import SocketIO, emit
# 导入 yuanbao_agent_chat 的核心类
from yuanbao_agent_chat import SoloAgent, ExecutionEngine
app = Flask(__name__)
app.config['SECRET_KEY'] = 'yuanbao-agent-secret'
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
# 全局状态
agent_instance = None
browser_running = False
message_queue = queue.Queue()
# HTML 模板
HTML_TEMPLATE = '''
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Yuanbao Agent - Web 控制台</title>
<script src="https://cdn.socket.io/4.5.4/socket.io.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0d1117;
color: #c9d1d9;
height: 100vh;
display: flex;
flex-direction: column;
}
.header {
background: #161b22;
border-bottom: 1px solid #30363d;
padding: 12px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.header h1 {
font-size: 18px;
color: #58a6ff;
}
.status-bar {
display: flex;
align-items: center;
gap: 16px;
}
.status {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #da3633;
}
.status-dot.connected {
background: #238636;
}
.status-dot.connecting {
background: #d29922;
animation: pulse 1s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
.browser-controls {
display: flex;
gap: 8px;
}
.browser-controls button {
padding: 6px 16px;
font-size: 13px;
}
.btn-start {
background: #238636;
}
.btn-start:hover {
background: #2ea043;
}
.btn-stop {
background: #da3633;
}
.btn-stop:hover {
background: #f85149;
}
.main-container {
flex: 1;
display: flex;
overflow: hidden;
}
.chat-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #30363d;
}
.messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
margin-bottom: 20px;
padding: 16px;
border-radius: 8px;
border: 1px solid #30363d;
}
.message.user {
background: #1c2128;
border-left: 3px solid #58a6ff;
}
.message.dp {
background: #161b22;
border-left: 3px solid #a371f7;
}
.message.system {
background: #1c2128;
border-left: 3px solid #238636;
}
.message.error {
background: #3d0c0c;
border-left: 3px solid #da3633;
}
.message-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 12px;
color: #8b949e;
}
.message-content {
white-space: pre-wrap;
word-break: break-word;
line-height: 1.6;
}
.message-content .action-json {
background: #0d1117;
padding: 12px;
border-radius: 6px;
margin: 8px 0;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
color: #7ee787;
overflow-x: auto;
}
.message-content .terminal-output {
background: #0d1117;
padding: 12px;
border-radius: 6px;
margin: 8px 0;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
color: #e6edf3;
white-space: pre-wrap;
max-height: 400px;
overflow-y: auto;
}
.input-area {
padding: 16px 20px;
background: #161b22;
border-top: 1px solid #30363d;
}
.input-wrapper {
display: flex;
gap: 12px;
}
textarea {
flex: 1;
background: #0d1117;
border: 1px solid #30363d;
border-radius: 8px;
padding: 12px 16px;
color: #c9d1d9;
font-size: 14px;
resize: none;
min-height: 60px;
max-height: 200px;
}
textarea:focus {
outline: none;
border-color: #58a6ff;
}
textarea:disabled {
background: #21262d;
cursor: not-allowed;
}
button {
background: #238636;
color: white;
border: none;
border-radius: 8px;
padding: 12px 24px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: #2ea043;
}
button:disabled {
background: #30363d;
cursor: not-allowed;
}
.side-panel {
width: 450px;
display: flex;
flex-direction: column;
background: #161b22;
}
.panel-tabs {
display: flex;
border-bottom: 1px solid #30363d;
}
.panel-tab {
flex: 1;
padding: 12px;
text-align: center;
cursor: pointer;
border-bottom: 2px solid transparent;
font-size: 13px;
}
.panel-tab.active {
border-bottom-color: #58a6ff;
color: #58a6ff;
}
.panel-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.log-entry {
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
padding: 8px;
margin-bottom: 4px;
border-radius: 4px;
background: #0d1117;
border-left: 2px solid #30363d;
}
.log-entry.debug {
border-left-color: #8b949e;
}
.log-entry.info {
border-left-color: #58a6ff;
}
.log-entry.success {
border-left-color: #238636;
}
.log-entry.error {
border-left-color: #da3633;
}
.log-time {
color: #8b949e;
margin-right: 8px;
}
.log-category {
color: #a371f7;
margin-right: 8px;
}
.typing-indicator {
display: none;
padding: 12px 20px;
color: #8b949e;
font-size: 14px;
}
.typing-indicator.active {
display: block;
}
.toolbar {
display: flex;
gap: 8px;
padding: 8px 20px;
background: #0d1117;
border-bottom: 1px solid #30363d;
}
.toolbar button {
padding: 6px 12px;
font-size: 12px;
}
.btn-secondary {
background: #21262d;
}
.btn-secondary:hover {
background: #30363d;
}
</style>
</head>
<body>
<div class="header">
<h1>🤖 Yuanbao Agent Web 控制台</h1>
<div class="status-bar">
<div class="status">
<span class="status-dot" id="browserStatusDot"></span>
<span id="browserStatusText">浏览器未启动</span>
</div>
<div class="browser-controls">
<button class="btn-start" id="startBtn" onclick="startBrowser()">🚀 启动浏览器</button>
<button class="btn-stop" id="stopBtn" onclick="stopBrowser()" disabled>⏹ 停止浏览器</button>
</div>
</div>
</div>
<div class="toolbar">
<button class="btn-secondary" onclick="clearMessages()">清空消息</button>
<button class="btn-secondary" onclick="requestTerminalOutput()">获取终端输出</button>
<button class="btn-secondary" onclick="clearTerminal()">清空终端</button>
</div>
<div class="main-container">
<div class="chat-panel">
<div class="messages" id="messages"></div>
<div class="typing-indicator" id="typingIndicator">
DeepSeek 正在思考...
</div>
<div class="input-area">
<div class="input-wrapper">
<textarea id="userInput" placeholder="请先启动浏览器,然后输入任务指令..."
onkeydown="handleKeyDown(event)" disabled></textarea>
<button onclick="sendMessage()" id="sendBtn" disabled>发送</button>
</div>
</div>
</div>
<div class="side-panel">
<div class="panel-tabs">
<div class="panel-tab active" onclick="switchTab('logs')">运行日志</div>
<div class="panel-tab" onclick="switchTab('terminal')">命令终端</div>
</div>
<div class="panel-content" id="panelContent">
<div id="logsPanel"></div>
<div id="terminalPanel" style="display:none;"></div>
</div>
</div>
</div>
<script>
const socket = io();
let currentTab = 'logs';
let browserReady = false;
socket.on('connect', () => {
addLogEntry({
time: new Date().toLocaleTimeString(),
category: 'SYSTEM',
message: 'Web 控制台已连接',
level: 'success'
});
});
socket.on('browser_status', (data) => {
updateBrowserStatus(data.running, data.message);
});
socket.on('user_message', (data) => {
addMessage('user', data.content, data.timestamp);
});
socket.on('dp_message', (data) => {
addMessage('dp', data.content, data.timestamp, data.raw);
hideTyping();
});
socket.on('system_message', (data) => {
addMessage('system', data.content, data.timestamp);
});
socket.on('error_message', (data) => {
addMessage('error', data.content, data.timestamp);
hideTyping();
});
socket.on('log_entry', (data) => {
addLogEntry(data);
});
socket.on('terminal_output', (data) => {
updateTerminalPanel(data.content);
});
socket.on('typing', () => {
showTyping();
});
function updateBrowserStatus(running, message) {
browserReady = running;
const dot = document.getElementById('browserStatusDot');
const text = document.getElementById('browserStatusText');
const startBtn = document.getElementById('startBtn');
const stopBtn = document.getElementById('stopBtn');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
if (running) {
dot.classList.add('connected');
dot.classList.remove('connecting');
startBtn.disabled = true;
stopBtn.disabled = false;
userInput.disabled = false;
sendBtn.disabled = false;
userInput.placeholder = '输入任务指令...';
} else {
dot.classList.remove('connected', 'connecting');
startBtn.disabled = false;
stopBtn.disabled = true;
userInput.disabled = true;
sendBtn.disabled = true;
userInput.placeholder = '请先启动浏览器,然后输入任务指令...';
}
text.textContent = message;
}
function startBrowser() {
document.getElementById('browserStatusDot').classList.add('connecting');
document.getElementById('browserStatusText').textContent = '正在启动...';
document.getElementById('startBtn').disabled = true;
socket.emit('start_browser');
}
function stopBrowser() {
socket.emit('stop_browser');
}
function addMessage(type, content, timestamp, raw) {
const messages = document.getElementById('messages');
const div = document.createElement('div');
div.className = `message ${type}`;
let displayContent = escapeHtml(content);
// 高亮 ACTION JSON
displayContent = displayContent.replace(
/(&lt;&lt;&lt;ACTION\s*)(\{[\s\S]*?\})(\s*ACTION&gt;&gt;&gt;)/g,
'$1<div class="action-json">$2</div>$3'
);
// 高亮终端输出
displayContent = displayContent.replace(
/```\n([\s\S]*?)```/g,
'<div class="terminal-output">$1</div>'
);
div.innerHTML = `
<div class="message-header">
<span>${type === 'user' ? '👤 用户' : type === 'dp' ? '🤖 DeepSeek' : type === 'error' ? '❌ 错误' : '⚙️ 系统'}</span>
<span>${timestamp}</span>
</div>
<div class="message-content">${displayContent}</div>
`;
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function addLogEntry(data) {
const panel = document.getElementById('logsPanel');
const div = document.createElement('div');
div.className = `log-entry ${data.level || 'info'}`;
div.innerHTML = `
<span class="log-time">${data.time}</span>
<span class="log-category">[${data.category}]</span>
<span>${escapeHtml(data.message)}</span>
`;
panel.appendChild(div);
panel.scrollTop = panel.scrollHeight;
}
function updateTerminalPanel(content) {
const panel = document.getElementById('terminalPanel');
panel.innerHTML = `<div class="terminal-output">${escapeHtml(content)}</div>`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
function sendMessage() {
if (!browserReady) {
alert('请先启动浏览器');
return;
}
const input = document.getElementById('userInput');
const content = input.value.trim();
if (!content) return;
socket.emit('user_input', { content });
input.value = '';
showTyping();
}
function handleKeyDown(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
}
function showTyping() {
document.getElementById('typingIndicator').classList.add('active');
document.getElementById('sendBtn').disabled = true;
}
function hideTyping() {
document.getElementById('typingIndicator').classList.remove('active');
if (browserReady) {
document.getElementById('sendBtn').disabled = false;
}
}
function clearMessages() {
document.getElementById('messages').innerHTML = '';
}
function requestTerminalOutput() {
socket.emit('get_terminal_output');
}
function clearTerminal() {
socket.emit('clear_terminal');
}
function switchTab(tab) {
currentTab = tab;
document.querySelectorAll('.panel-tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('logsPanel').style.display = tab === 'logs' ? 'block' : 'none';
document.getElementById('terminalPanel').style.display = tab === 'terminal' ? 'block' : 'none';
}
</script>
</body>
</html>
'''
class WebInterface:
"""Web 界面适配器"""
def __init__(self):
self.socket = None
def set_socket(self, socket):
self.socket = socket
def emit(self, event, data):
socketio.emit(event, data)
def print_user(self, message: str):
"""显示用户消息"""
self.emit('user_message', {
'content': message,
'timestamp': datetime.now().strftime('%H:%M:%S')
})
def print_ai(self, message: str, raw: str = ""):
"""显示 AI 消息"""
self.emit('dp_message', {
'content': message,
'raw': raw,
'timestamp': datetime.now().strftime('%H:%M:%S')
})
def print_system(self, message: str):
"""显示系统消息"""
self.emit('system_message', {
'content': message,
'timestamp': datetime.now().strftime('%H:%M:%S')
})
def print_error(self, message: str):
"""显示错误消息"""
self.emit('error_message', {
'content': message,
'timestamp': datetime.now().strftime('%H:%M:%S')
})
def log(self, category: str, message: str, level: str = "info"):
"""添加日志条目"""
self.emit('log_entry', {
'time': datetime.now().strftime('%H:%M:%S'),
'category': category,
'message': message,
'level': level
})
def update_browser_status(self, running: bool, message: str):
"""更新浏览器状态"""
self.emit('browser_status', {
'running': running,
'message': message
})
# 创建 Web 界面实例
web_interface = WebInterface()
@app.route('/')
def index():
return render_template_string(HTML_TEMPLATE)
@socketio.on('connect')
def handle_connect():
web_interface.set_socket(request.sid if hasattr(request, 'sid') else None)
web_interface.update_browser_status(browser_running, '浏览器未启动')
@socketio.on('disconnect')
def handle_disconnect():
pass
@socketio.on('start_browser')
def handle_start_browser():
"""启动浏览器"""
global agent_instance, browser_running
def start():
global agent_instance, browser_running
try:
web_interface.update_browser_status(False, '正在启动浏览器...')
web_interface.log('BROWSER', '正在初始化 Playwright...')
if agent_instance is None:
agent_instance = SoloAgent()
# 创建新的事件循环
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# 启动浏览器
loop.run_until_complete(agent_instance.start())
browser_running = True
web_interface.update_browser_status(True, '浏览器已连接')
web_interface.log('BROWSER', '浏览器启动成功', level='success')
web_interface.print_system('🚀 浏览器已启动,可以开始对话了')
loop.close()
except Exception as e:
browser_running = False
web_interface.update_browser_status(False, '启动失败')
web_interface.print_error(f'浏览器启动失败: {str(e)}')
web_interface.log('ERROR', str(e), level='error')
import traceback
web_interface.log('ERROR', traceback.format_exc(), level='error')
threading.Thread(target=start, daemon=True).start()
@socketio.on('stop_browser')
def handle_stop_browser():
"""停止浏览器"""
global agent_instance, browser_running
def stop():
global agent_instance, browser_running
try:
if agent_instance:
web_interface.log('BROWSER', '正在关闭浏览器...')
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
loop.run_until_complete(agent_instance.stop())
loop.close()
agent_instance = None
browser_running = False
web_interface.update_browser_status(False, '浏览器已停止')
web_interface.log('BROWSER', '浏览器已关闭', level='success')
web_interface.print_system('浏览器已停止')
except Exception as e:
web_interface.print_error(f'关闭浏览器出错: {str(e)}')
web_interface.log('ERROR', str(e), level='error')
threading.Thread(target=stop, daemon=True).start()
@socketio.on('user_input')
def handle_user_input(data):
"""处理用户输入"""
global browser_running
if not browser_running or agent_instance is None:
web_interface.print_error('浏览器未启动,请先启动浏览器')
return
content = data.get('content', '').strip()
if not content:
return
# 显示用户消息
web_interface.print_user(content)
web_interface.emit('typing', {})
# 在后台线程中处理
def process():
asyncio.run(process_user_input(content))
threading.Thread(target=process, daemon=True).start()
async def process_user_input(user_input: str):
"""处理用户输入(异步)"""
try:
# 发送用户输入给 DeepSeek
await agent_instance.send_message(user_input)
# 等待并获取响应
response = await agent_instance.wait_for_response()
if response:
# 解析 ACTION
action = agent_instance._parse_action(response)
# 显示完整的原始响应
web_interface.print_ai(response, raw=response)
# 记录日志
web_interface.log('AGENT', f'收到响应: {len(response)} 字符')
if action:
web_interface.log('ACTION', f'检测到 ACTION: {action.get("type")}')
# 执行命令
result = await agent_instance.engine.execute(
action.get('type', ''),
action.get('cmd', ''),
action.get('cwd', ''),
action.get('content', ''),
agent=agent_instance
)
# 显示执行结果
output = result.get('output', '')
web_interface.log('RESULT', f'执行结果: {output[:200]}...' if len(output) > 200 else f'执行结果: {output}')
# 发送结果回 DeepSeek
await agent_instance.send_result(output)
else:
web_interface.print_error('未收到 DeepSeek 的响应')
except Exception as e:
web_interface.print_error(f'处理出错: {str(e)}')
import traceback
web_interface.log('ERROR', traceback.format_exc(), level='error')
@socketio.on('get_terminal_output')
def handle_get_terminal():
"""获取终端输出"""
if agent_instance and agent_instance.engine:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = loop.run_until_complete(
agent_instance.engine.execute('get_terminal_output', '', '', '', agent=agent_instance)
)
loop.close()
web_interface.emit('terminal_output', {'content': result.get('output', '')})
@socketio.on('clear_terminal')
def handle_clear_terminal():
"""清空终端"""
if agent_instance and agent_instance.engine:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
result = loop.run_until_complete(
agent_instance.engine.execute('clear_terminal', '', '', '', agent=agent_instance)
)
loop.close()
web_interface.print_system('终端输出已清空')
def main():
print("=" * 60)
print("Yuanbao Agent Web 控制台")
print("=" * 60)
print(f"请打开浏览器访问: http://localhost:5000")
print("=" * 60)
# 启动 Flask-SocketIO 服务器
socketio.run(app, host='0.0.0.0', port=5000, debug=False, allow_unsafe_werkzeug=True)
if __name__ == '__main__':
main()