init: DeepSeek Agent Web 控制台
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.pyd
|
||||
agent_debug.log
|
||||
.workbuddy/
|
||||
@@ -0,0 +1,28 @@
|
||||
# 元宝全自动代理
|
||||
|
||||
## 安装(conda 版本)
|
||||
|
||||
```bash
|
||||
# 创建 conda 环境
|
||||
conda create -n yuanbao-agent python=3.12 -y
|
||||
|
||||
# 激活环境
|
||||
conda activate yuanbao-agent
|
||||
|
||||
# 安装 playwright(包含浏览器)
|
||||
pip install playwright
|
||||
python -m playwright install chrome
|
||||
```
|
||||
|
||||
## 运行
|
||||
|
||||
```bash
|
||||
# 确保在 yuanbao-agent 环境中
|
||||
conda activate yuanbao-agent
|
||||
|
||||
# 运行 MVP v1(显式指令)
|
||||
python yuanbao_agent_mvp.py
|
||||
|
||||
# 运行 v2 智能版(推荐)
|
||||
python yuanbao_agent_v2.py
|
||||
```
|
||||
@@ -0,0 +1,85 @@
|
||||
"""
|
||||
探测元宝页面的生成状态特征
|
||||
运行后,请在元宝页面发送一条消息,观察输出
|
||||
"""
|
||||
import asyncio
|
||||
from playwright.async_api import async_playwright
|
||||
from pathlib import Path
|
||||
|
||||
YUANBAO_URL = "https://yuanbao.tencent.com/chat"
|
||||
|
||||
async def detect():
|
||||
async with async_playwright() as p:
|
||||
user_data = str(Path.home() / ".yuanbao_chat_profile")
|
||||
context = await p.chromium.launch_persistent_context(
|
||||
user_data, channel="chrome", headless=False,
|
||||
args=["--start-maximized"], no_viewport=True
|
||||
)
|
||||
|
||||
page = context.pages[0] if context.pages else await context.new_page()
|
||||
await page.goto(YUANBAO_URL)
|
||||
|
||||
print("=" * 60)
|
||||
print("元宝生成状态探测器")
|
||||
print("=" * 60)
|
||||
print("\n1. 请在元宝页面登录并创建新对话")
|
||||
print("2. 在元宝页面发送一条消息(比如'你好')")
|
||||
print("3. 观察生成过程中和生成完成后的输出差异")
|
||||
print("4. 按 Ctrl+C 退出")
|
||||
print("=" * 60)
|
||||
|
||||
input("\n准备好后按回车开始探测...")
|
||||
|
||||
# 持续探测
|
||||
prev_state = None
|
||||
counter = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
# 获取最后一条AI消息的完整信息
|
||||
result = await page.evaluate("""
|
||||
() => {
|
||||
const aiMessages = document.querySelectorAll(
|
||||
'.agent-chat__list__item--ai, [class*="agent-chat"][class*="ai"], [class*="message"][class*="ai"]'
|
||||
);
|
||||
if (aiMessages.length === 0) return null;
|
||||
|
||||
const lastMsg = aiMessages[aiMessages.length - 1];
|
||||
const textEl = lastMsg.querySelector('.agent-chat__speech-text, [class*="text"], [class*="content"]');
|
||||
|
||||
return {
|
||||
className: lastMsg.className,
|
||||
innerHTML: lastMsg.innerHTML.substring(0, 500),
|
||||
textContent: textEl ? textEl.innerText.substring(0, 200) : '',
|
||||
hasCursor: lastMsg.innerHTML.includes('cursor') || lastMsg.innerHTML.includes('blink'),
|
||||
childElements: Array.from(lastMsg.children).map(c => c.tagName + '.' + c.className).slice(0, 5)
|
||||
};
|
||||
}
|
||||
""")
|
||||
|
||||
if result:
|
||||
state_hash = f"{result['className']}_{result['hasCursor']}"
|
||||
|
||||
if state_hash != prev_state:
|
||||
prev_state = state_hash
|
||||
counter += 1
|
||||
print(f"\n--- 探测 #{counter} ---")
|
||||
print(f"Class: {result['className']}")
|
||||
print(f"Has Cursor: {result['hasCursor']}")
|
||||
print(f"Child Elements: {result['childElements']}")
|
||||
print(f"Text Preview: {result['textContent'][:100]}...")
|
||||
print(f"HTML Snippet: {result['innerHTML'][:200]}...")
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
await context.close()
|
||||
print("\n探测结束")
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(detect())
|
||||
+869
@@ -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(
|
||||
/(<<<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()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user