From 6c6c88b22a562d47db728569a96595efa0cb386e Mon Sep 17 00:00:00 2001 From: qinglong Date: Sun, 14 Jun 2026 20:14:22 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E6=8F=92=E4=BB=B6API=E8=B7=AF?= =?UTF-8?q?=E7=94=B1=E7=BB=9F=E4=B8=80=E5=8A=A0=E9=9D=A2=E6=9D=BF=E5=89=8D?= =?UTF-8?q?=E7=BC=80(/SenSu)=EF=BC=8C=E8=A7=A3=E5=86=B3=E5=8F=8D=E4=BB=A3?= =?UTF-8?q?=E5=90=8E=E6=8F=92=E4=BB=B6=E9=A1=B5=E9=9D=A2=E7=A9=BA=E7=99=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- plugins/sentinel/dashboard.html | 10 +++++----- services/internet_service.py | 3 ++- services/web_panel/manager.py | 4 +++- services/web_panel/routes/plugin_web.py | 12 +++++++----- static/web_panel/js/app.js | 11 +++++++++-- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/plugins/sentinel/dashboard.html b/plugins/sentinel/dashboard.html index 1b67bf4..0d6ea27 100644 --- a/plugins/sentinel/dashboard.html +++ b/plugins/sentinel/dashboard.html @@ -527,7 +527,7 @@ function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-re function esc(s) { return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function pollNodes() { - fetch("/sentinel/api/nodes").then(function(r){return r.json()}).then(function(d){ + fetch((window.SENSU_BASE||"")+"/sentinel/api/nodes").then(function(r){return r.json()}).then(function(d){ renderNodes(d.nodes || []); }).catch(function(e){ var grid = document.getElementById("sentinel-grid"); @@ -579,7 +579,7 @@ function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-re notes: document.getElementById("edit-notes").value.trim() }; if(!data.name||!data.url){ alert("名称和 URL 不能为空"); return; } - var resp = await fetch("/sentinel/api/nodes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(data)}); + var resp = await fetch((window.SENSU_BASE||"")+"/sentinel/api/nodes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(data)}); var r = await resp.json(); if(r.ok){ hideEdit(); loadNodes(); } else alert("保存失败: "+(r.error||"")); @@ -587,7 +587,7 @@ function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-re async function deleteNode(id,name){ if(!confirm("确认删除节点 '"+name+"'?")) return; - await fetch("/sentinel/api/nodes/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:id})}); + await fetch((window.SENSU_BASE||"")+"/sentinel/api/nodes/delete",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:id})}); loadNodes(); } @@ -597,7 +597,7 @@ function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-re var el = document.getElementById("test-result"); el.innerHTML = '⏳ 测试中...'; try { - var resp = await fetch("/sentinel/api/nodes/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:url,api_key:key})}); + var resp = await fetch((window.SENSU_BASE||"")+"/sentinel/api/nodes/test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url:url,api_key:key})}); var r = await resp.json(); if(r.ok) el.innerHTML = '✅ 连接成功 — CPU:'+(r.system.cpu.percent||0)+'% MEM:'+(r.system.memory.percent||0)+'%'; else el.innerHTML = '❌ '+esc(r.error||'失败')+''; @@ -605,7 +605,7 @@ function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-re } async function loadNodes(){ - var resp = await fetch("/sentinel/api/nodes"); + var resp = await fetch((window.SENSU_BASE||"")+"/sentinel/api/nodes"); var data = await resp.json(); var nodes = data.nodes||[]; var list = document.getElementById("node-list"); diff --git a/services/internet_service.py b/services/internet_service.py index c41acec..91434bd 100644 --- a/services/internet_service.py +++ b/services/internet_service.py @@ -202,7 +202,8 @@ class InternetService: if not route_path.startswith('/'): route_path = '/' + route_path - full_path = f"/{plugin_name}{route_path}" + base = self.http_app.get('panel_base_path', '') or '' + full_path = f"{base}/{plugin_name}{route_path}" # 创建包装器处理权限验证 async def wrapped_handler(request): diff --git a/services/web_panel/manager.py b/services/web_panel/manager.py index 540fce5..156a320 100644 --- a/services/web_panel/manager.py +++ b/services/web_panel/manager.py @@ -85,7 +85,9 @@ class WebPanelManager: logs.setup_routes(app, self.base_path) projects.setup_project_routes(app, self.sm, self.base_path) proxy.setup_proxy_routes(app, self.sm, self.base_path) - plugin_web.setup_plugin_web_routes(app, self.sm) + # 存储面板前缀供插件路由使用 + app['panel_base_path'] = self.base_path + plugin_web.setup_plugin_web_routes(app, self.sm, self.base_path) files.setup_file_routes(app, self.sm, self.base_path) # 注册日志广播 diff --git a/services/web_panel/routes/plugin_web.py b/services/web_panel/routes/plugin_web.py index 119f405..379911e 100644 --- a/services/web_panel/routes/plugin_web.py +++ b/services/web_panel/routes/plugin_web.py @@ -4,7 +4,9 @@ from ..utils.auth import panel_auth logger = logging.getLogger(__name__) -def setup_plugin_web_routes(app, service_manager): +def setup_plugin_web_routes(app, service_manager, base_path=""): + prefix = base_path.rstrip("/") + async def plugin_page(request): name = request.match_info.get("name","") ps = service_manager.get_service("plugin") @@ -36,7 +38,7 @@ def setup_plugin_web_routes(app, service_manager): return web.json_response({"ok": True}) return web.json_response({"ok": False}, status=404) - app.router.add_get("/plugin/{name}", panel_auth(plugin_page)) - app.router.add_get("/plugin/{name}/sse", panel_auth(plugin_sse)) - app.router.add_post("/plugin/{name}/event", panel_auth(plugin_event)) - logger.info("Plugin web routes registered (已加认证)") + app.router.add_get(f"{prefix}/plugin/{{name}}", panel_auth(plugin_page)) + app.router.add_get(f"{prefix}/plugin/{{name}}/sse", panel_auth(plugin_sse)) + app.router.add_post(f"{prefix}/plugin/{{name}}/event", panel_auth(plugin_event)) + logger.info(f"Plugin web routes registered at {prefix}/plugin/* (已加认证)") diff --git a/static/web_panel/js/app.js b/static/web_panel/js/app.js index 08594cd..deccb87 100644 --- a/static/web_panel/js/app.js +++ b/static/web_panel/js/app.js @@ -44,7 +44,14 @@ window.toggleTheme = function(){ // ═══ 路由系统 ═══ // Hash format: #/dashboard | #/files | #/plugins/example_plugin // Built-in pages map to ./static/pages/{name}.html -// Plugin pages load from /plugin/{name} +// Plugin pages load from {base}/plugin/{name} + +// 从当前页面路径提取面板前缀 (e.g. /SenSu/home.html → /SenSu) +window.SENSU_BASE = (function(){ + var p = window.location.pathname; + // 去掉最后的文件名部分 + return p.substring(0, p.lastIndexOf('/')) || ''; +})(); var BUILTIN_PAGES = ['dashboard','logs','console','plugins','projects','proxy','files','settings']; var _pluginPagesCache = null; // {plugin_name: {path, title, icon}} @@ -167,7 +174,7 @@ async function loadPluginPage(pluginName) { if(!_pluginPagesCache) await fetchPluginPages(); try { - var resp = await fetch('/plugin/'+pluginName+'?t='+Date.now(), {credentials:'include'}); + var resp = await fetch(window.SENSU_BASE+'/plugin/'+pluginName+'?t='+Date.now(), {credentials:'include'}); if(!resp.ok) throw new Error(resp.status); var html = await resp.text(); From 10630f3b8fea65915557fe595de9e406f8b94af5 Mon Sep 17 00:00:00 2001 From: qinglong Date: Sun, 14 Jun 2026 20:15:27 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E6=8F=92=E4=BB=B6=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E6=B3=A8=E5=86=8C=E6=97=B6=E4=BB=8E=E9=85=8D=E7=BD=AE=E6=96=87?= =?UTF-8?q?=E4=BB=B6fallback=E8=AF=BB=E5=8F=96=E9=9D=A2=E6=9D=BF=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=EF=BC=8C=E8=A7=A3=E5=86=B3=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E9=A1=BA=E5=BA=8F=E5=AF=BC=E8=87=B4=E7=9A=84=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E7=BC=BA=E5=A4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- services/internet_service.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/services/internet_service.py b/services/internet_service.py index 91434bd..e251a64 100644 --- a/services/internet_service.py +++ b/services/internet_service.py @@ -193,7 +193,22 @@ class InternetService: logger.debug("默认路由设置完成 (已加认证)") - async def register_plugin_route(self, plugin_name: str, route_path: str, + def _read_panel_path(self) -> str: + """从 base_config.yaml 读取面板路径 (插件早于面板初始化时的 fallback)""" + try: + import yaml + from pathlib import Path + cfg_path = Path(__file__).resolve().parent.parent / "config" / "framework" / "base_config.yaml" + if cfg_path.exists(): + with open(cfg_path) as f: + cfg = yaml.safe_load(f) + p = cfg.get("panel", {}).get("entrance", {}).get("path", "/panel") + return f"/{p.strip('/')}" + except Exception: + pass + return "" + + async def register_plugin_route(self, plugin_name: str, route_path: str, handler: Callable, methods: List[str] = ["GET"], require_auth: bool = True): """为插件注册HTTP路由 - 修复冻结路由器问题""" @@ -202,7 +217,7 @@ class InternetService: if not route_path.startswith('/'): route_path = '/' + route_path - base = self.http_app.get('panel_base_path', '') or '' + base = self.http_app.get('panel_base_path', '') or self._read_panel_path() full_path = f"{base}/{plugin_name}{route_path}" # 创建包装器处理权限验证 @@ -268,7 +283,8 @@ class InternetService: if not ws_path.startswith('/'): ws_path = '/' + ws_path - full_path = f"/plugin/{plugin_name}/ws{ws_path}" + base = self.http_app.get('panel_base_path', '') or self._read_panel_path() + full_path = f"{base}/plugin/{plugin_name}/ws{ws_path}" async def websocket_handler(request): try: From 8ca6da67ab29aa29e84eee23148e3961f55a5ece Mon Sep 17 00:00:00 2001 From: qinglong Date: Sun, 14 Jun 2026 20:21:08 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20app=5Fv3.js=E5=90=8C=E6=AD=A5app.js?= =?UTF-8?q?=E7=9A=84SENSU=5FBASE=E5=89=8D=E7=BC=80=E6=94=B9=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- static/web_panel/js/app_v3.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/static/web_panel/js/app_v3.js b/static/web_panel/js/app_v3.js index 08594cd..a885700 100644 --- a/static/web_panel/js/app_v3.js +++ b/static/web_panel/js/app_v3.js @@ -44,7 +44,13 @@ window.toggleTheme = function(){ // ═══ 路由系统 ═══ // Hash format: #/dashboard | #/files | #/plugins/example_plugin // Built-in pages map to ./static/pages/{name}.html -// Plugin pages load from /plugin/{name} +// Plugin pages load from {base}/plugin/{name} + +// 从当前页面路径提取面板前缀 (e.g. /SenSu/home.html → /SenSu) +window.SENSU_BASE = (function(){ + var p = window.location.pathname; + return p.substring(0, p.lastIndexOf('/')) || ''; +})(); var BUILTIN_PAGES = ['dashboard','logs','console','plugins','projects','proxy','files','settings']; var _pluginPagesCache = null; // {plugin_name: {path, title, icon}} @@ -167,7 +173,7 @@ async function loadPluginPage(pluginName) { if(!_pluginPagesCache) await fetchPluginPages(); try { - var resp = await fetch('/plugin/'+pluginName+'?t='+Date.now(), {credentials:'include'}); + var resp = await fetch(window.SENSU_BASE+'/plugin/'+pluginName+'?t='+Date.now(), {credentials:'include'}); if(!resp.ok) throw new Error(resp.status); var html = await resp.text();