feat: 添加聊天会话界面
- 新增 OpenClaw Gateway 连接器 (api/openclaw_connector.py)
- 新增聊天 WebSocket 路由 (api/chat.py)
- 新增聊天界面模板 (templates/chat/index.html)
- 新增聊天样式 (static/css/chat.css)
- 修改 app.py 支持 SocketIO
- 登录后默认跳转到聊天界面
作者:小白 🐶
This commit is contained in:
155
api/chat.py
Normal file
155
api/chat.py
Normal file
@@ -0,0 +1,155 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
聊天 WebSocket 路由
|
||||
处理前端与 OpenClaw Gateway 之间的消息转发
|
||||
作者:小白 🐶
|
||||
"""
|
||||
|
||||
from flask import request, session
|
||||
from flask_login import current_user
|
||||
from flask_socketio import emit, join_room, leave_room
|
||||
import json
|
||||
import time
|
||||
|
||||
from .openclaw_connector import gateway_manager
|
||||
|
||||
# Gateway 配置(从环境变量或配置文件读取)
|
||||
GATEWAYS = {
|
||||
"local": {
|
||||
"url": "ws://127.0.0.1:18888",
|
||||
"token": "ae4d5989ba173a01cc721200614a8a8a8226724b46d5af13a65089aa628c32b9"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def init_gateways():
|
||||
"""初始化 Gateway 连接"""
|
||||
for name, config in GATEWAYS.items():
|
||||
connector = gateway_manager.add_gateway(
|
||||
name,
|
||||
config["url"],
|
||||
config["token"]
|
||||
)
|
||||
# 设置消息回调
|
||||
connector.message_callback = handle_gateway_response
|
||||
gateway_manager.connect_all()
|
||||
print(f"[Chat] Initialized {len(GATEWAYS)} gateway(s)")
|
||||
|
||||
|
||||
def handle_gateway_response(gateway_name: str, data: dict):
|
||||
"""处理 Gateway 响应并转发给前端"""
|
||||
from app import socketio
|
||||
|
||||
# 获取用户房间
|
||||
user_id = data.get("params", {}).get("userId", "anonymous")
|
||||
room = f"user_{user_id}"
|
||||
|
||||
# 转发给前端
|
||||
socketio.emit('agent_response', {
|
||||
'gateway': gateway_name,
|
||||
'data': data,
|
||||
'timestamp': int(time.time() * 1000)
|
||||
}, room=room, namespace='/chat')
|
||||
|
||||
print(f"[Chat] Forwarded response to {room}")
|
||||
|
||||
|
||||
def register_socket_handlers(socketio):
|
||||
"""注册 Socket.IO 事件处理器"""
|
||||
|
||||
@socketio.on('connect', namespace='/chat')
|
||||
def handle_connect():
|
||||
"""用户连接"""
|
||||
if not current_user.is_authenticated:
|
||||
print("[Chat] Unauthorized connection attempt")
|
||||
return False
|
||||
|
||||
user_id = current_user.id
|
||||
room = f"user_{user_id}"
|
||||
join_room(room)
|
||||
|
||||
# 返回 Gateway 列表和状态
|
||||
emit('connected', {
|
||||
'gateways': gateway_manager.list_gateways(),
|
||||
'status': gateway_manager.get_status(),
|
||||
'userId': user_id
|
||||
})
|
||||
|
||||
print(f"[Chat] User {current_user.username} connected, room: {room}")
|
||||
|
||||
|
||||
@socketio.on('disconnect', namespace='/chat')
|
||||
def handle_disconnect():
|
||||
"""用户断开连接"""
|
||||
if current_user.is_authenticated:
|
||||
room = f"user_{current_user.id}"
|
||||
leave_room(room)
|
||||
print(f"[Chat] User {current_user.username} disconnected")
|
||||
|
||||
|
||||
@socketio.on('send_message', namespace='/chat')
|
||||
def handle_message(data):
|
||||
"""处理用户消息"""
|
||||
if not current_user.is_authenticated:
|
||||
emit('error', {'message': '未授权'})
|
||||
return
|
||||
|
||||
gateway_name = data.get('gateway', 'local')
|
||||
message = data.get('message', '').strip()
|
||||
|
||||
if not message:
|
||||
emit('error', {'message': '消息不能为空'})
|
||||
return
|
||||
|
||||
connector = gateway_manager.get_gateway(gateway_name)
|
||||
|
||||
if connector and connector.connected:
|
||||
# 构造会话 key
|
||||
session_key = f"webchat:user_{current_user.id}"
|
||||
|
||||
# 发送到 Gateway
|
||||
success = connector.send_message(message, session_key)
|
||||
|
||||
if success:
|
||||
# 确认收到
|
||||
emit('message_sent', {
|
||||
'gateway': gateway_name,
|
||||
'message': message,
|
||||
'timestamp': int(time.time() * 1000)
|
||||
})
|
||||
print(f"[Chat] User {current_user.username} sent message to {gateway_name}")
|
||||
else:
|
||||
emit('error', {'message': f'发送失败:Gateway {gateway_name} 连接异常'})
|
||||
else:
|
||||
emit('error', {'message': f'Gateway {gateway_name} 未连接'})
|
||||
|
||||
|
||||
@socketio.on('switch_gateway', namespace='/chat')
|
||||
def handle_switch(data):
|
||||
"""切换 Gateway"""
|
||||
if not current_user.is_authenticated:
|
||||
return
|
||||
|
||||
gateway_name = data.get('gateway')
|
||||
connector = gateway_manager.get_gateway(gateway_name)
|
||||
|
||||
if connector:
|
||||
emit('gateway_changed', {
|
||||
'gateway': gateway_name,
|
||||
'connected': connector.connected,
|
||||
'status': gateway_manager.get_status()
|
||||
})
|
||||
print(f"[Chat] User {current_user.username} switched to {gateway_name}")
|
||||
else:
|
||||
emit('error', {'message': f'Gateway {gateway_name} 不存在'})
|
||||
|
||||
|
||||
@socketio.on('get_status', namespace='/chat')
|
||||
def handle_get_status():
|
||||
"""获取 Gateway 状态"""
|
||||
emit('status_update', {
|
||||
'status': gateway_manager.get_status()
|
||||
})
|
||||
|
||||
print("[Chat] Socket handlers registered")
|
||||
167
api/openclaw_connector.py
Normal file
167
api/openclaw_connector.py
Normal file
@@ -0,0 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
OpenClaw Gateway 连接器
|
||||
用于连接多个 OpenClaw 实例并转发消息
|
||||
作者:小白 🐶
|
||||
"""
|
||||
|
||||
import websocket
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
from typing import Dict, Optional, Callable, Any
|
||||
|
||||
|
||||
class OpenClawConnector:
|
||||
"""OpenClaw Gateway 连接器"""
|
||||
|
||||
def __init__(self, gateway_url: str, token: str, name: str = "default"):
|
||||
self.gateway_url = gateway_url
|
||||
self.token = token
|
||||
self.name = name
|
||||
self.ws: Optional[websocket.WebSocketApp] = None
|
||||
self.connected = False
|
||||
self.message_callback: Optional[Callable[[str, dict], None]] = None
|
||||
self._reconnect_delay = 1
|
||||
self._max_reconnect_delay = 30
|
||||
self._should_reconnect = True
|
||||
|
||||
def connect(self):
|
||||
"""连接到 OpenClaw Gateway"""
|
||||
if not self.gateway_url or not self.token:
|
||||
print(f"[Connector:{self.name}] Missing gateway_url or token")
|
||||
return
|
||||
|
||||
url = f"{self.gateway_url}?token={self.token}"
|
||||
print(f"[Connector:{self.name}] Connecting to {self.gateway_url}...")
|
||||
|
||||
try:
|
||||
self.ws = websocket.WebSocketApp(
|
||||
url,
|
||||
on_open=self._on_open,
|
||||
on_message=self._on_message,
|
||||
on_error=self._on_error,
|
||||
on_close=self._on_close
|
||||
)
|
||||
|
||||
# 启动后台线程
|
||||
thread = threading.Thread(target=self._run_forever, daemon=True)
|
||||
thread.start()
|
||||
except Exception as e:
|
||||
print(f"[Connector:{self.name}] Connection error: {e}")
|
||||
|
||||
def _run_forever(self):
|
||||
"""后台运行 WebSocket"""
|
||||
while self._should_reconnect:
|
||||
try:
|
||||
self.ws.run_forever()
|
||||
except Exception as e:
|
||||
print(f"[Connector:{self.name}] WebSocket error: {e}")
|
||||
|
||||
# 重连逻辑
|
||||
if self._should_reconnect:
|
||||
print(f"[Connector:{self.name}] Reconnecting in {self._reconnect_delay}s...")
|
||||
time.sleep(self._reconnect_delay)
|
||||
self._reconnect_delay = min(self._reconnect_delay * 2, self._max_reconnect_delay)
|
||||
|
||||
def _on_open(self, ws):
|
||||
self.connected = True
|
||||
self._reconnect_delay = 1
|
||||
print(f"[Connector:{self.name}] ✅ Connected")
|
||||
|
||||
def _on_message(self, ws, message):
|
||||
"""收到 Gateway 消息"""
|
||||
try:
|
||||
data = json.loads(message)
|
||||
print(f"[Connector:{self.name}] Received: {data.get('method', 'unknown')}")
|
||||
if self.message_callback:
|
||||
self.message_callback(self.name, data)
|
||||
except json.JSONDecodeError:
|
||||
print(f"[Connector:{self.name}] Invalid JSON: {message[:100]}")
|
||||
|
||||
def _on_error(self, ws, error):
|
||||
print(f"[Connector:{self.name}] Error: {error}")
|
||||
|
||||
def _on_close(self, ws, close_status_code, close_msg):
|
||||
self.connected = False
|
||||
print(f"[Connector:{self.name}] Disconnected: {close_status_code} {close_msg}")
|
||||
|
||||
def send_message(self, message: str, session_key: str = None) -> bool:
|
||||
"""发送消息到 Gateway"""
|
||||
if not self.connected or not self.ws:
|
||||
print(f"[Connector:{self.name}] Not connected")
|
||||
return False
|
||||
|
||||
payload = {
|
||||
"method": "agent.turn",
|
||||
"id": str(int(time.time() * 1000)),
|
||||
"params": {
|
||||
"message": message,
|
||||
"sessionKey": session_key or f"webchat:{self.name}",
|
||||
"deliver": False # 不直接发送,等待响应
|
||||
}
|
||||
}
|
||||
|
||||
try:
|
||||
self.ws.send(json.dumps(payload))
|
||||
print(f"[Connector:{self.name}] Sent message: {message[:50]}...")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"[Connector:{self.name}] Send error: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""断开连接"""
|
||||
self._should_reconnect = False
|
||||
if self.ws:
|
||||
self.ws.close()
|
||||
|
||||
|
||||
class MultiGatewayManager:
|
||||
"""多 Gateway 连接管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.gateways: Dict[str, OpenClawConnector] = {}
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def add_gateway(self, name: str, url: str, token: str) -> OpenClawConnector:
|
||||
"""添加 Gateway 连接"""
|
||||
with self._lock:
|
||||
if name in self.gateways:
|
||||
self.gateways[name].disconnect()
|
||||
|
||||
connector = OpenClawConnector(url, token, name)
|
||||
self.gateways[name] = connector
|
||||
return connector
|
||||
|
||||
def connect_all(self):
|
||||
"""连接所有 Gateway"""
|
||||
for connector in self.gateways.values():
|
||||
connector.connect()
|
||||
|
||||
def get_gateway(self, name: str) -> Optional[OpenClawConnector]:
|
||||
return self.gateways.get(name)
|
||||
|
||||
def list_gateways(self) -> list:
|
||||
return list(self.gateways.keys())
|
||||
|
||||
def get_status(self) -> Dict[str, Any]:
|
||||
"""获取所有 Gateway 状态"""
|
||||
return {
|
||||
name: {
|
||||
"connected": connector.connected,
|
||||
"url": connector.gateway_url
|
||||
}
|
||||
for name, connector in self.gateways.items()
|
||||
}
|
||||
|
||||
def disconnect_all(self):
|
||||
"""断开所有连接"""
|
||||
for connector in self.gateways.values():
|
||||
connector.disconnect()
|
||||
self.gateways.clear()
|
||||
|
||||
|
||||
# 全局单例
|
||||
gateway_manager = MultiGatewayManager()
|
||||
Reference in New Issue
Block a user