VWED_server/routes/script_websocket_api.py

351 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
脚本WebSocket API路由
提供实时日志推送和脚本状态监控
"""
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query
from fastapi.responses import HTMLResponse
import json
from typing import Dict, Any
from services.script_websocket_service import get_websocket_manager
from utils.logger import get_logger
logger = get_logger("routes.script_websocket_api")
router = APIRouter()
@router.websocket("/ws/script-logs")
async def script_logs_websocket(
websocket: WebSocket,
client_id: str = Query(..., description="客户端ID"),
client_type: str = Query(default="web", description="客户端类型")
):
"""脚本日志实时推送WebSocket连接"""
websocket_manager = get_websocket_manager()
connection_id = None
try:
# 建立连接
connection_id = await websocket_manager.connect(websocket, client_id, client_type)
while True:
# 接收客户端消息
data = await websocket.receive_text()
message = json.loads(data)
message_type = message.get("type")
if message_type == "subscribe":
# 订阅脚本日志
script_id = message.get("script_id")
if script_id:
success = await websocket_manager.subscribe_script(connection_id, script_id)
if not success:
await websocket.send_text(json.dumps({
"type": "error",
"message": f"订阅脚本 {script_id} 失败"
}))
elif message_type == "unsubscribe":
# 取消订阅脚本日志
script_id = message.get("script_id")
if script_id:
success = await websocket_manager.unsubscribe_script(connection_id, script_id)
if not success:
await websocket.send_text(json.dumps({
"type": "error",
"message": f"取消订阅脚本 {script_id} 失败"
}))
elif message_type == "ping":
# 心跳响应
await websocket.send_text(json.dumps({
"type": "pong",
"timestamp": message.get("timestamp")
}))
elif message_type == "get_status":
# 获取连接状态
status = await websocket_manager.get_connection_status()
await websocket.send_text(json.dumps({
"type": "status",
"data": status
}))
else:
await websocket.send_text(json.dumps({
"type": "error",
"message": f"未知消息类型: {message_type}"
}))
except WebSocketDisconnect:
logger.info(f"WebSocket客户端断开连接: {client_id}")
except json.JSONDecodeError:
logger.error(f"WebSocket消息JSON解析失败: {data}")
try:
await websocket.send_text(json.dumps({
"type": "error",
"message": "消息格式错误请发送有效的JSON"
}))
except:
pass
except Exception as e:
logger.error(f"WebSocket处理异常: {e}", exc_info=True)
finally:
# 清理连接
if connection_id:
await websocket_manager.disconnect(connection_id)
@router.get("/ws/test-page")
async def get_websocket_test_page():
"""获取WebSocket测试页面"""
html = """
<!DOCTYPE html>
<html>
<head>
<title>VWED 脚本日志 WebSocket 测试</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; }
.container { max-width: 1200px; margin: 0 auto; }
.section { margin: 20px 0; padding: 15px; border: 1px solid #ddd; border-radius: 5px; }
.controls { background-color: #f5f5f5; }
.logs { background-color: #f9f9f9; height: 400px; overflow-y: scroll; }
input, button, select { margin: 5px; padding: 5px; }
.log-entry { margin: 2px 0; padding: 5px; border-radius: 3px; }
.log-info { background-color: #d4edda; }
.log-warning { background-color: #fff3cd; }
.log-error { background-color: #f8d7da; }
.log-debug { background-color: #d1ecf1; }
.status { background-color: #e2e3e5; }
</style>
</head>
<body>
<div class="container">
<h1>VWED 脚本日志 WebSocket 测试</h1>
<div class="section controls">
<h3>连接控制</h3>
<input type="text" id="clientId" placeholder="客户端ID" value="test-client-1">
<select id="clientType">
<option value="web">Web</option>
<option value="admin">Admin</option>
<option value="monitor">Monitor</option>
</select>
<button onclick="connect()">连接</button>
<button onclick="disconnect()">断开</button>
<span id="connectionStatus">未连接</span>
</div>
<div class="section controls">
<h3>订阅控制</h3>
<input type="text" id="scriptId" placeholder="脚本ID" value="test_script_123">
<button onclick="subscribe()">订阅</button>
<button onclick="unsubscribe()">取消订阅</button>
<button onclick="getStatus()">获取状态</button>
<button onclick="clearLogs()">清空日志</button>
</div>
<div class="section logs">
<h3>实时日志</h3>
<div id="logContainer"></div>
</div>
</div>
<script>
let ws = null;
let clientId = '';
function connect() {
clientId = document.getElementById('clientId').value;
const clientType = document.getElementById('clientType').value;
if (!clientId) {
alert('请输入客户端ID');
return;
}
ws = new WebSocket(`ws://localhost:8000/api/script/ws/script-logs?client_id=${clientId}&client_type=${clientType}`);
ws.onopen = function(event) {
document.getElementById('connectionStatus').textContent = '已连接';
addLog('系统', 'WebSocket连接建立', 'status');
};
ws.onmessage = function(event) {
const message = JSON.parse(event.data);
handleMessage(message);
};
ws.onerror = function(error) {
addLog('系统', '连接错误: ' + error, 'error');
};
ws.onclose = function(event) {
document.getElementById('connectionStatus').textContent = '已断开';
addLog('系统', 'WebSocket连接关闭', 'status');
};
}
function disconnect() {
if (ws) {
ws.close();
ws = null;
}
}
function subscribe() {
const scriptId = document.getElementById('scriptId').value;
if (!scriptId || !ws) {
alert('请先连接并输入脚本ID');
return;
}
ws.send(JSON.stringify({
type: 'subscribe',
script_id: scriptId
}));
}
function unsubscribe() {
const scriptId = document.getElementById('scriptId').value;
if (!scriptId || !ws) {
alert('请先连接并输入脚本ID');
return;
}
ws.send(JSON.stringify({
type: 'unsubscribe',
script_id: scriptId
}));
}
function getStatus() {
if (!ws) {
alert('请先连接');
return;
}
ws.send(JSON.stringify({
type: 'get_status'
}));
}
function clearLogs() {
document.getElementById('logContainer').innerHTML = '';
}
function handleMessage(message) {
switch(message.type) {
case 'welcome':
addLog('系统', message.message + ' (连接ID: ' + message.connection_id + ')', 'status');
break;
case 'script_log':
addLog('脚本日志', `[${message.script_id}] ${message.message}`, message.level.toLowerCase());
break;
case 'script_status':
addLog('脚本状态', `[${message.script_id}] 状态: ${message.status} - ${message.message}`, 'status');
break;
case 'function_execution':
addLog('函数执行', `[${message.script_id}] ${message.function_name}: ${JSON.stringify(message.result)}`, 'info');
break;
case 'subscription_success':
addLog('系统', message.message, 'status');
break;
case 'unsubscription_success':
addLog('系统', message.message, 'status');
break;
case 'status':
addLog('状态信息', JSON.stringify(message.data, null, 2), 'info');
break;
case 'error':
addLog('错误', message.message, 'error');
break;
case 'ping':
ws.send(JSON.stringify({type: 'pong', timestamp: message.timestamp}));
break;
case 'pong':
addLog('系统', '心跳响应', 'debug');
break;
default:
addLog('未知', JSON.stringify(message), 'debug');
}
}
function addLog(source, message, level = 'info') {
const logContainer = document.getElementById('logContainer');
const logEntry = document.createElement('div');
logEntry.className = `log-entry log-${level}`;
const timestamp = new Date().toLocaleTimeString();
logEntry.innerHTML = `<strong>[${timestamp}] [${source}]</strong> ${message}`;
logContainer.appendChild(logEntry);
logContainer.scrollTop = logContainer.scrollHeight;
}
// 定期发送心跳
setInterval(function() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'ping',
timestamp: new Date().toISOString()
}));
}
}, 30000);
</script>
</body>
</html>
"""
return HTMLResponse(content=html)
@router.get("/ws/connections/status", summary="获取WebSocket连接状态")
async def get_websocket_connections_status():
"""获取当前WebSocket连接状态"""
try:
websocket_manager = get_websocket_manager()
status = await websocket_manager.get_connection_status()
return {
"success": True,
"data": status
}
except Exception as e:
logger.error(f"获取WebSocket连接状态失败: {e}", exc_info=True)
return {
"success": False,
"error": f"获取WebSocket连接状态失败: {str(e)}"
}
@router.post("/ws/broadcast/test", summary="测试广播消息")
async def test_broadcast_message(request: Dict[str, Any]):
"""测试向指定脚本的订阅者广播消息"""
try:
script_id = request.get("script_id")
message = request.get("message", "测试消息")
level = request.get("level", "info")
if not script_id:
return {"success": False, "error": "缺少script_id参数"}
websocket_manager = get_websocket_manager()
await websocket_manager.broadcast_script_log(script_id, level.upper(), message)
return {
"success": True,
"message": f"测试消息已广播到脚本 {script_id} 的订阅者"
}
except Exception as e:
logger.error(f"测试广播消息失败: {e}", exc_info=True)
return {
"success": False,
"error": f"测试广播消息失败: {str(e)}"
}