⚡ 初始提交 - FutureOSS v1.0 插件化运行时框架
一切皆为插件的开发者工具运行时框架
🧩 核心特性:
- 插件热插拔 (importlib 动态加载)
- 依赖自动解析 (拓扑排序 + 循环检测)
- 企业级稳定 (熔断/降级/重试/隔离)
- 事件驱动 (发布/订阅事件总线)
- 完整配置 (YAML 配置 + 热重载)
This commit is contained in:
50
store/@{FutureOSS}/ws-api/README.md
Normal file
50
store/@{FutureOSS}/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
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
BIN
store/@{FutureOSS}/ws-api/__pycache__/events.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/events.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/ws-api/__pycache__/main.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/main.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/middleware.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/ws-api/__pycache__/router.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/router.cpython-313.pyc
Normal file
Binary file not shown.
BIN
store/@{FutureOSS}/ws-api/__pycache__/server.cpython-313.pyc
Normal file
BIN
store/@{FutureOSS}/ws-api/__pycache__/server.cpython-313.pyc
Normal file
Binary file not shown.
23
store/@{FutureOSS}/ws-api/events.py
Normal file
23
store/@{FutureOSS}/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"
|
||||
30
store/@{FutureOSS}/ws-api/main.py
Normal file
30
store/@{FutureOSS}/ws-api/main.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""WebSocket API 插件入口 - 简化版"""
|
||||
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):
|
||||
"""初始化"""
|
||||
print("[ws-api] 初始化完成")
|
||||
|
||||
def start(self):
|
||||
"""启动"""
|
||||
self._running = True
|
||||
print("[ws-api] 已启动")
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
self._running = False
|
||||
print("[ws-api] 已停止")
|
||||
|
||||
|
||||
register_plugin_type("WsApiPlugin", WsApiPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return WsApiPlugin()
|
||||
18
store/@{FutureOSS}/ws-api/manifest.json
Normal file
18
store/@{FutureOSS}/ws-api/manifest.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "ws-api",
|
||||
"version": "1.0.0",
|
||||
"author": "FutureOSS",
|
||||
"description": "WebSocket API 服务 - 实时双向通信",
|
||||
"type": "protocol"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8081
|
||||
}
|
||||
},
|
||||
"dependencies": [],
|
||||
"permissions": []
|
||||
}
|
||||
41
store/@{FutureOSS}/ws-api/middleware.py
Normal file
41
store/@{FutureOSS}/ws-api/middleware.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""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
|
||||
|
||||
async def next_fn():
|
||||
nonlocal idx
|
||||
if idx < len(self.middlewares):
|
||||
mw = self.middlewares[idx]
|
||||
idx += 1
|
||||
return await mw.process(client, message, next_fn)
|
||||
return message
|
||||
|
||||
return await next_fn()
|
||||
39
store/@{FutureOSS}/ws-api/router.py
Normal file
39
store/@{FutureOSS}/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/@{FutureOSS}/ws-api/server.py
Normal file
125
store/@{FutureOSS}/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