fix: 插件API路由统一加面板前缀(/SenSu),解决反代后插件页面空白

This commit is contained in:
qinglong
2026-06-14 20:14:22 +08:00
parent 26d515e080
commit 6c6c88b22a
5 changed files with 26 additions and 14 deletions
+5 -5
View File
@@ -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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); } function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
function pollNodes() { 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 || []); renderNodes(d.nodes || []);
}).catch(function(e){ }).catch(function(e){
var grid = document.getElementById("sentinel-grid"); 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() notes: document.getElementById("edit-notes").value.trim()
}; };
if(!data.name||!data.url){ alert("名称和 URL 不能为空"); return; } 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(); var r = await resp.json();
if(r.ok){ hideEdit(); loadNodes(); } if(r.ok){ hideEdit(); loadNodes(); }
else alert("保存失败: "+(r.error||"")); 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){ async function deleteNode(id,name){
if(!confirm("确认删除节点 '"+name+"'?")) return; 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(); 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"); var el = document.getElementById("test-result");
el.innerHTML = '<span style="color:var(--text-dim)">⏳ 测试中...</span>'; el.innerHTML = '<span style="color:var(--text-dim)">⏳ 测试中...</span>';
try { 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(); var r = await resp.json();
if(r.ok) el.innerHTML = '<span style="color:var(--success)">✅ 连接成功 — CPU:'+(r.system.cpu.percent||0)+'% MEM:'+(r.system.memory.percent||0)+'%</span>'; if(r.ok) el.innerHTML = '<span style="color:var(--success)">✅ 连接成功 — CPU:'+(r.system.cpu.percent||0)+'% MEM:'+(r.system.memory.percent||0)+'%</span>';
else el.innerHTML = '<span style="color:var(--error)">❌ '+esc(r.error||'失败')+'</span>'; else el.innerHTML = '<span style="color:var(--error)">❌ '+esc(r.error||'失败')+'</span>';
@@ -605,7 +605,7 @@ function bar(v) { return v < 60 ? 'bar-green' : (v < 85 ? 'bar-yellow' : 'bar-re
} }
async function loadNodes(){ 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 data = await resp.json();
var nodes = data.nodes||[]; var nodes = data.nodes||[];
var list = document.getElementById("node-list"); var list = document.getElementById("node-list");
+2 -1
View File
@@ -202,7 +202,8 @@ class InternetService:
if not route_path.startswith('/'): if not route_path.startswith('/'):
route_path = '/' + route_path 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): async def wrapped_handler(request):
+3 -1
View File
@@ -85,7 +85,9 @@ class WebPanelManager:
logs.setup_routes(app, self.base_path) logs.setup_routes(app, self.base_path)
projects.setup_project_routes(app, self.sm, self.base_path) projects.setup_project_routes(app, self.sm, self.base_path)
proxy.setup_proxy_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) files.setup_file_routes(app, self.sm, self.base_path)
# 注册日志广播 # 注册日志广播
+7 -5
View File
@@ -4,7 +4,9 @@ from ..utils.auth import panel_auth
logger = logging.getLogger(__name__) 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): async def plugin_page(request):
name = request.match_info.get("name","") name = request.match_info.get("name","")
ps = service_manager.get_service("plugin") 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": True})
return web.json_response({"ok": False}, status=404) return web.json_response({"ok": False}, status=404)
app.router.add_get("/plugin/{name}", panel_auth(plugin_page)) app.router.add_get(f"{prefix}/plugin/{{name}}", panel_auth(plugin_page))
app.router.add_get("/plugin/{name}/sse", panel_auth(plugin_sse)) app.router.add_get(f"{prefix}/plugin/{{name}}/sse", panel_auth(plugin_sse))
app.router.add_post("/plugin/{name}/event", panel_auth(plugin_event)) app.router.add_post(f"{prefix}/plugin/{{name}}/event", panel_auth(plugin_event))
logger.info("Plugin web routes registered (已加认证)") logger.info(f"Plugin web routes registered at {prefix}/plugin/* (已加认证)")
+9 -2
View File
@@ -44,7 +44,14 @@ window.toggleTheme = function(){
// ═══ 路由系统 ═══ // ═══ 路由系统 ═══
// Hash format: #/dashboard | #/files | #/plugins/example_plugin // Hash format: #/dashboard | #/files | #/plugins/example_plugin
// Built-in pages map to ./static/pages/{name}.html // 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 BUILTIN_PAGES = ['dashboard','logs','console','plugins','projects','proxy','files','settings'];
var _pluginPagesCache = null; // {plugin_name: {path, title, icon}} var _pluginPagesCache = null; // {plugin_name: {path, title, icon}}
@@ -167,7 +174,7 @@ async function loadPluginPage(pluginName) {
if(!_pluginPagesCache) await fetchPluginPages(); if(!_pluginPagesCache) await fetchPluginPages();
try { 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); if(!resp.ok) throw new Error(resp.status);
var html = await resp.text(); var html = await resp.text();