Phase 3: PluginWebMixin SDK + web plugin control panels

- New: sdk/plugin_web.py (PluginWebMixin: web pages, API routes, SSE)
- New: services/web_panel/routes/plugin_web.py (3 endpoints)
- New: plugins/example_plugin/dashboard.html (counter + SSE demo)
- Updated: example_plugin uses PluginWebMixin
- Tests: 28/28 passing
This commit is contained in:
2026-06-10 20:54:11 +08:00
parent c680fea8e0
commit e52f7f3566
7 changed files with 155 additions and 12 deletions
+50
View File
@@ -0,0 +1,50 @@
"""PluginWebMixin — plugin web panel SDK"""
import logging, json, asyncio
from aiohttp import web
from typing import Dict, Callable
logger = logging.getLogger(__name__)
class PluginWebMixin:
def __init__(self):
self._web_pages: Dict[str, dict] = {}
self._api_routes: list = []
self._sse_clients: list = []
def register_web_page(self, path: str, title: str, html_content: str, icon: str = "P"):
self._web_pages[path] = {"title": title, "icon": icon, "html": html_content}
logger.info(f"Web page registered: {path}")
def register_api_route(self, method: str, path: str, handler: Callable):
self._api_routes.append((method, path, handler))
def get_web_pages(self) -> dict:
return self._web_pages
def get_api_routes(self) -> list:
return self._api_routes
async def push_sse_event(self, event_type: str, data: dict):
payload = json.dumps(data)
dead = []
for client in self._sse_clients:
try:
await client.send(f"event: {event_type}\\ndata: {payload}\\n\\n")
except:
dead.append(client)
for d in dead:
self._sse_clients.remove(d)
async def handle_sse(self, request):
resp = web.StreamResponse()
resp.headers["Content-Type"] = "text/event-stream"
resp.headers["Cache-Control"] = "no-cache"
await resp.prepare(request)
self._sse_clients.append(resp)
try:
while True:
await asyncio.sleep(30)
await resp.write(b": keepalive\\n\\n")
except:
self._sse_clients.remove(resp)
return resp