更改项目名为NebulaShell
This commit is contained in:
53
store/@{NebulaShell}/http-api/README.md
Normal file
53
store/@{NebulaShell}/http-api/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# http-api HTTP API 服务
|
||||
|
||||
提供 HTTP RESTful API 服务,支持路由、中间件等功能。
|
||||
|
||||
## 功能
|
||||
|
||||
- HTTP 服务器(GET/POST/PUT/DELETE)
|
||||
- 路由匹配(支持参数路由 `:id`)
|
||||
- 中间件链(CORS/日志/限流)
|
||||
- 分散式布局(每个文件 < 200 行)
|
||||
|
||||
## 路由使用
|
||||
|
||||
```python
|
||||
# 在插件中获取 router
|
||||
http_plugin = plugin_mgr.get("http-api")
|
||||
router = http_plugin.router
|
||||
|
||||
# 添加路由
|
||||
router.get("/health", lambda req: Response(status=200, body='{"status": "ok"}'))
|
||||
router.get("/api/users", handle_users)
|
||||
router.post("/api/users", handle_create_user)
|
||||
router.get("/api/users/:id", handle_user_by_id)
|
||||
```
|
||||
|
||||
## 中间件
|
||||
|
||||
```python
|
||||
middleware = http_plugin.middleware
|
||||
|
||||
# 添加自定义中间件
|
||||
class MyMiddleware(Middleware):
|
||||
def process(self, ctx, next_fn):
|
||||
# 前置处理
|
||||
resp = next_fn() # 继续执行
|
||||
# 后置处理
|
||||
return resp
|
||||
|
||||
middleware.add(MyMiddleware())
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8080
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
8
store/@{NebulaShell}/http-api/SIGNATURE
Normal file
8
store/@{NebulaShell}/http-api/SIGNATURE
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"signature": "0WK7Njn0KAUP+jfg/uuJxwW0/tWCF+WieK0N0T2crWbvutKQmEOtaNDHnjT6qFz1dcI4+ba3julE4fFi3W3xFiToMEP2VcPXe0WNQ9/kvKNTKSDbwadiBssf43TO1G9E1BxNMxVM91mN8iqybuy+VMdU0Esv2rJ5dcwwwsnT9NWot2RQLez75PRhmMtJpEWRUmrZn2r+u5QnQdjxucONq9Nhwxw0eheTxMCu8IDvIiO6QIWP5ErA/wUz+Hg6IoEZwcVif/lSN2EMqNGqPNR/nIWWVXo9CXWB9qMZZApgEnAZfKYGCAkLzSTwqG64T4iJh4deGxafyMhsONckqRaG82NRTLuzHMReP5+VAichuEGbHI7nxXFOFG7q1mgQQLmHm3LB577usAgCNCh5X3i8SMAj7Sutykxhj0ZyTqMnOfpwnzE2tsNisJF0/8Kw22k7dZChV1obOeLWXjy5InLjdm4hIWTp7wMPjSNWRMZGR+1aZHi9XA1GKd965/30jmo876EXX23xoTAN4ZRhZNlcQg710LhycNohggnQ7qzB9LsV3Ckgh7aY/V/hzND6bpRADCGu62sZtBye2P1yaaAorC8+hRaiJoXlV9Yukg+3yhfKC+qTbn307fI53kgcw1KMSeGGctfTYJUOfK8u0mYsGi50bnM+2Tz45YJiwwdOJJk=",
|
||||
"signer": "NebulaShell",
|
||||
"algorithm": "RSA-SHA256",
|
||||
"timestamp": 1775960645.890869,
|
||||
"plugin_hash": "ca13c933ffa2c5dd8874e3ad6f7b8dda5dd9a5f9c24be6aeb47228d65097a280",
|
||||
"author": "NebulaShell"
|
||||
}
|
||||
59
store/@{NebulaShell}/http-api/events.py
Normal file
59
store/@{NebulaShell}/http-api/events.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""HTTP 事件系统 - 请求/响应生命周期事件"""
|
||||
from typing import Callable, Any, Optional
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
|
||||
@dataclass
|
||||
class HttpEvent:
|
||||
"""HTTP 事件"""
|
||||
type: str # request, response, error, etc
|
||||
request: Any = None
|
||||
response: Any = None
|
||||
error: Exception = None
|
||||
context: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class HttpEventBus:
|
||||
"""HTTP 事件总线"""
|
||||
|
||||
def __init__(self):
|
||||
self._handlers: dict[str, list[Callable]] = {}
|
||||
|
||||
def on(self, event_type: str, handler: Callable):
|
||||
"""订阅事件"""
|
||||
if event_type not in self._handlers:
|
||||
self._handlers[event_type] = []
|
||||
self._handlers[event_type].append(handler)
|
||||
|
||||
def off(self, event_type: str, handler: Callable):
|
||||
"""取消订阅"""
|
||||
if event_type in self._handlers:
|
||||
try:
|
||||
self._handlers[event_type].remove(handler)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def emit(self, event: HttpEvent):
|
||||
"""发布事件"""
|
||||
handlers = self._handlers.get(event.type, [])
|
||||
for handler in handlers:
|
||||
try:
|
||||
handler(event)
|
||||
except Exception as e:
|
||||
import traceback; print(f"[events.py] 错误:{type(e).__name__}:{e}"); traceback.print_exc()
|
||||
pass
|
||||
|
||||
def clear(self):
|
||||
"""清空所有订阅"""
|
||||
self._handlers.clear()
|
||||
|
||||
|
||||
# 事件类型常量
|
||||
EVENT_REQUEST = "http.request"
|
||||
EVENT_BEFORE_ROUTE = "http.before_route"
|
||||
EVENT_AFTER_ROUTE = "http.after_route"
|
||||
EVENT_BEFORE_HANDLER = "http.before_handler"
|
||||
EVENT_AFTER_HANDLER = "http.after_handler"
|
||||
EVENT_RESPONSE = "http.response"
|
||||
EVENT_ERROR = "http.error"
|
||||
EVENT_COMPLETE = "http.complete"
|
||||
68
store/@{NebulaShell}/http-api/main.py
Normal file
68
store/@{NebulaShell}/http-api/main.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""HTTP API 插件 - 分散式布局"""
|
||||
import json
|
||||
from oss.plugin.types import Plugin, register_plugin_type
|
||||
from .server import HttpServer, Response
|
||||
from .router import Router
|
||||
from .middleware import MiddlewareChain
|
||||
|
||||
|
||||
class HttpApiPlugin(Plugin):
|
||||
"""HTTP API 插件"""
|
||||
|
||||
def __init__(self):
|
||||
self.server = None
|
||||
self.router = Router()
|
||||
self.middleware = MiddlewareChain()
|
||||
|
||||
def init(self, deps: dict = None):
|
||||
"""初始化"""
|
||||
# 注册基础路由
|
||||
self.router.get("/health", self._health_handler)
|
||||
self.router.get("/api/server/info", self._server_info_handler)
|
||||
self.router.get("/api/status", self._status_handler)
|
||||
|
||||
self.server = HttpServer(self.router, self.middleware)
|
||||
|
||||
def start(self):
|
||||
"""启动"""
|
||||
self.server.start()
|
||||
|
||||
def stop(self):
|
||||
"""停止"""
|
||||
if self.server:
|
||||
self.server.stop()
|
||||
|
||||
def _health_handler(self, request):
|
||||
"""健康检查"""
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps({"status": "ok", "service": "http-api"}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
def _server_info_handler(self, request):
|
||||
"""服务器信息"""
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps({
|
||||
"name": "NebulaShell HTTP API",
|
||||
"version": "1.0.0",
|
||||
"endpoints": ["/health", "/api/server/info", "/api/status"]
|
||||
}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
def _status_handler(self, request):
|
||||
"""状态检查"""
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps({"status": "running", "plugins_loaded": True}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
|
||||
register_plugin_type("HttpApiPlugin", HttpApiPlugin)
|
||||
|
||||
|
||||
def New():
|
||||
return HttpApiPlugin()
|
||||
25
store/@{NebulaShell}/http-api/manifest.json
Normal file
25
store/@{NebulaShell}/http-api/manifest.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "http-api",
|
||||
"version": "1.1.0",
|
||||
"author": "NebulaShell",
|
||||
"description": "HTTP API 服务 - 提供 RESTful API/路由功能/多语言支持/安全中间件",
|
||||
"type": "protocol"
|
||||
},
|
||||
"config": {
|
||||
"enabled": true,
|
||||
"args": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 8080,
|
||||
"ssl_enabled": false,
|
||||
"ssl_cert": "",
|
||||
"ssl_key": "",
|
||||
"cors_enabled": true,
|
||||
"rate_limit_enabled": true,
|
||||
"max_body_size": 10485760,
|
||||
"timeout": 30
|
||||
}
|
||||
},
|
||||
"dependencies": ["i18n"],
|
||||
"permissions": ["lifecycle", "circuit-breaker"]
|
||||
}
|
||||
60
store/@{NebulaShell}/http-api/middleware.py
Normal file
60
store/@{NebulaShell}/http-api/middleware.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""中间件链 - CORS/日志/限流等"""
|
||||
from typing import Callable, Optional, Any
|
||||
from .server import Request, Response
|
||||
|
||||
|
||||
class Middleware:
|
||||
"""中间件基类"""
|
||||
def process(self, ctx: dict[str, Any], next_fn: Callable) -> Optional[Response]:
|
||||
"""处理请求"""
|
||||
return None
|
||||
|
||||
|
||||
class CorsMiddleware(Middleware):
|
||||
"""CORS 中间件"""
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
ctx["response_headers"] = {
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
class LoggerMiddleware(Middleware):
|
||||
"""日志中间件"""
|
||||
# 静默的路由(不打印日志)
|
||||
_silent_paths = {"/api/dashboard/stats", "/favicon.ico", "/health"}
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
req = ctx.get("request")
|
||||
if req and req.path not in self._silent_paths:
|
||||
print(f"[http-api] {req.method} {req.path}")
|
||||
return None
|
||||
|
||||
|
||||
class MiddlewareChain:
|
||||
"""中间件链"""
|
||||
|
||||
def __init__(self):
|
||||
self.middlewares: list[Middleware] = []
|
||||
self.add(LoggerMiddleware())
|
||||
self.add(CorsMiddleware())
|
||||
|
||||
def add(self, middleware: Middleware):
|
||||
"""添加中间件"""
|
||||
self.middlewares.append(middleware)
|
||||
|
||||
def run(self, ctx: dict[str, Any]) -> Optional[Response]:
|
||||
"""执行中间件链"""
|
||||
idx = 0
|
||||
|
||||
def next_fn():
|
||||
nonlocal idx
|
||||
if idx < len(self.middlewares):
|
||||
mw = self.middlewares[idx]
|
||||
idx += 1
|
||||
return mw.process(ctx, next_fn)
|
||||
return None
|
||||
|
||||
return next_fn()
|
||||
18
store/@{NebulaShell}/http-api/router.py
Normal file
18
store/@{NebulaShell}/http-api/router.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""路由器 - 路径匹配和处理器分发"""
|
||||
from typing import Callable, Optional
|
||||
from oss.shared.router import BaseRouter, match_path
|
||||
from .server import Request, Response
|
||||
|
||||
|
||||
class Router(BaseRouter):
|
||||
"""HTTP API 路由器"""
|
||||
|
||||
def handle(self, request: Request) -> Response:
|
||||
"""处理请求"""
|
||||
result = self.find_route(request.method, request.path)
|
||||
if result:
|
||||
route, params = result
|
||||
# 将路径参数注入到请求中
|
||||
request.path_params = params
|
||||
return route.handler(request)
|
||||
return Response(status=404, body='{"error": "Not Found"}')
|
||||
115
store/@{NebulaShell}/http-api/server.py
Normal file
115
store/@{NebulaShell}/http-api/server.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""HTTP 服务器核心"""
|
||||
import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from typing import Any
|
||||
from oss.config import get_config
|
||||
|
||||
|
||||
class Request:
|
||||
"""请求对象"""
|
||||
def __init__(self, method, path, headers, body):
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
|
||||
|
||||
class Response:
|
||||
"""响应对象"""
|
||||
def __init__(self, status=200, headers=None, body=""):
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self.body = body
|
||||
|
||||
|
||||
class HttpServer:
|
||||
"""HTTP 服务器"""
|
||||
|
||||
def __init__(self, router, middleware, host=None, port=None):
|
||||
config = get_config()
|
||||
self.host = host or config.get("HOST", "0.0.0.0")
|
||||
self.port = port or config.get("HTTP_API_PORT", 8080)
|
||||
self.router = router
|
||||
self.middleware = middleware
|
||||
self._server = None
|
||||
self._thread = None
|
||||
|
||||
def start(self):
|
||||
"""启动服务器"""
|
||||
handler = self._create_handler()
|
||||
self._server = HTTPServer((self.host, self.port), handler)
|
||||
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
||||
self._thread.start()
|
||||
print(f"[http-api] 服务器启动: {self.host}:{self.port}")
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
if self._server:
|
||||
self._server.shutdown()
|
||||
print("[http-api] 服务器已停止")
|
||||
|
||||
def _create_handler(self):
|
||||
"""创建请求处理器"""
|
||||
router = self.router
|
||||
middleware = self.middleware
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self._handle("GET")
|
||||
|
||||
def do_POST(self):
|
||||
self._handle("POST")
|
||||
|
||||
def do_PUT(self):
|
||||
self._handle("PUT")
|
||||
|
||||
def do_DELETE(self):
|
||||
self._handle("DELETE")
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""处理 CORS 预检请求"""
|
||||
self.send_response(200)
|
||||
self.send_header("Access-Control-Allow-Origin", "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||
self.end_headers()
|
||||
|
||||
def _handle(self, method):
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_length) if content_length else b""
|
||||
|
||||
req = Request(
|
||||
method=method,
|
||||
path=self.path,
|
||||
headers=dict(self.headers),
|
||||
body=body.decode("utf-8")
|
||||
)
|
||||
|
||||
# 执行中间件
|
||||
ctx = {"request": req, "response": None}
|
||||
result = middleware.run(ctx)
|
||||
if result:
|
||||
self._send_response(result)
|
||||
return
|
||||
|
||||
# 路由匹配
|
||||
resp = router.handle(req)
|
||||
self._send_response(resp)
|
||||
|
||||
def _send_response(self, resp: Response):
|
||||
try:
|
||||
self.send_response(resp.status)
|
||||
for k, v in resp.headers.items():
|
||||
self.send_header(k, v)
|
||||
self.end_headers()
|
||||
if isinstance(resp.body, str):
|
||||
self.wfile.write(resp.body.encode("utf-8"))
|
||||
else:
|
||||
self.wfile.write(resp.body)
|
||||
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
||||
pass # 忽略客户端断开
|
||||
|
||||
def log_message(self, format, *args):
|
||||
pass
|
||||
|
||||
return Handler
|
||||
Reference in New Issue
Block a user