更改项目名为NebulaShell
This commit is contained in:
50
store/@{NebulaShell}/ws-api/README.md
Normal file
50
store/@{NebulaShell}/ws-api/README.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# ws-api WebSocket API
|
||||
|
||||
提供 WebSocket 实时双向通信服务。
|
||||
|
||||
## 功能
|
||||
|
||||
- WebSocket 服务器
|
||||
- 路由匹配
|
||||
- 中间件链(认证/日志)
|
||||
- 广播消息
|
||||
- 连接/断开/消息事件
|
||||
- 与 HTTP 插件集成
|
||||
|
||||
## 使用
|
||||
|
||||
```python
|
||||
ws = plugin_mgr.get("ws-api")
|
||||
|
||||
# 注册消息路由
|
||||
ws.router.on_message("/chat", lambda client, msg: client.send({"echo": msg}))
|
||||
|
||||
# 广播
|
||||
ws.server.broadcast({"type": "announcement", "data": "服务器维护通知"})
|
||||
|
||||
# 获取客户端列表
|
||||
clients = ws.server.get_clients()
|
||||
```
|
||||
|
||||
## 事件
|
||||
|
||||
```python
|
||||
# 通过 plugin-bridge 订阅 WS 事件
|
||||
bridge = plugin_mgr.get("plugin-bridge")
|
||||
bridge.event_bus.on("ws.connect", lambda event: print(f"新连接: {event.client.path}"))
|
||||
bridge.event_bus.on("ws.message", lambda event: print(f"消息: {event.message}"))
|
||||
bridge.event_bus.on("ws.disconnect", lambda event: print(f"断开: {event.client.id}"))
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8081
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
8
store/@{NebulaShell}/ws-api/SIGNATURE
Normal file
8
store/@{NebulaShell}/ws-api/SIGNATURE
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"signature": "jCGTXt+uLD/djjYQIxogWXFztNVz4Y/Xyzsj3JaXtvrvNppdUm7Ei8/N08l+dZxqU7Lpjg2UIMakX2jVXSE2rlZlTZyL5cfvxr3j5mdDXP4VhXScdVwEaeScDz0qUn23e6qOqEdS+KbjK6pZCk34GjwBoV2/ze8DteK+whSMCSwqxYASHNTTrNhELKoDhTBnnlCEAAlEM6y95EIpdL6FsvJRP8w8xgg1Ah2gRvQdyC1785zbGdFJ3Oib6mw1fuKaV90Phqb8qCjD8qePqocdSArNJ/Jz+073lVH0IZMUw45cIF1uBgpXErnJrnY2KWtSLpC9AGK62vNLMjTYnKa7HZ0J2xwmg89skW2w3YSaiZweijelFgut7tmgdof52Xvb6hdnzNWTAKorT6C8d6jZWzNv0BrJKGtThzCoyBhTQOSiEfgz+QO2yFpbWxCtMjX1SfVEOaWWJq5H5fgTu6YqCJlwSU3ur3pdBLaBNYH3PMYW4aIOUs5mOzpnFBq1FTJmfz9BT9ZEIK/7bbVjCfifMMH1Xkq2gudQDfElok7WZQ3CaHNzms4wbwbS6yIzzRggDo1s7tTXR0i5AazoUOCKj6cxTqwPnBiO19J0Okjhj8TkApbNHAoezcgYHuq1HxxZ/ckGxs/5yVuN0qkBGESwfWPRmXd/CS8ddhIowSOds9c=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775964952.9217672,
|
||||
"plugin_hash": "b056cef4d2a2aceeeb199ef47fb709b021f9d2a7198109fa4023e3f2ded4b108",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
23
store/@{NebulaShell}/ws-api/events.py
Normal file
23
store/@{NebulaShell}/ws-api/events.py
Normal file
@@ -0,0 +1,23 @@
|
||||
"""WebSocket 事件定义"""
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
@dataclass
|
||||
class WsEvent:
|
||||
"""WebSocket 事件"""
|
||||
type: str
|
||||
client: Any = None
|
||||
path: str = ""
|
||||
message: str = ""
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
# 事件类型常量
|
||||
EVENT_CONNECT = "ws.connect"
|
||||
EVENT_DISCONNECT = "ws.disconnect"
|
||||
EVENT_MESSAGE = "ws.message"
|
||||
EVENT_ERROR = "ws.error"
|
||||
EVENT_SUBSCRIBE = "ws.subscribe"
|
||||
EVENT_UNSUBSCRIBE = "ws.unsubscribe"
|
||||
EVENT_BROADCAST = "ws.broadcast"
|
||||
31
store/@{NebulaShell}/ws-api/main.py
Normal file
31
store/@{NebulaShell}/ws-api/main.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""WebSocket API 插件入口 - 简化版"""
|
||||
from oss.logger.logger import Log
|
||||
from oss.plugin.types import Plugin, register_plugin_type
|
||||
|
||||
|
||||
class WsApiPlugin(Plugin):
|
||||
"""WebSocket API 插件"""
|
||||
|
||||
def __init__(self):
|
||||
self._running = False
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
"""初始化"""
|
||||
Log.info("ws-api", "初始化完成")
|
||||
|
||||
def start(self):
|
||||
"""启动"""
|
||||
self._running = True
|
||||
Log.info("ws-api", "已启动")
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
self._running = False
|
||||
Log.error("ws-api", "已停止")
|
||||
|
||||
|
||||
register_plugin_type("WsApiPlugin", WsApiPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return WsApiPlugin()
|
||||
22
store/@{NebulaShell}/ws-api/manifest.json
Normal file
22
store/@{NebulaShell}/ws-api/manifest.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "ws-api",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "WebSocket API 服务 - 实时双向通信/多语言支持/安全认证",
|
||||
"type": "protocol"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8081,
|
||||
"ssl_enabled": false,
|
||||
"heartbeat_interval": 30,
|
||||
"max_connections": 1000,
|
||||
"auth_enabled": true
|
||||
}
|
||||
},
|
||||
"dependencies": ["i18n"],
|
||||
"permissions": ["lifecycle"]
|
||||
}
|
||||
44
store/@{NebulaShell}/ws-api/middleware.py
Normal file
44
store/@{NebulaShell}/ws-api/middleware.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""WebSocket 中间件链"""
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
|
||||
class WsMiddleware:
|
||||
"""WebSocket 中间件基类"""
|
||||
async def process(self, client: Any, message: str, next_fn: Callable) -> Optional[str]:
|
||||
"""处理消息"""
|
||||
return await next_fn()
|
||||
|
||||
|
||||
class AuthMiddleware(WsMiddleware):
|
||||
"""认证中间件"""
|
||||
async def process(self, client, message, next_fn):
|
||||
# 可以在这里验证 token
|
||||
return await next_fn()
|
||||
|
||||
|
||||
class WsMiddlewareChain:
|
||||
"""WebSocket 中间件链"""
|
||||
|
||||
def __init__(self):
|
||||
self.middlewares: list[WsMiddleware] = []
|
||||
|
||||
def add(self, middleware: WsMiddleware):
|
||||
"""添加中间件"""
|
||||
self.middlewares.append(middleware)
|
||||
|
||||
async def run(self, client, message) -> Optional[str]:
|
||||
"""执行中间件链"""
|
||||
idx = 0
|
||||
current_message = message
|
||||
|
||||
async def next_fn(msg=None):
|
||||
nonlocal idx, current_message
|
||||
if msg is not None:
|
||||
current_message = msg
|
||||
if idx < len(self.middlewares):
|
||||
mw = self.middlewares[idx]
|
||||
idx += 1
|
||||
return await mw.process(client, current_message, next_fn)
|
||||
return current_message
|
||||
|
||||
return await next_fn()
|
||||
39
store/@{NebulaShell}/ws-api/router.py
Normal file
39
store/@{NebulaShell}/ws-api/router.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""WebSocket 路由器"""
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Callable, Optional, Any
|
||||
from .server import WsClient
|
||||
|
||||
|
||||
class WsRoute:
|
||||
"""WebSocket 路由"""
|
||||
def __init__(self, path: str, handler: Callable):
|
||||
self.path = path
|
||||
self.handler = handler
|
||||
|
||||
|
||||
class WsRouter:
|
||||
"""WebSocket 路由器"""
|
||||
|
||||
def __init__(self):
|
||||
self.routes: dict[str, WsRoute] = {}
|
||||
|
||||
def on_message(self, path: str, handler: Callable):
|
||||
"""注册消息路由"""
|
||||
self.routes[path] = WsRoute(path, handler)
|
||||
|
||||
async def handle(self, client: WsClient, path: str, message: str):
|
||||
"""处理消息"""
|
||||
# 精确匹配
|
||||
if path in self.routes:
|
||||
await self.routes[path].handler(client, message)
|
||||
return
|
||||
|
||||
# 前缀匹配
|
||||
for route_path, route in self.routes.items():
|
||||
if path.startswith(route_path):
|
||||
await route.handler(client, message)
|
||||
return
|
||||
|
||||
# 无匹配路由
|
||||
await client.send({"error": "No handler for path", "path": path})
|
||||
125
store/@{NebulaShell}/ws-api/server.py
Normal file
125
store/@{NebulaShell}/ws-api/server.py
Normal file
@@ -0,0 +1,125 @@
|
||||
"""WebSocket 服务器核心"""
|
||||
import asyncio
|
||||
import websockets
|
||||
import threading
|
||||
import json
|
||||
from typing import Any, Callable, Optional
|
||||
from .events import WsEvent, EVENT_CONNECT, EVENT_DISCONNECT, EVENT_MESSAGE
|
||||
|
||||
|
||||
class WsClient:
|
||||
"""WebSocket 客户端连接"""
|
||||
|
||||
def __init__(self, websocket, path: str):
|
||||
self.websocket = websocket
|
||||
self.path = path
|
||||
self.id = id(websocket)
|
||||
self.closed = False
|
||||
|
||||
async def send(self, message: Any):
|
||||
"""发送消息"""
|
||||
if not self.closed:
|
||||
data = json.dumps(message, ensure_ascii=False) if isinstance(message, dict) else str(message)
|
||||
await self.websocket.send(data)
|
||||
|
||||
async def close(self):
|
||||
"""关闭连接"""
|
||||
self.closed = True
|
||||
await self.websocket.close()
|
||||
|
||||
|
||||
class WsServer:
|
||||
"""WebSocket 服务器"""
|
||||
|
||||
def __init__(self, router, middleware, event_bus, host="0.0.0.0", port=8081):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.router = router
|
||||
self.middleware = middleware
|
||||
self.event_bus = event_bus
|
||||
self._server = None
|
||||
self._loop = None
|
||||
self._thread = None
|
||||
self._clients: dict[int, WsClient] = {}
|
||||
|
||||
def start(self):
|
||||
"""启动服务器"""
|
||||
self._loop = asyncio.new_event_loop()
|
||||
self._thread = threading.Thread(target=self._run_loop, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _run_loop(self):
|
||||
"""运行事件循环"""
|
||||
asyncio.set_event_loop(self._loop)
|
||||
start_server = websockets.serve(
|
||||
self._handle_connection,
|
||||
self.host,
|
||||
self.port
|
||||
)
|
||||
self._loop.run_until_complete(start_server)
|
||||
self._loop.run_forever()
|
||||
|
||||
async def _handle_connection(self, websocket, path=None):
|
||||
"""处理客户端连接(兼容 websockets 新旧版本)"""
|
||||
# websockets 16.0+ 只传入 connection 参数
|
||||
if path is None:
|
||||
# 新版本:从 websocket.request 获取路径
|
||||
try:
|
||||
path = websocket.request.path
|
||||
except AttributeError:
|
||||
path = "/"
|
||||
|
||||
client = WsClient(websocket, path)
|
||||
self._clients[client.id] = client
|
||||
|
||||
# 触发连接事件
|
||||
self.event_bus.emit(WsEvent(
|
||||
type=EVENT_CONNECT,
|
||||
client=client,
|
||||
path=path
|
||||
))
|
||||
|
||||
try:
|
||||
async for message in websocket:
|
||||
# 触发消息事件
|
||||
self.event_bus.emit(WsEvent(
|
||||
type=EVENT_MESSAGE,
|
||||
client=client,
|
||||
path=path,
|
||||
message=message
|
||||
))
|
||||
|
||||
# 路由处理
|
||||
await self.router.handle(client, path, message)
|
||||
|
||||
except websockets.exceptions.ConnectionClosed:
|
||||
pass
|
||||
finally:
|
||||
del self._clients[client.id]
|
||||
# 触发断开事件
|
||||
self.event_bus.emit(WsEvent(
|
||||
type=EVENT_DISCONNECT,
|
||||
client=client,
|
||||
path=path
|
||||
))
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
if self._loop and self._loop.is_running():
|
||||
self._loop.call_soon_threadsafe(self._loop.stop)
|
||||
print("[ws-api] 服务器已停止")
|
||||
|
||||
def broadcast(self, message: Any, exclude_client: int = None):
|
||||
"""广播消息"""
|
||||
async def _broadcast():
|
||||
for client_id, client in self._clients.items():
|
||||
if exclude_client and client_id == exclude_client:
|
||||
continue
|
||||
await client.send(message)
|
||||
|
||||
if self._loop and self._loop.is_running():
|
||||
asyncio.run_coroutine_threadsafe(_broadcast(), self._loop)
|
||||
|
||||
def get_clients(self) -> list[WsClient]:
|
||||
"""获取所有客户端"""
|
||||
return list(self._clients.values())
|
||||
Reference in New Issue
Block a user