Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2798100323 | |||
| 2a0f62ef1c | |||
| 8ca6da67ab | |||
| 10630f3b8f | |||
| 6c6c88b22a | |||
| 11e8bf2a71 | |||
| 26d515e080 | |||
| cd45372141 | |||
| d626a0631a | |||
| 846b8a979a | |||
| 2c5cd57ad4 | |||
| b9252bdc58 | |||
| 40a1502f8b | |||
| 6d30096005 | |||
| 28670204cf | |||
| 316986c420 | |||
| 6c36743b89 | |||
| 280cd3e994 | |||
| f40b87ea52 | |||
| 5f38c9e8eb | |||
| 8168d24fad | |||
| a82c94bd51 | |||
| 35e2368023 | |||
| b29b236a52 | |||
| 3f5b01f817 | |||
| 4d51a7620e | |||
| 103d192e8f | |||
| 90a563aef0 | |||
| 5a9d96dc1b |
+31
-2
@@ -1,7 +1,7 @@
|
||||
# SenSu 开发路线图
|
||||
|
||||
> 当前版本: Alpha 0.7.0
|
||||
> 更新: 2026-06-13
|
||||
> 当前版本: v0.8.0
|
||||
> 更新: 2026-06-14
|
||||
|
||||
---
|
||||
|
||||
@@ -46,4 +46,33 @@
|
||||
- [x] systemd 集成 — `deploy/sensu.service`
|
||||
- [x] Docker 化 — `deploy/Dockerfile` (Alpine, <100MB)
|
||||
|
||||
## 六、v0.6 — 安全 + 监控 ✅ 全部完成
|
||||
|
||||
- [x] API Key 系统 — sk-前缀, 权限模板(readonly/monitor/full), TTL过期, 用量追踪
|
||||
- [x] 全端点 CSRF 保护 — panel_auth 装饰器, X-CSRF-Token 检查
|
||||
- [x] 安全中间件 — X-Content-Type-Options/X-Frame-Options/XSS-Protection/Referrer-Policy
|
||||
- [x] SSRF 防护 — URL scheme 白名单, 错误脱敏
|
||||
- [x] Sentinel 集群监控插件 — 多节点实时仪表盘 + 节点管理 WebUI
|
||||
- [x] 版本更新检测 — version.json, Gitea ZIP 下载, 备份/回滚
|
||||
- [x] 插件导入安装 — zip 上传解压
|
||||
- [x] 配置文件自动创建 — config.yaml.example → config.yaml
|
||||
|
||||
## 七、v0.7 — Windows 兼容 ✅ 全部完成
|
||||
|
||||
- [x] Windows 兼容 — SO_REUSEPORT 条件化, /proc 回退, 路径分隔符
|
||||
- [x] 嵌入式 CPython 支持 — venv 懒加载, UTF-8 编码强制, sys.path 注入
|
||||
- [x] `start.bat` — Windows 一键启动, `--no-venv` 系统 Python 模式
|
||||
- [x] PYTHONPATH 自动注入 — 嵌入式 Python 模块搜索路径
|
||||
- [x] HiChart 高分辨率 Canvas 图表 — devicePixelRatio 缩放, CSS 尺寸对齐
|
||||
- [x] 宝塔面板部署适配 — D:\BtSoft\python\ 嵌入式 CPython 替换 PyPy
|
||||
|
||||
## 八、v0.8 — 网络 + 反代 ✅ 全部完成
|
||||
|
||||
- [x] WebSocket WSS 自动检测 — location.protocol → ws/wss
|
||||
- [x] 插件路由统一面板前缀 — /SenSu/plugin/{name}, /SenSu/{plugin}/api/*
|
||||
- [x] nginx 反代 WebSocket 支持 — proxy_http_version 1.1 + Upgrade/Connection 头
|
||||
- [x] HTTP 链路延迟显示 — 节点卡片 RTT 实时展示 (横版左下角/网格右上角)
|
||||
- [x] 多服务器部署 — test/t2/t3/cd 四节点集群
|
||||
- [x] 仪表盘版本号修复 — 从 version.json 读取, 不再硬编码
|
||||
|
||||
---
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
framework:
|
||||
debug: true
|
||||
name: SenSu
|
||||
version: v0.8.0
|
||||
branch: dev
|
||||
repo_url: https://git.yeij.top/AskaEth/SenSu
|
||||
logging:
|
||||
|
||||
@@ -4,9 +4,23 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import sys
|
||||
import os as _os
|
||||
import signal
|
||||
import time
|
||||
import argparse
|
||||
|
||||
# 确保项目根目录在 sys.path 中 (嵌入式 CPython / PyPy 需要)
|
||||
_sys_path_root = _os.path.dirname(_os.path.abspath(__file__))
|
||||
if _sys_path_root not in sys.path:
|
||||
sys.path.insert(0, _sys_path_root)
|
||||
|
||||
# Windows 控制台强制 UTF-8 (避免 GBK 编解码 emoji 报错)
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
||||
sys.stderr.reconfigure(encoding='utf-8', errors='replace')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
from services.project_engine import ProjectEngine
|
||||
from services.pyenv_manager import PyEnvManager
|
||||
from services.proxy_service import ProxyService
|
||||
|
||||
@@ -116,6 +116,7 @@ class Plugin(PluginWebMixin):
|
||||
"system": c.get("system", {}),
|
||||
"platform": c.get("platform", {}),
|
||||
"last_seen": c.get("last_seen", 0),
|
||||
"latency_ms": c.get("latency_ms", 0),
|
||||
"error": c.get("error", ""),
|
||||
})
|
||||
return web.json_response({"nodes": result})
|
||||
@@ -195,12 +196,13 @@ class Plugin(PluginWebMixin):
|
||||
for node in nodes:
|
||||
if not node.get("enabled", True):
|
||||
continue
|
||||
online, sys_info, err = await self._fetch_system(node)
|
||||
online, sys_info, err, latency = await self._fetch_system(node)
|
||||
_NODE_CACHE[node.get("id", "")] = {
|
||||
"online": online,
|
||||
"system": sys_info or {},
|
||||
"platform": (sys_info or {}).get("platform", {}),
|
||||
"last_seen": time.time(),
|
||||
"latency_ms": latency,
|
||||
"error": err if not online else "",
|
||||
}
|
||||
# 每次轮询都推送 SSE (确保实时性)
|
||||
@@ -217,14 +219,17 @@ class Plugin(PluginWebMixin):
|
||||
|
||||
def _sync_fetch():
|
||||
try:
|
||||
t0 = time.time()
|
||||
headers = {"Authorization": f"Bearer {api_key}"} if api_key else {}
|
||||
req = urllib.request.Request(f"{url}{base}/api/system", headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
elapsed = round((time.time() - t0) * 1000) # ms
|
||||
if resp.status == 200:
|
||||
return True, _json.loads(resp.read().decode()), ""
|
||||
return False, None, f"HTTP {resp.status}"
|
||||
return True, _json.loads(resp.read().decode()), "", elapsed
|
||||
return False, None, f"HTTP {resp.status}", elapsed
|
||||
except Exception as e:
|
||||
return False, None, str(e)[:120]
|
||||
elapsed = round((time.time() - t0) * 1000)
|
||||
return False, None, str(e)[:120], elapsed
|
||||
|
||||
return await asyncio.get_event_loop().run_in_executor(None, _sync_fetch)
|
||||
|
||||
@@ -242,6 +247,7 @@ class Plugin(PluginWebMixin):
|
||||
"system": c.get("system", {}),
|
||||
"platform": c.get("platform", {}),
|
||||
"last_seen": c.get("last_seen", 0),
|
||||
"latency_ms": c.get("latency_ms", 0),
|
||||
"error": c.get("error", ""),
|
||||
})
|
||||
return result
|
||||
|
||||
@@ -157,12 +157,12 @@
|
||||
window.addEventListener('resize', function(){ self._resize(); });
|
||||
}
|
||||
HiChart.prototype._resize = function() {
|
||||
var rect = this.canvas.parentElement.getBoundingClientRect();
|
||||
var w = Math.max(rect.width - 8, 100);
|
||||
var h = 60;
|
||||
// 用 canvas 自身的 CSS 渲染尺寸 → 内部分辨率 = CSS尺寸 × DPR (保持高清)
|
||||
var box = this.canvas.getBoundingClientRect();
|
||||
var w = box.width, h = box.height;
|
||||
if (w <= 0 || h <= 0) { w = 280; h = 60; } // fallback
|
||||
this.canvas.width = w * this._dpr;
|
||||
this.canvas.height = h * this._dpr;
|
||||
// CSS controls display size via width:100%;height:60px
|
||||
this.ctx.setTransform(this._dpr, 0, 0, this._dpr, 0, 0);
|
||||
this._draw();
|
||||
};
|
||||
@@ -294,12 +294,13 @@
|
||||
(sysInfo.trim() ? '<div class="node-sys">'+esc(sysInfo)+'</div>' : '')+
|
||||
'<div class="node-url">'+esc(n.url||'')+'</div>'+
|
||||
(n.notes ? '<div class="node-notes">📝 '+esc(n.notes)+'</div>' : '')+
|
||||
'<div class="node-latency" style="font-size:.7rem;color:var(--text-dim);margin-top:4px">⏱ —</div>'+
|
||||
'<div class="col-info" style="font-size:.65rem;color:var(--text-dim);margin-top:2px"></div>'+
|
||||
'</div>'+
|
||||
'<div class="col-right-stack">'+
|
||||
'<div class="mtr" id="mtr-'+id+'-cpu"><div class="mtr-data"><span class="rlabel">CPU</span> <span class="rval cpu-val">0%</span></div><canvas id="'+id+'-cpu" height="60"></canvas></div>'+
|
||||
'<div class="mtr" id="mtr-'+id+'-mem"><div class="mtr-data"><span class="rlabel">MEM</span> <span class="rval mem-val">0%</span> <span class="mem-row-detail" style="font-size:.65rem;color:var(--text-dim)"></span></div><canvas id="'+id+'-mem" height="60"></canvas></div>'+
|
||||
'<div class="mtr" id="mtr-'+id+'-net"><div class="mtr-data"><span class="rlabel">NET</span> <span class="rval net-down" style="font-size:.85rem">0</span> <span class="rval net-up" style="font-size:.85rem">0</span></div><canvas id="'+id+'-net" height="60"></canvas></div>'+
|
||||
'<div class="col-info" style="text-align:right;font-size:.7rem;color:var(--text-dim);margin-top:4px"></div>'+
|
||||
'</div>'+
|
||||
'<div class="col-disk"><div class="disk-list"></div></div>'+
|
||||
'<div class="col-extra">'+
|
||||
@@ -311,7 +312,10 @@
|
||||
'</div>';
|
||||
} else {
|
||||
card.innerHTML =
|
||||
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
|
||||
'<div style="display:flex;justify-content:space-between;align-items:flex-start">'+
|
||||
'<div class="node-name">'+(n.online?'🟢':'🔴')+' '+esc(n.name)+'</div>'+
|
||||
'<span class="latency-val" style="font-size:.7rem;color:var(--text-dim);flex-shrink:0">⏱ —</span>'+
|
||||
'</div>'+
|
||||
(sysInfo.trim() ? '<div class="node-sys">'+esc(sysInfo)+'</div>' : '')+
|
||||
'<div class="node-url">'+esc(n.url||'')+'</div>'+
|
||||
(n.notes ? '<div class="node-notes">📝 '+esc(n.notes)+'</div>' : '')+
|
||||
@@ -342,6 +346,17 @@
|
||||
sysEl.textContent = si2;
|
||||
} else if (sysEl) { sysEl.remove(); }
|
||||
|
||||
// 延迟 (横版: col-left底部 / 网格: 右上角)
|
||||
var latEl = card.querySelector('.node-latency') || card.querySelector('.latency-val');
|
||||
if (latEl && n.latency_ms > 0) {
|
||||
var ms = n.latency_ms;
|
||||
latEl.textContent = '⏱ ' + (ms < 1000 ? ms + 'ms' : (ms/1000).toFixed(1)+'s');
|
||||
latEl.style.color = ms < 100 ? 'var(--success)' : ms < 500 ? 'var(--warning,#e0af68)' : 'var(--error)';
|
||||
} else if (latEl && n.online === false) {
|
||||
latEl.textContent = '⏱ —';
|
||||
latEl.style.color = 'var(--text-dim)';
|
||||
}
|
||||
|
||||
var body = isRow ? card.querySelector('.col-right-stack') : card.querySelector('.metrics-body');
|
||||
if (!body) return;
|
||||
|
||||
@@ -512,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 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");
|
||||
@@ -564,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||""));
|
||||
@@ -572,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();
|
||||
}
|
||||
|
||||
@@ -582,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 = '<span style="color:var(--text-dim)">⏳ 测试中...</span>';
|
||||
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 = '<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>';
|
||||
@@ -590,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");
|
||||
|
||||
@@ -193,6 +193,21 @@ class InternetService:
|
||||
|
||||
logger.debug("默认路由设置完成 (已加认证)")
|
||||
|
||||
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):
|
||||
@@ -202,7 +217,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 self._read_panel_path()
|
||||
full_path = f"{base}/{plugin_name}{route_path}"
|
||||
|
||||
# 创建包装器处理权限验证
|
||||
async def wrapped_handler(request):
|
||||
@@ -267,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:
|
||||
|
||||
@@ -89,7 +89,8 @@ class LogService:
|
||||
runtime_handler = logging.handlers.RotatingFileHandler(
|
||||
self.log_dir / "runtime" / runtime_log_file,
|
||||
maxBytes=self._parse_size(self.config['logging'].get('max_file_size', '10MB')),
|
||||
backupCount=self.config['logging'].get('max_log_files', 3)
|
||||
backupCount=self.config['logging'].get('max_log_files', 3),
|
||||
encoding='utf-8'
|
||||
)
|
||||
runtime_handler.setLevel(getattr(logging, self.config['logging']['level'], logging.INFO))
|
||||
runtime_handler.setFormatter(file_formatter)
|
||||
@@ -101,7 +102,8 @@ class LogService:
|
||||
debug_handler = logging.handlers.RotatingFileHandler(
|
||||
self.log_dir / "debug" / debug_log_file,
|
||||
maxBytes=self._parse_size(self.config['logging'].get('max_file_size', '10MB')),
|
||||
backupCount=self.config['logging'].get('max_log_files', 3)
|
||||
backupCount=self.config['logging'].get('max_log_files', 3),
|
||||
encoding='utf-8'
|
||||
)
|
||||
debug_handler.setLevel(logging.DEBUG)
|
||||
debug_handler.setFormatter(file_formatter)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
"""SenSu PyEnvManager — Python 版本管理 + venv + 依赖安装"""
|
||||
import os, sys, subprocess, logging, venv, shutil
|
||||
import os, sys, subprocess, logging, shutil
|
||||
from pathlib import Path
|
||||
from typing import Optional, List
|
||||
|
||||
@@ -76,7 +76,8 @@ class PyEnvManager:
|
||||
logger.info(f"创建 venv: {venv_path} (Python {python_version or 'default'})")
|
||||
|
||||
try:
|
||||
venv.create(str(venv_path), with_pip=True, clear=True)
|
||||
import venv as _venv
|
||||
_venv.create(str(venv_path), with_pip=True, clear=True)
|
||||
# Install/upgrade pip
|
||||
pip = str(venv_path / "bin" / "pip")
|
||||
subprocess.run([pip, "install", "--upgrade", "pip"], capture_output=True, timeout=60)
|
||||
|
||||
+15
-16
@@ -149,11 +149,16 @@ class UpdateService:
|
||||
return None
|
||||
|
||||
def _current_version(self) -> str:
|
||||
"""从 version.json 读取本地版本号"""
|
||||
try:
|
||||
init = self.sm.get_service("init")
|
||||
return init.get_config("base").get("framework", {}).get("version", "0.0.0").lstrip("vV")
|
||||
vf = _PROJECT_ROOT / "version.json"
|
||||
if vf.exists():
|
||||
with open(vf) as f:
|
||||
data = json.load(f)
|
||||
return data.get("framework", {}).get("version", "0.0.0").lstrip("vV")
|
||||
except Exception:
|
||||
return "0.0.0"
|
||||
pass
|
||||
return "0.0.0"
|
||||
|
||||
# ── 执行更新 ──
|
||||
|
||||
@@ -250,21 +255,15 @@ class UpdateService:
|
||||
|
||||
# 4.5 同步版本号到旧配置文件
|
||||
try:
|
||||
old_cfg_path = str(_PROJECT_ROOT / "config" / "framework" / "base_config.yaml")
|
||||
new_tpl_path = str(_PROJECT_ROOT / "config" / "framework" / "base_config.yaml.example")
|
||||
if os.path.exists(old_cfg_path) and os.path.exists(new_tpl_path):
|
||||
import yaml as _yaml
|
||||
with open(new_tpl_path) as f:
|
||||
new_tpl = _yaml.safe_load(f)
|
||||
new_ver = new_tpl.get("framework", {}).get("version", "")
|
||||
# 同步版本号到 version.json
|
||||
vf = _PROJECT_ROOT / "version.json"
|
||||
if vf.exists():
|
||||
with open(vf) as f:
|
||||
vdata = json.load(f)
|
||||
new_ver = vdata.get("framework", {}).get("version", "")
|
||||
if new_ver:
|
||||
with open(old_cfg_path) as f:
|
||||
old_cfg = _yaml.safe_load(f) or {}
|
||||
old_cfg.setdefault("framework", {})["version"] = new_ver
|
||||
with open(old_cfg_path, "w") as f:
|
||||
_yaml.dump(old_cfg, f, default_flow_style=False, allow_unicode=True)
|
||||
self._log(f"版本号已同步: {new_ver}")
|
||||
logger.info(f"📝 版本号已同步: {new_ver}")
|
||||
logger.info(f"📝 版本号已同步: {new_ver}")
|
||||
except Exception as e:
|
||||
logger.warning(f"版本号同步失败: {e}")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 注册日志广播
|
||||
|
||||
@@ -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/* (已加认证)")
|
||||
|
||||
@@ -49,14 +49,14 @@ def setup_routes(app, prefix=''):
|
||||
logger.info(f"📡 系统状态WS端点已注册: {prefix}/api/system/ws (已加认证)")
|
||||
|
||||
def _read_version():
|
||||
"""Read version from config file (shared helper)"""
|
||||
ver = "v0.6.0"
|
||||
"""Read version from version.json (shared helper)"""
|
||||
ver = "v0.0.0"
|
||||
try:
|
||||
import yaml, os
|
||||
config_path = os.path.join(os.path.dirname(__file__), "..", "..", "..", "config", "framework", "base_config.yaml")
|
||||
with open(config_path) as f:
|
||||
cfg = yaml.safe_load(f)
|
||||
ver = cfg.get("framework", {}).get("version", ver)
|
||||
import json, os
|
||||
vf = os.path.join(os.path.dirname(__file__), "..", "..", "..", "version.json")
|
||||
with open(vf) as f:
|
||||
data = json.load(f)
|
||||
ver = data.get("framework", {}).get("version", ver)
|
||||
except:
|
||||
pass
|
||||
return ver
|
||||
@@ -193,7 +193,7 @@ async def create_backup(req):
|
||||
if sm:
|
||||
try:
|
||||
init = sm.get_service("init")
|
||||
ver = init.get_config("base").get("framework", {}).get("version", "0.0.0")
|
||||
ver = _read_version()
|
||||
except: pass
|
||||
name = f"SenSu_backup_{ver}_{_time.strftime('%Y%m%d_%H%M%S')}.zip"
|
||||
dest = _os.path.join(backup_dir, name)
|
||||
|
||||
@@ -11,9 +11,22 @@ echo.
|
||||
set PASS=0
|
||||
set FAIL=0
|
||||
|
||||
:: ── Python (venv) ──
|
||||
set PYTHON=%~dp0.venv\Scripts\python.exe
|
||||
if not exist "%PYTHON%" (echo [FAIL] .venv 未创建,请先运行: python -m venv .venv ^&^& .venv\Scripts\pip install -r requirements.txt & pause & exit /b 1)
|
||||
:: ── 检查 --no-venv 参数 ──
|
||||
set NO_VENV=0
|
||||
echo %* | findstr /i "\-\-no-venv" >nul && set NO_VENV=1
|
||||
|
||||
:: ── Python ──
|
||||
if %NO_VENV% equ 1 (
|
||||
set PYTHON=python
|
||||
echo [INFO] --no-venv: 使用系统 Python
|
||||
) else (
|
||||
set PYTHON=%~dp0.venv\Scripts\python.exe
|
||||
if not exist "%PYTHON%" (
|
||||
echo [FAIL] .venv 未创建,请先运行: python -m venv .venv ^&^& .venv\Scripts\pip install -r requirements.txt
|
||||
echo 或使用: start.bat --no-venv 跳过虚拟环境
|
||||
pause & exit /b 1
|
||||
)
|
||||
)
|
||||
"%PYTHON%" --version >nul 2>&1
|
||||
if %errorlevel% equ 0 (
|
||||
echo [OK] Python
|
||||
@@ -58,9 +71,13 @@ if exist "config\framework\base_config.yaml" (
|
||||
echo.
|
||||
if %FAIL% gtr 0 (
|
||||
echo [WARN] %FAIL% 项缺失,自动安装...
|
||||
"%PYTHON%" -m pip install -r requirements.txt
|
||||
"%PYTHON%" -m pip install -r requirements.txt --no-warn-script-location --root-user-action=ignore 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [FAIL] 安装失败,请手动执行: .venv\Scripts\pip install -r requirements.txt
|
||||
if %NO_VENV% equ 1 (
|
||||
echo [FAIL] 安装失败,请手动执行: pip install -r requirements.txt
|
||||
) else (
|
||||
echo [FAIL] 安装失败,请手动执行: .venv\Scripts\pip install -r requirements.txt
|
||||
)
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
@@ -73,6 +90,9 @@ echo 🚀 启动 SenSu (无头模式)...
|
||||
echo Web 面板: http://0.0.0.0:4200/SenSu
|
||||
echo.
|
||||
|
||||
:: 确保项目根目录在 Python 搜索路径中 (嵌入式 CPython 需要)
|
||||
set PYTHONPATH=%~dp0;%PYTHONPATH%
|
||||
|
||||
"%PYTHON%" main.py --headless
|
||||
|
||||
pause
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -50,7 +50,8 @@ window.DashboardModule = {
|
||||
}
|
||||
|
||||
var base = window.location.pathname.split("/").slice(0, 2).join("/");
|
||||
var ws = new WebSocket("ws://" + location.host + base + "/api/system/ws");
|
||||
var proto = location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
var ws = new WebSocket(proto + location.host + base + "/api/system/ws");
|
||||
self.ws = ws;
|
||||
|
||||
ws.onopen = function() {
|
||||
|
||||
@@ -25,7 +25,8 @@ window.LogsModule = {
|
||||
}
|
||||
|
||||
var base = window.location.pathname.split("/").slice(0, 2).join("/");
|
||||
var ws = new WebSocket("ws://" + location.host + base + "/api/logs/ws");
|
||||
var proto = location.protocol === 'https:' ? 'wss://' : 'ws://';
|
||||
var ws = new WebSocket(proto + location.host + base + "/api/logs/ws");
|
||||
self.ws = ws;
|
||||
|
||||
ws.onopen = function() {
|
||||
|
||||
+9
-3
@@ -1,11 +1,17 @@
|
||||
{
|
||||
"framework": {
|
||||
"version": "v0.8.0",
|
||||
"url": "https://git.yeij.top/AskaEth/SenSu/archive/dev.zip",
|
||||
"url": "https://git.yeij.top/AskaEth/SenSu/archive/main.zip",
|
||||
"notes": "Sentinel横版折线图+磁盘分区+负载/SWAP/温度 | Windows兼容 | 自动更新+备份回滚 | 插件导入安装 | 安全加固29项"
|
||||
},
|
||||
"plugins": {
|
||||
"sentinel": { "version": "1.1.0", "url": "" },
|
||||
"example_plugin": { "version": "1.0.0", "url": "" }
|
||||
"sentinel": {
|
||||
"version": "1.1.0",
|
||||
"url": ""
|
||||
},
|
||||
"example_plugin": {
|
||||
"version": "1.0.0",
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user