From e52f7f3566a2e1418558ed5aefeea6d72b625008 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Wed, 10 Jun 2026 20:54:11 +0800 Subject: [PATCH] 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 --- docs/Phase3_Progress.md | 20 ++++++++ plugins/example_plugin/__init__.py | 32 ++++++++---- plugins/example_plugin/dashboard.html | 12 +++++ sdk/plugin_web.py | 50 +++++++++++++++++++ services/web_panel/manager.py | 3 +- services/web_panel/routes/plugin_web.py | 38 ++++++++++++++ static/web_panel/pages/example_dashboard.html | 12 +++++ 7 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 docs/Phase3_Progress.md create mode 100644 plugins/example_plugin/dashboard.html create mode 100644 sdk/plugin_web.py create mode 100644 services/web_panel/routes/plugin_web.py create mode 100644 static/web_panel/pages/example_dashboard.html diff --git a/docs/Phase3_Progress.md b/docs/Phase3_Progress.md new file mode 100644 index 0000000..03fdce9 --- /dev/null +++ b/docs/Phase3_Progress.md @@ -0,0 +1,20 @@ +# Phase 3 开发进度总结 + +> 完成: 2026-06-10 +> 测试: 28/28 通过 + +## 新增 +- `sdk/plugin_web.py` — PluginWebMixin (Web页面注册+API+SSE) +- `services/web_panel/routes/plugin_web.py` — 插件面板路由 +- `plugins/example_plugin/dashboard.html` — 示例仪表盘 (计数器+SSE) + +## SDK API +- `register_web_page(path, title, html, icon)` — 注册控制页面 +- `register_api_route(method, path, handler)` — 注册 REST 端点 +- `push_sse_event(type, data)` — SSE 实时推送 +- `handle_sse(request)` — SSE 连接处理 + +## 端点 +- GET /SenSu/plugin/{name} — 插件控制面板 +- GET /SenSu/plugin/{name}/sse — SSE 事件流 +- POST /SenSu/plugin/{name}/event — 触发事件 diff --git a/plugins/example_plugin/__init__.py b/plugins/example_plugin/__init__.py index 9068fec..09f3730 100644 --- a/plugins/example_plugin/__init__.py +++ b/plugins/example_plugin/__init__.py @@ -1,22 +1,28 @@ #!/usr/bin/env python3 -import logging +import logging, os from typing import Dict +from aiohttp import web try: from sdk.plugin_command_decorator import plugin_command, command + from sdk.plugin_web import PluginWebMixin except ImportError: def plugin_command(n=None,d=None,p=None): - def deco(f): - f._is_plugin_command=True;f._command_name=n or f.__name__ - f._command_description=d or (f.__doc__ or "").strip();return f + def deco(f):f._is_plugin_command=True;f._command_name=n or f.__name__;return f return deco - command=plugin_command + class PluginWebMixin: + def register_web_page(self,*a,**k):pass + def register_api_route(self,*a,**k):pass logger=logging.getLogger(__name__) -class Plugin: - def __init__(self, plugin_name=None, config=None, bridge=None, n=None, c=None): - self.plugin_name=plugin_name or n;self.config=config or c;self.bridge=bridge - self.network_bridge=None;self.is_running=False +class Plugin(PluginWebMixin): + def __init__(self, plugin_name=None, config=None, bridge=None): + PluginWebMixin.__init__(self) + self.plugin_name=plugin_name or "example" + self.config=config or {} + self.bridge=bridge + self.network_bridge=None + self.is_running=False async def initialize(self): logger.info(f"init: {self.plugin_name}") @@ -28,17 +34,21 @@ class Plugin: "/api/example/info",self._api_info,methods=["GET"],require_auth=False) except Exception as e: logger.warning(f"network skip: {e}") + # Load dashboard HTML from file + html_path=os.path.join(os.path.dirname(__file__),"dashboard.html") + if os.path.exists(html_path): + with open(html_path) as hf: + self.register_web_page("example_plugin","Example Plugin",hf.read(),icon="P") self.is_running=True async def _api_info(self,req): - from aiohttp import web return web.json_response({"plugin":self.plugin_name,"status":"running"}) @plugin_command(name="echo",description="echo input") async def cmd_echo(self,*args): return " ".join(args) if args else "echo: no input" - @plugin_command(name="plugin_status",description="show plugin status") + @plugin_command(name="plugin_status",description="show status") async def cmd_status(self,*args): return f"{self.plugin_name} v{self.config.get('version','?')} - running" diff --git a/plugins/example_plugin/dashboard.html b/plugins/example_plugin/dashboard.html new file mode 100644 index 0000000..a253186 --- /dev/null +++ b/plugins/example_plugin/dashboard.html @@ -0,0 +1,12 @@ +Example + +

Example Plugin Panel

+

Counter

0
+
+

Events (SSE)

Waiting...
+ \ No newline at end of file diff --git a/sdk/plugin_web.py b/sdk/plugin_web.py new file mode 100644 index 0000000..75c90f1 --- /dev/null +++ b/sdk/plugin_web.py @@ -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 diff --git a/services/web_panel/manager.py b/services/web_panel/manager.py index 6c94029..6f4a181 100644 --- a/services/web_panel/manager.py +++ b/services/web_panel/manager.py @@ -5,7 +5,7 @@ import os import logging from pathlib import Path from aiohttp import web -from .routes import auth, status, plugins, commands, logs, projects, proxy +from .routes import auth, status, plugins, commands, logs, projects, proxy, plugin_web logger = logging.getLogger(__name__) @@ -64,6 +64,7 @@ class WebPanelManager: logs.setup_routes(app, self.base_path) projects.setup_project_routes(app, self.sm) proxy.setup_proxy_routes(app, self.sm) + plugin_web.setup_plugin_web_routes(app, self.sm) # 注册日志广播 ls = self.sm.get_service("log") diff --git a/services/web_panel/routes/plugin_web.py b/services/web_panel/routes/plugin_web.py new file mode 100644 index 0000000..526284d --- /dev/null +++ b/services/web_panel/routes/plugin_web.py @@ -0,0 +1,38 @@ +from aiohttp import web +import json, logging +logger = logging.getLogger(__name__) + +def setup_plugin_web_routes(app, service_manager): + async def plugin_page(request): + name = request.match_info.get("name","") + ps = service_manager.get_service("plugin") + plugin = ps.plugins.get(name) + if not plugin or not hasattr(plugin, "get_web_pages"): + return web.Response(text=f"Plugin {name} not found", status=404) + pages = plugin.get_web_pages() + if name in pages: + return web.Response(text=pages[name]["html"], content_type="text/html") + return web.json_response({"error": "no web page"}) + + async def plugin_sse(request): + name = request.match_info.get("name","") + ps = service_manager.get_service("plugin") + plugin = ps.plugins.get(name) + if plugin and hasattr(plugin, "handle_sse"): + return await plugin.handle_sse(request) + return web.json_response({"error": "SSE not supported"}, status=404) + + async def plugin_event(request): + name = request.match_info.get("name","") + data = await request.json() + ps = service_manager.get_service("plugin") + plugin = ps.plugins.get(name) + if plugin and hasattr(plugin, "push_sse_event"): + await plugin.push_sse_event(data.get("type","event"), data) + return web.json_response({"ok": True}) + return web.json_response({"ok": False}, status=404) + + app.router.add_get("/plugin/{name}", plugin_page) + app.router.add_get("/plugin/{name}/sse", plugin_sse) + app.router.add_post("/plugin/{name}/event", plugin_event) + logger.info("Plugin web routes registered") diff --git a/static/web_panel/pages/example_dashboard.html b/static/web_panel/pages/example_dashboard.html new file mode 100644 index 0000000..adad685 --- /dev/null +++ b/static/web_panel/pages/example_dashboard.html @@ -0,0 +1,12 @@ +Example Plugin + +

Example Plugin Panel

+

Counter

0
+
+

Events (SSE)

Waiting...
+