Merge branch 'dev'
This commit is contained in:
@@ -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,'>').replace(/"/g,'"'); }
|
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"'); }
|
||||||
|
|
||||||
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");
|
||||||
|
|||||||
@@ -193,7 +193,22 @@ class InternetService:
|
|||||||
|
|
||||||
logger.debug("默认路由设置完成 (已加认证)")
|
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"],
|
handler: Callable, methods: List[str] = ["GET"],
|
||||||
require_auth: bool = True):
|
require_auth: bool = True):
|
||||||
"""为插件注册HTTP路由 - 修复冻结路由器问题"""
|
"""为插件注册HTTP路由 - 修复冻结路由器问题"""
|
||||||
@@ -202,7 +217,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 self._read_panel_path()
|
||||||
|
full_path = f"{base}/{plugin_name}{route_path}"
|
||||||
|
|
||||||
# 创建包装器处理权限验证
|
# 创建包装器处理权限验证
|
||||||
async def wrapped_handler(request):
|
async def wrapped_handler(request):
|
||||||
@@ -267,7 +283,8 @@ class InternetService:
|
|||||||
if not ws_path.startswith('/'):
|
if not ws_path.startswith('/'):
|
||||||
ws_path = '/' + ws_path
|
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):
|
async def websocket_handler(request):
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
# 注册日志广播
|
# 注册日志广播
|
||||||
|
|||||||
@@ -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/* (已加认证)")
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -44,7 +44,13 @@ 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 +173,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();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user