29 Commits

Author SHA1 Message Date
qinglong 2798100323 docs: 更新ROADMAP至v0.8.0,补充v0.6-v0.8完成内容 2026-06-14 21:44:42 +08:00
qinglong 2a0f62ef1c Merge branch 'dev' 2026-06-14 20:22:37 +08:00
qinglong 8ca6da67ab fix: app_v3.js同步app.js的SENSU_BASE前缀改动 2026-06-14 20:21:08 +08:00
qinglong 10630f3b8f fix: 插件路由注册时从配置文件fallback读取面板路径,解决初始化顺序导致的路径缺失 2026-06-14 20:15:27 +08:00
qinglong 6c6c88b22a fix: 插件API路由统一加面板前缀(/SenSu),解决反代后插件页面空白 2026-06-14 20:14:22 +08:00
qinglong 11e8bf2a71 Merge branch 'dev' 2026-06-14 19:52:16 +08:00
qinglong 26d515e080 fix: WebSocket连接根据页面协议自动选择ws/wss,修复HTTPS下Mixed Content报错 2026-06-14 19:39:36 +08:00
qinglong cd45372141 fix: start.bat pip安装抑制警告避免误判失败,报错信息区分venv/no-venv 2026-06-14 19:23:50 +08:00
qinglong d626a0631a fix: RotatingFileHandler强制UTF-8编码避免Windows GBK日志乱码 2026-06-14 19:06:32 +08:00
qinglong 846b8a979a fix: Windows控制台强制UTF-8避免emoji编解码报错 2026-06-14 19:03:23 +08:00
qinglong 2c5cd57ad4 fix: venv改为懒加载,兼容嵌入式CPython无venv模块 2026-06-14 19:02:27 +08:00
qinglong b9252bdc58 fix: start.bat添加PYTHONPATH确保嵌入式CPython能找到项目模块 2026-06-14 18:59:37 +08:00
qinglong 40a1502f8b feat: start.bat支持--no-venv跳过虚拟环境使用系统Python 2026-06-14 18:29:31 +08:00
qinglong 6d30096005 feat: start.bat支持--no-venv跳过虚拟环境使用系统Python 2026-06-14 18:28:49 +08:00
qinglong 28670204cf fix: 横版数据更新时间移至卡片左下角col-left 2026-06-14 17:03:03 +08:00
qinglong 316986c420 Merge branch 'dev' 2026-06-14 16:57:54 +08:00
qinglong 6c36743b89 feat: Sentinel节点卡片添加HTTP链路延迟显示(横版左下角/网格右上角) 2026-06-14 16:57:04 +08:00
qinglong 280cd3e994 fix: HiChart用canvas自身getBoundingClientRect获取渲染尺寸,消除CSS拉伸导致的线条变形 2026-06-14 16:30:53 +08:00
qinglong f40b87ea52 fix: 仪表盘版本号从version.json读取,修复硬编码v0.6.0 2026-06-14 16:24:41 +08:00
qinglong 5f38c9e8eb fix: version.json url 指向 main.zip 2026-06-14 16:19:44 +08:00
qinglong 8168d24fad Merge branch 'dev' 2026-06-14 16:18:24 +08:00
qinglong a82c94bd51 chore: version.json切回dev 2026-06-14 16:04:09 +08:00
qinglong 35e2368023 Merge branch 'dev' 2026-06-14 16:04:07 +08:00
qinglong b29b236a52 chore: version.json→main 2026-06-14 16:04:05 +08:00
qinglong 3f5b01f817 refactor: 版本号从version.json读取 配置模板删version字段 2026-06-14 16:03:04 +08:00
qinglong 4d51a7620e chore: version.json切回dev分支 2026-06-14 15:58:36 +08:00
qinglong 103d192e8f merge: dev → main (v0.8.0 稳定版) 2026-06-14 15:57:08 +08:00
qinglong 90a563aef0 chore: version.json指向main分支 2026-06-14 15:56:45 +08:00
AskaEth 5a9d96dc1b Initial commit 2026-06-10 12:23:04 +08:00
18 changed files with 197 additions and 70 deletions
+31 -2
View File
@@ -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:
+14
View File
@@ -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
+10 -4
View File
@@ -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
+25 -10
View File
@@ -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 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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
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");
+19 -2
View File
@@ -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:
+4 -2
View File
@@ -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)
+3 -2
View File
@@ -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)
+13 -14
View File
@@ -149,10 +149,15 @@ 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:
pass
return "0.0.0"
# ── 执行更新 ──
@@ -250,19 +255,13 @@ 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}")
except Exception as e:
+3 -1
View File
@@ -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)
# 注册日志广播
+7 -5
View File
@@ -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/* (已加认证)")
+8 -8
View File
@@ -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)
+23 -3
View File
@@ -11,9 +11,22 @@ echo.
set PASS=0
set FAIL=0
:: ── Python (venv) ──
:: ── 检查 --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 & pause & exit /b 1)
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 (
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
+9 -2
View File
@@ -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();
+8 -2
View File
@@ -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();
+2 -1
View File
@@ -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() {
+2 -1
View File
@@ -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
View File
@@ -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": ""
}
}
}