feat: WS实时推送、网速双线图、按钮MD3修复、页面加载器修复 (v0.6.0)

后端:
- 新增 /api/system/ws WebSocket端点 替代HTTP轮询 (每2s推送系统+框架数据)
- 修复 uptime始终为0 (sm.start_time时间戳)
- 网络采集新增TX上行+实时网速(delta法)
- 进程内存采集 (/proc/self/status VmRSS)
- 抑制aiohttp内部WS帧日志防刷爆日志文件

前端:
- 仪表盘: HTTP轮询→WS连接+指数退避自动重连
- 实时日志: 修复onclose重连bug+批量渲染30fps+500行上限防卡死
- 网速卡片: DualLineChart双线图(下行实线/上行虚线)
- Y轴零点偏移-5% 防止零网速贴底
- 所有按钮修复MD3组合类(btn+btn-tonal+btn-sm)
- 页面加载器: 改动态script元素执行 修复onclick全局作用域问题
- emoji图标→Material Design SVG图标
- CSS去残留</style>+新增.nav-item svg约束
- 登录页输入框+标签左对齐+按钮MD3样式
- 多个版本号显示修复(去双重v前缀)
- chart.js/app.js/HTML页面统一加版本号防浏览器缓存
- .gitignore新增docs/目录

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
qinglong
2026-06-11 16:23:31 +08:00
parent fd0a97fec5
commit 4a989bf50f
24 changed files with 759 additions and 212 deletions
@@ -0,0 +1,21 @@
framework:
debug: true
name: SenSu
version: Alpha_0.2.0
logging:
debug_level_file: true
level: INFO
max_file_size: 10MB
max_log_files: 20
plugins:
auto_load: true
hot_reload: true
max_retry_count: 3
services:
internet:
api_port: 8000
enable_reverse_proxy: false
ws_port: 8765
tui:
layout:
grid-rows: 4fr 5fr 1fr
@@ -0,0 +1,12 @@
admin_permissions:
- framework.*
- plugin.*
- service.*
default_permissions:
- framework.status.read
- plugin.self.info.read
permission_levels:
- read
- write
- execute
- admin
@@ -0,0 +1 @@
{}
@@ -0,0 +1 @@
{}
@@ -0,0 +1,89 @@
commands:
autoscroll:
description: '滚动控制: 切换自动滚动'
permissions:
- framework.tui.control
source: internal
create-plugin:
description: 创建新插件脚手架
permissions:
- framework.scaffold.plugin
source: internal
help:
description: 显示帮助信息
permissions:
- framework.command.help.read
source: internal
history:
description: 显示命令历史
permissions:
- framework.command.history.read
source: internal
netdiag:
description: 网络服务诊断
permissions:
- framework.network.diagnose
source: internal
permissions:
description: '权限管理: 显示权限状态'
permissions:
- framework.permission.read
source: internal
pm_plugin_status:
description: '权限管理: 查看插件权限状态'
permissions:
- framework.permission.read
source: internal
pmallow:
description: '权限管理: 同意权限请求'
permissions:
- framework.permission.read
source: internal
pmdeny:
description: '权限管理: 拒绝权限请求'
permissions:
- framework.permission.read
source: internal
pmhelp:
description: '权限管理: 显示权限命令帮助'
permissions:
- framework.permission.read
source: internal
pmignore:
description: '权限管理: 暂时忽略权限请求'
permissions:
- framework.permission.read
source: internal
pmpending:
description: '权限管理: 查看待授权请求列表'
permissions:
- framework.permission.read
source: internal
pmrequests:
description: '权限管理: 查看待授权请求列表(别名)'
permissions:
- framework.permission.read
source: internal
pmtest:
description: '权限管理: 测试权限配置文件'
permissions:
- framework.permission.read
source: internal
scroll:
description: '滚动控制: 手动滚动到底部'
permissions:
- framework.tui.control
source: internal
status:
description: 显示框架状态
permissions:
- framework.status.read
source: internal
testlog:
description: 生成测试日志
permissions:
- framework.command.test
source: internal
last_updated: 283025.170846772
plugin_commands: {}
total_commands: 17
+27 -7
View File
@@ -23,11 +23,22 @@
<canvas id="chart-cpu" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<h3>🌐 网络接收</h3>
<div class="stat-value" id="d-net">--</div>
<h3>🌐 网络流量</h3>
<div style="display:flex; gap:16px">
<div><div class="stat-sub" style="font-size:.65rem">下行</div><div class="stat-value" id="d-net-rx" style="font-size:1.4rem">--</div></div>
<div><div class="stat-sub" style="font-size:.65rem">上行</div><div class="stat-value" id="d-net-tx" style="font-size:1.4rem">--</div></div>
</div>
<canvas id="chart-net" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<div class="stat-card">
<h3>⚡ 实时网速</h3>
<div style="display:flex; gap:16px">
<div><div class="stat-sub" style="font-size:.65rem">↓ 下载</div><div class="stat-value" id="d-speed-rx" style="font-size:1.4rem">--</div></div>
<div><div class="stat-sub" style="font-size:.65rem">↑ 上传</div><div class="stat-value" id="d-speed-tx" style="font-size:1.4rem">--</div></div>
</div>
<canvas id="chart-speed" class="mini-chart"></canvas>
</div>
<div class="stat-card">
<h3>💾 进程内存</h3>
<div class="stat-value" id="d-proc-mem">--</div>
<canvas id="chart-proc-mem" class="mini-chart"></canvas>
@@ -60,11 +71,20 @@
</div>
</div>
<!-- 3. 快捷操作 (占位) -->
<!-- 3. 快捷操作 -->
<div class="side-card">
<h3>🛠️ 快捷操作</h3>
<button class="btn-sm" style="width:100%; margin-bottom:6px" onclick="window.PluginsModule?.refresh()">🔄 刷新插件列表</button>
<button class="btn-sm" style="width:100%; color:var(--error); border-color:var(--error)" onclick="doLogout()">🚪 退出登录</button>
<h3>
<svg width="18" height="18" viewBox="0 0 24 24" style="vertical-align:-3px;margin-right:6px"><path fill="var(--primary)" d="M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94L14.4 2.81c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41L9.25 5.35c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
快捷操作
</h3>
<button class="btn btn-sm btn-tonal" style="width:100%;margin-bottom:6px" onclick="window.PluginsModule?.refresh()">
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
刷新插件列表
</button>
<button class="btn btn-sm btn-outlined" style="width:100%;color:var(--error);border-color:var(--error)" onclick="doLogout()">
<svg width="16" height="16" viewBox="0 0 24 24"><path fill="currentColor" d="M17 7l-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4V5z"/></svg>
退出登录
</button>
</div>
</aside>
</div>
+161 -84
View File
@@ -1,105 +1,182 @@
window.DashboardModule = {
ws: null,
reconnectTimer: null,
reconnectDelay: 1000,
charts: {},
init: () => {
// 1. 初始化图表实例
window.DashboardModule.charts = {
mem: new MiniChart('chart-mem', '#9ece6a'),
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
net: new MiniChart('chart-net', '#e0af68'),
init: function() {
var self = this;
// 初始化图表
self.charts = {
mem: new MiniChart('chart-mem', '#9ece6a'),
cpu: new MiniChart('chart-cpu', '#7aa2f7'),
net: new MiniChart('chart-net', '#e0af68'),
speed: new DualLineChart('chart-speed', '#bb9af7', '#c0a8f0'),
proc_mem: new MiniChart('chart-proc-mem', '#f7768e')
};
// 2. 获取并填充右侧固定信息 (只获取一次即可,除非重启)
// 静态信息 (HTTP 一次)
fetchSystemStaticInfo();
// 3. 启动实时数据轮询
fetchDash();
window._dashInterval = setInterval(fetchDash, 2000); // 2秒刷新
// WebSocket 实时推送
self._connect();
},
destroy: () => {
clearInterval(window._dashInterval);
window.DashboardModule.charts = {};
_connect: function() {
var self = this;
if (self.ws) {
self.ws.onclose = null;
self.ws.close();
self.ws = null;
}
var base = window.location.pathname.split("/").slice(0, 2).join("/");
var ws = new WebSocket("ws://" + location.host + base + "/api/system/ws");
self.ws = ws;
ws.onopen = function() {
self.reconnectDelay = 1000;
};
ws.onmessage = function(e) {
try {
var d = JSON.parse(e.data);
if (d.type === "sys") {
self._updateUI(d.system, d.framework);
}
} catch(ex) {}
};
ws.onclose = function() {
self.ws = null;
self.reconnectTimer = setTimeout(function() {
self.reconnectDelay = Math.min(self.reconnectDelay * 1.5, 15000);
self._connect();
}, self.reconnectDelay);
};
ws.onerror = function() { ws.close(); };
},
_updateUI: function(sys, fw) {
// ── 框架卡片 ──
if (fw) {
var el;
el = document.getElementById('d-uptime'); if (el) el.textContent = formatUptime(fw.uptime || 0);
el = document.getElementById('d-plugins'); if (el) el.textContent = fw.plugins || 0;
el = document.getElementById('d-ver-badge'); if (el) el.textContent = fw.version || '?';
}
if (!sys) return;
var self = this;
// ── 内存 ──
if (sys.memory) {
var m = sys.memory.percent || 0;
var el = document.getElementById('d-mem'); if (el) el.textContent = m + '%';
self.charts.mem.update(m);
}
// ── CPU ──
if (sys.cpu) {
var cpuVal = sys.cpu.percent;
if (cpuVal === null || cpuVal === undefined) {
var load = (sys.cpu.load_avg && sys.cpu.load_avg[0]) ? sys.cpu.load_avg[0] : 0;
cpuVal = Math.min(100, (load / (sys.cpu.cores || 1)) * 100);
}
var el = document.getElementById('d-cpu'); if (el) el.textContent = Math.round(cpuVal) + '%';
self.charts.cpu.update(cpuVal);
if (sys.cpu.load_avg) {
el = document.getElementById('info-load'); if (el) el.textContent = sys.cpu.load_avg[2].toFixed(2);
}
}
// ── 网络累计流量 (RX + TX) ──
if (sys.network) {
var el;
el = document.getElementById('d-net-rx'); if (el) el.textContent = formatTraffic(sys.network.rx_mb);
el = document.getElementById('d-net-tx'); if (el) el.textContent = formatTraffic(sys.network.tx_mb);
// Chart shows RX trend
self.charts.net.update(sys.network.rx_mb || 0);
}
// ── 实时网速 ──
if (sys.net_speed) {
var el;
el = document.getElementById('d-speed-rx');
if (el) el.textContent = formatSpeed(sys.net_speed.rx_bytes_sec);
el = document.getElementById('d-speed-tx');
if (el) el.textContent = formatSpeed(sys.net_speed.tx_bytes_sec);
// Chart: RX solid line, TX dashed line
self.charts.speed.update(sys.net_speed.rx_bytes_sec || 0, sys.net_speed.tx_bytes_sec || 0);
}
// ── 进程内存 ──
if (sys.process && sys.process.memory_mb !== undefined) {
var pm = sys.process.memory_mb || 0;
var el = document.getElementById('d-proc-mem'); if (el) el.textContent = pm + ' MB';
self.charts.proc_mem.update(pm);
}
if (sys.process && sys.process.pid !== undefined) {
var el = document.getElementById('info-pid'); if (el) el.textContent = sys.process.pid;
}
},
destroy: function() {
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
if (this.ws) { this.ws.onclose = null; this.ws.close(); this.ws = null; }
this.charts = {};
}
};
/* ── 静态信息 ── */
async function fetchSystemStaticInfo() {
try {
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
// 硬件信息
if(sys.platform) {
document.getElementById('info-os').textContent = sys.platform.system || '--';
document.getElementById('info-arch').textContent = sys.platform.machine || '--';
document.getElementById('info-env').textContent = sys.platform.env || 'Standard';
var sys = await fetch('./api/system', {credentials:'include'}).then(function(r){return r.json();});
var el;
if (sys.platform) {
el = document.getElementById('info-os'); if (el) el.textContent = sys.platform.system || '--';
el = document.getElementById('info-arch'); if (el) el.textContent = sys.platform.machine || '--';
el = document.getElementById('info-env'); if (el) el.textContent = sys.platform.env || 'Standard';
}
if(sys.cpu) {
const c = sys.cpu.cores || 0;
document.getElementById('info-cores').textContent = `${c} / ${c}`; // Android下通常逻辑核=物理核
if (sys.cpu) {
var c = sys.cpu.cores || 0;
el = document.getElementById('info-cores'); if (el) el.textContent = c + ' / ' + c;
}
if(sys.memory) {
document.getElementById('info-mem-total').textContent = sys.memory.total_gb + ' GB';
if (sys.memory) {
el = document.getElementById('info-mem-total'); if (el) el.textContent = sys.memory.total_gb + ' GB';
}
// 框架信息 (部分需结合 API)
const host = window.location.hostname + (window.location.port ? ':'+window.location.port : '');
document.getElementById('info-addr').textContent = host;
// 框架信息 (部分来自 API)
try {
var fw = await fetch('./api/framework', {credentials:'include'}).then(function(r){return r.json();});
el = document.getElementById('info-fw-ver'); if (el) el.textContent = fw.version || '?';
} catch(e) {}
var host = window.location.hostname + (window.location.port ? ':' + window.location.port : '');
el = document.getElementById('info-addr'); if (el) el.textContent = host;
} catch(e) {}
}
async function fetchDash() {
try {
const fw = await fetch('./api/framework', {credentials:'include'}).then(r => r.json());
const sys = await fetch('./api/system', {credentials:'include'}).then(r => r.json());
// --- 左侧动态数据更新 ---
if(fw) {
document.getElementById('d-uptime').textContent = formatUptime(fw.uptime || 0);
document.getElementById('d-plugins').textContent = fw.plugins || 0;
if(document.getElementById('d-ver-badge')) document.getElementById('d-ver-badge').textContent = 'v' + (fw.version||'?');
}
if(sys) {
// 内存
const m = sys.memory?.percent || 0;
document.getElementById('d-mem').textContent = m + '%';
window.DashboardModule.charts.mem.update(m);
// CPU (兼容 Android null 情况)
let cpuVal = sys.cpu?.percent;
if (cpuVal === null || cpuVal === undefined) {
const load = sys.cpu?.load_avg?.[0] || 0;
const cores = sys.cpu?.cores || 1;
cpuVal = Math.min(100, (load / cores) * 100);
}
document.getElementById('d-cpu').textContent = Math.round(cpuVal) + '%';
window.DashboardModule.charts.cpu.update(cpuVal);
// 网络 (RX 总量)
const netRx = sys.network?.rx || 0;
document.getElementById('d-net').textContent = netRx + ' MB';
window.DashboardModule.charts.net.update(netRx); // 图表显示总流量趋势
// 进程内存
const pm = sys.process?.memory_mb || 0;
document.getElementById('d-proc-mem').textContent = pm + ' MB';
window.DashboardModule.charts.proc_mem.update(pm);
// --- 右侧动态数据更新 ---
if(sys.process) {
document.getElementById('info-pid').textContent = sys.process.pid || '--';
}
if(sys.cpu?.load_avg) {
document.getElementById('info-load').textContent = sys.cpu.load_avg[2].toFixed(2);
}
}
} catch(e) { console.warn("Dashboard fetch error", e); }
}
// 辅助:秒数转时间格式
/* ── 格式化 ── */
function formatUptime(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return `${h}h ${m}m ${s}s`;
var h = Math.floor(seconds / 3600);
var m = Math.floor((seconds % 3600) / 60);
var s = Math.floor(seconds % 60);
if (h > 0) return h + 'h ' + m + 'm';
if (m > 0) return m + 'm ' + s + 's';
return s + 's';
}
function formatTraffic(mb) {
if (mb === undefined || mb === null) return '--';
if (mb >= 1024) return (mb / 1024).toFixed(1) + ' GB';
if (mb >= 1) return mb.toFixed(1) + ' MB';
return (mb * 1024).toFixed(0) + ' KB';
}
function formatSpeed(bytesPerSec) {
if (bytesPerSec === undefined || bytesPerSec === null || bytesPerSec < 0) return '0 B/s';
if (bytesPerSec >= 1048576) return (bytesPerSec / 1048576).toFixed(1) + ' MB/s';
if (bytesPerSec >= 1024) return (bytesPerSec / 1024).toFixed(0) + ' KB/s';
return bytesPerSec.toFixed(0) + ' B/s';
}
+128 -21
View File
@@ -1,25 +1,132 @@
window.LogsModule = {
ws: null,
init: () => {
const box = document.getElementById("log-box");
const base = window.location.pathname.split("/").slice(0,2).join("/");
const connect = () => {
window.LogsModule.ws = new WebSocket("ws://"+location.host+base+"/api/logs/ws");
window.LogsModule.ws.onopen = () => box.innerHTML += "<div style=\"color:var(--success)\">Connected</div>";
window.LogsModule.ws.onmessage = e => {
try {
const d = JSON.parse(e.data);
if(d.type === "log") {
const cls = d.level === "ERROR" ? "log-ERROR" : d.level === "WARNING" ? "log-WARNING" : "log-INFO";
const t = d.timestamp ? new Date(d.timestamp*1000).toLocaleTimeString() : "--";
box.innerHTML += "<div class=\"log-entry\"><span style=\"color:#555;margin-right:5px\">"+t+"</span><span class=\""+cls+"\">["+d.level+"]</span> "+d.message+"</div>";
box.scrollTop = box.scrollHeight;
}
}catch(e){}
};
window.LogsModule.ws.onclose = setTimeout(connect, 3000);
};
connect();
reconnectTimer: null,
reconnectDelay: 1000,
maxLines: 500,
lineCount: 0,
batchBuffer: [],
batchTimer: null,
init: function() {
var self = this;
self.box = document.getElementById("log-box");
if (!self.box) return;
self.box.innerHTML = "";
self.lineCount = 0;
self._connect();
},
destroy: () => window.LogsModule.ws?.close()
_connect: function() {
var self = this;
if (self.ws) {
self.ws.onclose = null;
self.ws.close();
self.ws = null;
}
var base = window.location.pathname.split("/").slice(0, 2).join("/");
var ws = new WebSocket("ws://" + location.host + base + "/api/logs/ws");
self.ws = ws;
ws.onopen = function() {
self.reconnectDelay = 1000;
self._appendHTML('<div style="color:var(--md-sys-color-primary)">🟢 Connected</div>');
};
ws.onmessage = function(e) {
try {
var d = JSON.parse(e.data);
if (d.type === "log") {
self._bufferLog(d);
}
} catch(ex) {}
};
ws.onclose = function() {
self.ws = null;
self._appendHTML('<div style="color:var(--error)">🔴 Disconnected — reconnecting...</div>');
self.reconnectTimer = setTimeout(function() {
self.reconnectDelay = Math.min(self.reconnectDelay * 1.5, 15000);
self._connect();
}, self.reconnectDelay);
};
ws.onerror = function() {
ws.close();
};
},
/* Buffer log entries then flush at ~30fps to avoid DOM thrashing */
_bufferLog: function(d) {
var self = this;
self.batchBuffer.push(d);
if (!self.batchTimer) {
self.batchTimer = setTimeout(function() {
self._flush();
self.batchTimer = null;
}, 33); // ~30fps flush
}
},
_flush: function() {
var self = this;
var batch = self.batchBuffer;
self.batchBuffer = [];
if (!batch.length || !self.box) return;
var html = '';
for (var i = 0; i < batch.length; i++) {
var d = batch[i];
var cls = d.level === "ERROR" ? "log-ERROR" : d.level === "WARNING" ? "log-WARNING" : "log-INFO";
var t = d.timestamp ? new Date(d.timestamp * 1000).toLocaleTimeString() : "--";
html += '<div class="log-entry">' +
'<span style="color:#555;margin-right:5px">' + t + '</span>' +
'<span class="' + cls + '">[' + d.level + ']</span> ' +
self._escapeHtml(d.message || '') + '</div>';
}
self._appendHTML(html);
},
_appendHTML: function(html) {
var self = this;
if (!self.box) return;
// Estimate line count from <div> tags
var newLines = (html.match(/<div/g) || []).length;
self.lineCount += newLines;
// Trim old lines if over cap
while (self.lineCount > self.maxLines && self.box.firstChild) {
// Count lines in the first child
var removed = 1;
if (self.box.firstChild.nodeType === 1) {
var inner = self.box.firstChild.innerHTML || '';
removed = Math.max(1, (inner.match(/<div/g) || []).length);
}
self.box.removeChild(self.box.firstChild);
self.lineCount = Math.max(0, self.lineCount - removed);
}
// Efficient append: insertAdjacentHTML instead of innerHTML +=
self.box.insertAdjacentHTML("beforeend", html);
self.box.scrollTop = self.box.scrollHeight;
},
_escapeHtml: function(text) {
return String(text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
},
destroy: function() {
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; }
if (this.ws) { this.ws.onclose = null; this.ws.close(); this.ws = null; }
this.batchBuffer = [];
this.box = null;
}
};
+8 -2
View File
@@ -1,5 +1,11 @@
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem;">
<h2 style="color:var(--accent);">📦 插件管理</h2>
<button class="btn-sm" onclick="window.PluginsModule.refresh()">🔄 刷新</button>
<h2 style="color:var(--primary); display:flex; align-items:center; gap:8px">
<svg width="22" height="22" viewBox="0 0 24 24"><path fill="var(--primary)" d="M20.5 11H19V7c0-1.1-.9-2-2-2h-4V3.5C13 2.12 11.88 1 10.5 1S8 2.12 8 3.5V5H4c-1.1 0-1.99.9-1.99 2v3.8H3.5c1.49 0 2.7 1.21 2.7 2.7s-1.21 2.7-2.7 2.7H2V20c0 1.1.9 2 2 2h3.8v-1.5c0-1.49 1.21-2.7 2.7-2.7 1.49 0 2.7 1.21 2.7 2.7V22H17c1.1 0 2-.9 2-2v-4h1.5c1.38 0 2.5-1.12 2.5-2.5S21.88 11 20.5 11z"/></svg>
插件管理
</h2>
<button class="btn btn-sm btn-tonal" onclick="window.PluginsModule.refresh()">
<svg width="16" height="16" viewBox="0 0 24 24" style="vertical-align:-2px"><path fill="currentColor" d="M17.65 6.35C16.2 4.9 14.21 4 12 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08c-.82 2.33-3.04 4-5.65 4-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/></svg>
刷新
</button>
</div>
<div id="plugin-list" class="plugin-list">加载中...</div>
+3 -3
View File
@@ -17,10 +17,10 @@ window.PluginsModule = {
</div>
<div class="plugin-act">
${p.running
? `<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','disable')">停用</button>`
: `<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','enable')">启用</button>`
? `<button class="btn btn-sm btn-tonal" onclick="window.PluginsModule.act('${p.name}','disable')">停用</button>`
: `<button class="btn btn-sm btn-tonal" onclick="window.PluginsModule.act('${p.name}','enable')">启用</button>`
}
<button class="btn-sm" onclick="window.PluginsModule.act('${p.name}','reload')">重载</button>
<button class="btn btn-sm btn-outlined" onclick="window.PluginsModule.act('${p.name}','reload')">重载</button>
</div>
</div>
`).join('');
+6 -6
View File
@@ -38,23 +38,23 @@
var projects=[];
function showTab(e,t){document.querySelectorAll("#tab-running,#tab-add,#tab-git").forEach(el=>el.style.display="none");document.getElementById("tab-"+t).style.display="block";document.querySelectorAll(".tab").forEach(el=>el.classList.remove("active"));e.target.classList.add("active")}
async function refresh(){
try{var r=await fetch("/SenSu/api/projects");var d=await r.json();projects=d.projects;var h="";for(var p of projects){var cls=p.status==="running"?"status-running":p.status==="error"?"status-error":"status-stopped";h+='<div class="mgmt-card"><div class="status-dot '+cls+'"></div><div class="info"><b>'+p.name+'</b><br><small>'+p.status+" PID:"+(p.pid||"-")+" :"+(p.port||"-")+" "+(p.uptime?p.uptime+"s":"")+(p.proxy?" → "+p.proxy:"")+'</small></div><div class="actions"><button class="btn btn-sm btn-danger" onclick="stopP(this,'+JSON.stringify(p.name)+')">停止</button><button class="btn btn-sm btn-outlined" onclick="logsP(this,'+JSON.stringify(p.name)+')">日志</button><button class="btn btn-sm btn-text" onclick="cmdP(this,'+JSON.stringify(p.name)+')">命令</button></div></div><div class="log-box" id="logs-'+p.name+'"></div>'}document.getElementById("project-list").innerHTML=h||"<div style=\"color:var(--text-dim);text-align:center;padding:40px\">暂无项目</div>"}catch(e){document.getElementById("project-list").innerHTML="<div style=\"color:var(--error);text-align:center;padding:40px\">引擎未就绪 (API 错误)</div>"}
try{var r=await fetch("./api/projects");var d=await r.json();projects=d.projects;var h="";for(var p of projects){var cls=p.status==="running"?"status-running":p.status==="error"?"status-error":"status-stopped";h+='<div class="mgmt-card"><div class="status-dot '+cls+'"></div><div class="info"><b>'+p.name+'</b><br><small>'+p.status+" PID:"+(p.pid||"-")+" :"+(p.port||"-")+" "+(p.uptime?p.uptime+"s":"")+(p.proxy?" → "+p.proxy:"")+'</small></div><div class="actions"><button class="btn btn-sm btn-danger" onclick="stopP(this,'+JSON.stringify(p.name)+')">停止</button><button class="btn btn-sm btn-outlined" onclick="logsP(this,'+JSON.stringify(p.name)+')">日志</button><button class="btn btn-sm btn-text" onclick="cmdP(this,'+JSON.stringify(p.name)+')">命令</button></div></div><div class="log-box" id="logs-'+p.name+'"></div>'}document.getElementById("project-list").innerHTML=h||"<div style=\"color:var(--text-dim);text-align:center;padding:40px\">暂无项目</div>"}catch(e){document.getElementById("project-list").innerHTML="<div style=\"color:var(--error);text-align:center;padding:40px\">引擎未就绪 (API 错误)</div>"}
}
async function addProject(){
var n=document.getElementById("proj-name").value,cmd=document.getElementById("proj-cmd").value;
if(!n||!cmd){alert("请填写名称和命令");return}
await fetch("/SenSu/api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:cmd.split(" "),cwd:document.getElementById("proj-cwd").value,port:parseInt(document.getElementById("proj-port").value)||0,proxy_path:document.getElementById("proj-proxy").value})});
await fetch("./api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:cmd.split(" "),cwd:document.getElementById("proj-cwd").value,port:parseInt(document.getElementById("proj-port").value)||0,proxy_path:document.getElementById("proj-proxy").value})});
refresh()
}
async function deployGit(){
var u=document.getElementById("git-url").value,b=document.getElementById("git-branch").value;
if(!u){alert("请填写 Git URL");return}
var n=u.split("/").pop().replace(".git","");
await fetch("/SenSu/api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:["git","clone","-b",b,u,n],cwd:"data/projects"})});
await fetch("./api/projects/run",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:n,cmd:["git","clone","-b",b,u,n],cwd:"data/projects"})});
refresh()
}
async function stopP(el,n){await fetch("/SenSu/api/projects/"+n+"/stop",{method:"POST"});refresh()}
async function logsP(el,n){var b=document.getElementById("logs-"+n);b.style.display=b.style.display==="none"?"block":"none";if(b.style.display==="block"){var r=await fetch("/SenSu/api/projects/"+n+"/logs?tail=30");var d=await r.json();b.innerHTML=d.logs.join("<br>")||"(无日志)"}}
async function cmdP(el,n){var t=prompt("命令: "+n);if(t)await fetch("/SenSu/api/projects/"+n+"/stdin",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})})}
async function stopP(el,n){await fetch("./api/projects/"+n+"/stop",{method:"POST"});refresh()}
async function logsP(el,n){var b=document.getElementById("logs-"+n);b.style.display=b.style.display==="none"?"block":"none";if(b.style.display==="block"){var r=await fetch("./api/projects/"+n+"/logs?tail=30");var d=await r.json();b.innerHTML=d.logs.join("<br>")||"(无日志)"}}
async function cmdP(el,n){var t=prompt("命令: "+n);if(t)await fetch("./api/projects/"+n+"/stdin",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:t})})}
setInterval(refresh,4000);refresh();
</script>