VWED_server/routes/script_websocket_api.py

351 lines
13 KiB
Python
Raw Normal View History

2025-09-12 16:15:13 +08:00
#!/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
2025-09-20 16:50:45 +08:00
from services.online_script.script_websocket_service import get_websocket_manager
2025-09-12 16:15:13 +08:00
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)}"
}