351 lines
13 KiB
Python
351 lines
13 KiB
Python
#!/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)}"
|
||
} |