870 lines
26 KiB
Python
870 lines
26 KiB
Python
"""
|
|||
|
|
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(
|
||
|
|
/(<<<ACTION\s*)(\{[\s\S]*?\})(\s*ACTION>>>)/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()
|