feat: add mihomo service start/stop/restart control
This commit is contained in:
+99
-3
@@ -216,12 +216,39 @@
|
||||
<div class="stat-value" id="stat-subs">-</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mihomo Service Control -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">快捷操作</span>
|
||||
<span class="card-title">🔧 Mihomo 服务控制</span>
|
||||
<button class="btn btn-ghost btn-sm" onclick="loadMihomoStatus()">🔄 刷新状态</button>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:16px">
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span style="font-weight:600">运行状态:</span>
|
||||
<span id="mihomo-status" class="badge badge-danger">未知</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span style="font-weight:600">自启:</span>
|
||||
<span id="mihomo-enabled" class="badge badge-warning">未知</span>
|
||||
</div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<span style="font-weight:600">PID:</span>
|
||||
<span id="mihomo-pid" style="color:var(--text2);font-family:monospace">-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-success" id="btn-start" onclick="mihomoAction('start')">▶ 启动</button>
|
||||
<button class="btn btn-danger" id="btn-stop" onclick="mihomoAction('stop')">■ 停止</button>
|
||||
<button class="btn btn-primary" id="btn-restart" onclick="mihomoAction('restart')">🔄 重启</button>
|
||||
<button class="btn btn-ghost" onclick="reloadConfig()">📋 重载配置</button>
|
||||
<a class="btn btn-ghost" href="/yacd/" target="_blank">🎨 Yacd 面板</a>
|
||||
</div>
|
||||
<div id="mihomo-logs" style="margin-top:16px;display:none">
|
||||
<details>
|
||||
<summary style="cursor:pointer;color:var(--text2);font-size:13px">📝 查看服务日志</summary>
|
||||
<pre id="mihomo-log-content" style="margin-top:8px;font-size:11px;max-height:300px;overflow-y:auto;background:var(--bg);padding:12px;border-radius:6px;color:var(--text2)"></pre>
|
||||
</details>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="reloadConfig()">🔄 重载配置</button>
|
||||
<a class="btn btn-ghost" href="/yacd/" target="_blank" style="margin-left:8px">🎨 打开 Yacd 面板</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -556,9 +583,78 @@
|
||||
} catch(e) { toast('加载日志失败', 'error'); }
|
||||
}
|
||||
|
||||
// ========== Mihomo Service Control ==========
|
||||
async function loadMihomoStatus() {
|
||||
try {
|
||||
const status = await api('GET', '/api/mihomo/status');
|
||||
const statusEl = document.getElementById('mihomo-status');
|
||||
const enabledEl = document.getElementById('mihomo-enabled');
|
||||
const pidEl = document.getElementById('mihomo-pid');
|
||||
|
||||
if (status.active) {
|
||||
statusEl.textContent = '运行中';
|
||||
statusEl.className = 'badge badge-success';
|
||||
document.getElementById('btn-start').disabled = true;
|
||||
document.getElementById('btn-stop').disabled = false;
|
||||
document.getElementById('btn-restart').disabled = false;
|
||||
} else {
|
||||
statusEl.textContent = '已停止';
|
||||
statusEl.className = 'badge badge-danger';
|
||||
document.getElementById('btn-start').disabled = false;
|
||||
document.getElementById('btn-stop').disabled = true;
|
||||
document.getElementById('btn-restart').disabled = true;
|
||||
}
|
||||
|
||||
if (status.enabled) {
|
||||
enabledEl.textContent = '已启用';
|
||||
enabledEl.className = 'badge badge-success';
|
||||
} else {
|
||||
enabledEl.textContent = '未启用';
|
||||
enabledEl.className = 'badge badge-warning';
|
||||
}
|
||||
|
||||
pidEl.textContent = status.pid || '-';
|
||||
} catch(e) {
|
||||
console.error('Failed to load mihomo status:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function mihomoAction(action) {
|
||||
const btnMap = { start: 'btn-start', stop: 'btn-stop', restart: 'btn-restart' };
|
||||
const btn = document.getElementById(btnMap[action]);
|
||||
const origText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> 处理中...';
|
||||
|
||||
try {
|
||||
const res = await api('POST', '/api/mihomo/' + action);
|
||||
if (res.error) throw new Error(res.error);
|
||||
toast('Mihomo ' + (action === 'start' ? '已启动' : action === 'stop' ? '已停止' : '已重启'), 'success');
|
||||
await new Promise(r => setTimeout(r, 1500));
|
||||
loadMihomoStatus();
|
||||
if (action !== 'stop') loadDashboard();
|
||||
} catch(e) {
|
||||
toast(action + ' 失败: ' + e.message, 'error');
|
||||
} finally {
|
||||
btn.textContent = origText;
|
||||
btn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMihomoLogs() {
|
||||
try {
|
||||
const res = await api('GET', '/api/mihomo/logs');
|
||||
if (res.logs) {
|
||||
document.getElementById('mihomo-log-content').textContent = res.logs;
|
||||
document.getElementById('mihomo-logs').style.display = 'block';
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
// ========== Init ==========
|
||||
window.addEventListener('load', () => {
|
||||
loadDashboard();
|
||||
loadMihomoStatus();
|
||||
// Handle hash navigation
|
||||
const hash = location.hash.replace('#', '');
|
||||
if (hash) showPage(hash);
|
||||
|
||||
@@ -91,6 +91,15 @@ function sendError(res, msg, status = 500) {
|
||||
sendJson(res, { error: msg }, status);
|
||||
}
|
||||
|
||||
function execCommand(cmd) {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(cmd, { timeout: 15000 }, (err, stdout, stderr) => {
|
||||
if (err) reject(new Error(stderr || err.message));
|
||||
else resolve(stdout.trim());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ========== Static File Server ==========
|
||||
const MIME_TYPES = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
@@ -381,6 +390,59 @@ async function handleApi(req, res, apiPath) {
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/mihomo/status - Get mihomo service status
|
||||
if (apiPath === '/api/mihomo/status' && method === 'GET') {
|
||||
try {
|
||||
const active = await execCommand('systemctl is-active mihomo');
|
||||
const enabled = await execCommand('systemctl is-enabled mihomo');
|
||||
let pid = '';
|
||||
try { pid = await execCommand('systemctl show mihomo --property=MainPID --value'); } catch {}
|
||||
return sendJson(res, { active: active === 'active', enabled: enabled === 'enabled', pid, status: active });
|
||||
} catch (e) {
|
||||
return sendJson(res, { active: false, enabled: false, pid: '', status: 'unknown' });
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/mihomo/start - Start mihomo
|
||||
if (apiPath === '/api/mihomo/start' && method === 'POST') {
|
||||
try {
|
||||
await execCommand('systemctl start mihomo');
|
||||
return sendJson(res, { ok: true, message: 'Mihomo started' });
|
||||
} catch (e) {
|
||||
return sendError(res, 'Failed to start: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/mihomo/stop - Stop mihomo
|
||||
if (apiPath === '/api/mihomo/stop' && method === 'POST') {
|
||||
try {
|
||||
await execCommand('systemctl stop mihomo');
|
||||
return sendJson(res, { ok: true, message: 'Mihomo stopped' });
|
||||
} catch (e) {
|
||||
return sendError(res, 'Failed to stop: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// POST /api/mihomo/restart - Restart mihomo
|
||||
if (apiPath === '/api/mihomo/restart' && method === 'POST') {
|
||||
try {
|
||||
await execCommand('systemctl restart mihomo');
|
||||
return sendJson(res, { ok: true, message: 'Mihomo restarted' });
|
||||
} catch (e) {
|
||||
return sendError(res, 'Failed to restart: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/mihomo/logs - Get mihomo service logs
|
||||
if (apiPath === '/api/mihomo/logs' && method === 'GET') {
|
||||
try {
|
||||
const logs = await execCommand('journalctl -u mihomo --no-pager -n 50');
|
||||
return sendJson(res, { logs });
|
||||
} catch (e) {
|
||||
return sendError(res, e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/config - Get current config
|
||||
if (apiPath === '/api/config' && method === 'GET') {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user