Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0c54af8d26 | |||
| f7722be539 | |||
| 779f42c98c | |||
| 525620b0f8 | |||
| ebc8d687d0 |
@@ -8,7 +8,7 @@ import os
|
||||
import re
|
||||
import json
|
||||
import subprocess
|
||||
from flask import jsonify
|
||||
from flask import jsonify, request
|
||||
from utils import log_message
|
||||
from config import PROJECTS_FILE
|
||||
|
||||
@@ -71,13 +71,10 @@ def _get_listening_ports():
|
||||
port = int(port_match.group(1))
|
||||
|
||||
pid_match = re.search(r'pid=(\d+)', line)
|
||||
if not pid_match:
|
||||
continue
|
||||
pid = int(pid_match.group(1))
|
||||
pid = int(pid_match.group(1)) if pid_match else None
|
||||
|
||||
# 提取进程名
|
||||
proc_match = re.search(r'users:\(\(\"([^\"]+)\"', line)
|
||||
proc_name = proc_match.group(1) if proc_match else 'unknown'
|
||||
proc_name = proc_match.group(1) if proc_match else '未知进程'
|
||||
|
||||
services.append({
|
||||
'port': port,
|
||||
@@ -165,7 +162,7 @@ def register_discovery_routes(app):
|
||||
@app.route('/api/discovery/services')
|
||||
def api_discovery_services():
|
||||
"""扫描本机未注册的新服务"""
|
||||
# 1. 获取已知端口
|
||||
# 1. 收集已知端口
|
||||
known_ports = set()
|
||||
try:
|
||||
with open(PROJECTS_FILE, 'r', encoding='utf-8') as f:
|
||||
@@ -179,7 +176,16 @@ def register_discovery_routes(app):
|
||||
except:
|
||||
pass
|
||||
|
||||
# 2. 获取 Docker 容器端口
|
||||
# 2. 前端传入的额外排除端口(自定义服务等)
|
||||
exclude_str = request.args.get('exclude_ports', '')
|
||||
if exclude_str:
|
||||
try:
|
||||
for p in exclude_str.split(','):
|
||||
known_ports.add(int(p.strip()))
|
||||
except:
|
||||
pass
|
||||
|
||||
# 3. 获取 Docker 容器端口
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker', 'ps', '--format', '{{.Ports}}'],
|
||||
@@ -210,10 +216,16 @@ def register_discovery_routes(app):
|
||||
continue
|
||||
seen_ports.add(port)
|
||||
|
||||
cmdline = _get_process_cmdline(pid)
|
||||
cwd = _get_process_cwd(pid)
|
||||
name = _guess_service_name(pid, svc['process'], cmdline)
|
||||
svc_type = _guess_service_type(cmdline)
|
||||
if pid:
|
||||
cmdline = _get_process_cmdline(pid)
|
||||
cwd = _get_process_cwd(pid)
|
||||
name = _guess_service_name(pid, svc['process'], cmdline)
|
||||
svc_type = _guess_service_type(cmdline)
|
||||
else:
|
||||
cmdline = ''
|
||||
cwd = ''
|
||||
name = svc['process']
|
||||
svc_type = 'unknown'
|
||||
|
||||
undiscovered.append({
|
||||
'port': port,
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
.history-item.toggle { border-left-color: #8b5cf6; }
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 50; display: flex; align-items: center; justify-content: center; }
|
||||
.modal-content { max-width: 600px; width: 90%; max-height: 80vh; overflow: auto; }
|
||||
.section-header { cursor: pointer; user-select: none; }
|
||||
.section-header:hover { opacity: 0.85; }
|
||||
.chevron { display: inline-block; transition: transform 0.2s ease; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen text-gray-100">
|
||||
@@ -555,20 +558,20 @@
|
||||
</div>
|
||||
|
||||
<!-- 容器列表 -->
|
||||
<h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2">
|
||||
<i class="ri-computer-line text-blue-400"></i> 容器列表
|
||||
<h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2 section-header" onclick="toggleSection('docker-containers-grid', this.querySelector('.chevron'))">
|
||||
<i class="ri-arrow-down-s-line chevron"></i><i class="ri-computer-line text-blue-400"></i> 容器列表
|
||||
<span id="dockerContainerCount" class="text-xs text-gray-500"></span>
|
||||
</h3>
|
||||
<div id="dockerContainersList" class="space-y-2 mb-6">
|
||||
<div id="docker-containers-grid" class="space-y-2 mb-6">
|
||||
<div class="text-gray-500 text-sm py-4 text-center">加载中...</div>
|
||||
</div>
|
||||
|
||||
<!-- 镜像列表 -->
|
||||
<h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2">
|
||||
<i class="ri-stack-line text-purple-400"></i> 镜像列表
|
||||
<h3 class="text-sm font-semibold text-gray-300 mb-2 flex items-center gap-2 section-header" onclick="toggleSection('docker-images-grid', this.querySelector('.chevron'))">
|
||||
<i class="ri-arrow-down-s-line chevron"></i><i class="ri-stack-line text-purple-400"></i> 镜像列表
|
||||
<span id="dockerImageCount" class="text-xs text-gray-500"></span>
|
||||
</h3>
|
||||
<div id="dockerImagesList" class="space-y-1">
|
||||
<div id="docker-images-grid" class="space-y-1">
|
||||
<div class="text-gray-500 text-sm py-4 text-center">加载中...</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -929,11 +932,46 @@
|
||||
// Tab 切换
|
||||
// 系统资源监控变量(必须先声明)
|
||||
let realtimeTimer = null;
|
||||
let systemRefreshTimer = null;
|
||||
let realtimeInterval = 2; // 秒
|
||||
let lastAlertTime = 0; // 上次警告时间
|
||||
let thresholds = { cpu: 80, cpu_temp: 80, memory: 85, disk: 90, interval: 60 };
|
||||
let desktopNotifyEnabled = false;
|
||||
|
||||
// ==================== 折叠/展开 ====================
|
||||
function getCollapsedState() {
|
||||
const stored = localStorage.getItem('collapsedSections');
|
||||
return stored ? JSON.parse(stored) : {};
|
||||
}
|
||||
function saveCollapsedState(state) {
|
||||
localStorage.setItem('collapsedSections', JSON.stringify(state));
|
||||
}
|
||||
function toggleSection(sectionId, chevronEl) {
|
||||
const grid = document.getElementById(sectionId);
|
||||
if (!grid) return;
|
||||
const state = getCollapsedState();
|
||||
if (grid.classList.contains('hidden')) {
|
||||
grid.classList.remove('hidden');
|
||||
if (chevronEl) chevronEl.style.transform = 'rotate(0deg)';
|
||||
state[sectionId] = false;
|
||||
} else {
|
||||
grid.classList.add('hidden');
|
||||
if (chevronEl) chevronEl.style.transform = 'rotate(-90deg)';
|
||||
state[sectionId] = true;
|
||||
}
|
||||
saveCollapsedState(state);
|
||||
}
|
||||
function applyCollapsedState(sectionId, gridEl, chevronEl) {
|
||||
const state = getCollapsedState();
|
||||
if (state[sectionId]) {
|
||||
gridEl.classList.add('hidden');
|
||||
if (chevronEl) chevronEl.style.transform = 'rotate(-90deg)';
|
||||
} else {
|
||||
gridEl.classList.remove('hidden');
|
||||
if (chevronEl) chevronEl.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
function switchTab(tab) {
|
||||
currentTab = tab;
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
@@ -957,6 +995,12 @@
|
||||
document.getElementById('processListSection').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 离开系统Tab时关闭非实时刷新定时器
|
||||
if (tab !== 'system' && systemRefreshTimer) {
|
||||
clearInterval(systemRefreshTimer);
|
||||
systemRefreshTimer = null;
|
||||
}
|
||||
|
||||
if (tab === 'cron') {
|
||||
loadCronTasks();
|
||||
}
|
||||
@@ -965,6 +1009,8 @@
|
||||
loadDesktopNotifySetting();
|
||||
renderEmailRules();
|
||||
loadSystemStats();
|
||||
// 启动系统资源定时刷新(按页面刷新间隔)
|
||||
startSystemRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1281,14 +1327,17 @@
|
||||
const toggle = document.getElementById('realtimeToggle');
|
||||
const indicator = document.getElementById('realtimeIndicator');
|
||||
|
||||
// 先停止系统定时刷新
|
||||
if (systemRefreshTimer) {
|
||||
clearInterval(systemRefreshTimer);
|
||||
systemRefreshTimer = null;
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
toggle.classList.add('active');
|
||||
indicator.classList.remove('hidden');
|
||||
// 立即加载一次
|
||||
loadSystemStats();
|
||||
// 启动定时器
|
||||
realtimeTimer = setInterval(loadSystemStats, realtimeInterval * 1000);
|
||||
// 如果进程列表开关也开启,则显示并加载
|
||||
if (document.getElementById('showProcessListCheck').checked) {
|
||||
document.getElementById('processListSection').classList.remove('hidden');
|
||||
loadTopProcesses();
|
||||
@@ -1297,14 +1346,28 @@
|
||||
toggle.classList.remove('active');
|
||||
indicator.classList.add('hidden');
|
||||
document.getElementById('processListSection').classList.add('hidden');
|
||||
// 停止定时器
|
||||
if (realtimeTimer) {
|
||||
clearInterval(realtimeTimer);
|
||||
realtimeTimer = null;
|
||||
}
|
||||
// 关闭实时后,按页面刷新间隔继续刷新系统资源
|
||||
startSystemRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
function startSystemRefresh() {
|
||||
// 先停掉旧定时器
|
||||
if (systemRefreshTimer) {
|
||||
clearInterval(systemRefreshTimer);
|
||||
systemRefreshTimer = null;
|
||||
}
|
||||
// 如果在实时监控模式则不启动(由realtimeTimer负责)
|
||||
if (document.getElementById('realtimeCheck').checked) return;
|
||||
// 按页面刷新间隔定时刷新
|
||||
const intervalSec = parseInt(document.getElementById('refreshInterval').value) || 30;
|
||||
systemRefreshTimer = setInterval(loadSystemStats, intervalSec * 1000);
|
||||
}
|
||||
|
||||
function toggleProcessList() {
|
||||
const checked = document.getElementById('showProcessListCheck').checked;
|
||||
const realtimeChecked = document.getElementById('realtimeCheck').checked;
|
||||
@@ -1419,23 +1482,38 @@
|
||||
const archivedProjs = projs.filter(p => activeStatus[p.id] === false);
|
||||
|
||||
if (activeProjs.length > 0) {
|
||||
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2"><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${activeProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">`;
|
||||
const secId = 'section-web-active';
|
||||
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${activeProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
|
||||
activeProjs.forEach(p => { html += renderProjectCard(p, true); });
|
||||
html += '</div></div>';
|
||||
}
|
||||
|
||||
if (archivedProjs.length > 0) {
|
||||
html += `<div class="mb-6 opacity-60"><h2 class="text-lg font-semibold text-gray-400 mb-3 flex items-center gap-2"><i class="ri-archive-line text-gray-400"></i>归档服务<span class="text-sm text-gray-500">(${archivedProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">`;
|
||||
const secId = 'section-web-archived';
|
||||
html += `<div class="mb-6 opacity-60"><h2 class="text-lg font-semibold text-gray-400 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="ri-archive-line text-gray-400"></i>归档服务<span class="text-sm text-gray-500">(${archivedProjs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
|
||||
archivedProjs.forEach(p => { html += renderProjectCard(p, false); });
|
||||
html += '</div></div>';
|
||||
}
|
||||
} else {
|
||||
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2"><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${projs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">`;
|
||||
const secId = 'section-' + type;
|
||||
html += `<div class="mb-6"><h2 class="text-lg font-semibold text-gray-300 mb-3 flex items-center gap-2 section-header" onclick="toggleSection('${secId}', this.querySelector('.chevron'))"><i class="ri-arrow-down-s-line chevron"></i><i class="${typeInfo.icon} text-${typeInfo.color}-400"></i>${typeInfo.name}<span class="text-sm text-gray-500">(${projs.length})</span></h2><div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3" id="${secId}">`;
|
||||
projs.forEach(p => { html += renderProjectCard(p, true); });
|
||||
html += '</div></div>';
|
||||
}
|
||||
}
|
||||
list.innerHTML = html;
|
||||
// 应用已保存的折叠状态
|
||||
setTimeout(() => {
|
||||
const state = getCollapsedState();
|
||||
for (const [secId, collapsed] of Object.entries(state)) {
|
||||
const grid = document.getElementById(secId);
|
||||
const chevron = grid?.parentElement?.querySelector('.chevron');
|
||||
if (grid && collapsed) {
|
||||
grid.classList.add('hidden');
|
||||
if (chevron) chevron.style.transform = 'rotate(-90deg)';
|
||||
}
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
function getActiveStatus() {
|
||||
@@ -2238,6 +2316,7 @@
|
||||
const newService = {
|
||||
id: 'custom_' + Date.now(),
|
||||
name: name,
|
||||
port: port || '',
|
||||
url: mainUrl,
|
||||
admin_url: adminUrl || null,
|
||||
description: desc || '自定义外部服务',
|
||||
@@ -2750,6 +2829,11 @@
|
||||
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, seconds * 1000);
|
||||
|
||||
// 如果当前在系统Tab且非实时模式,更新系统刷新间隔
|
||||
if (currentTab === 'system' && !document.getElementById('realtimeCheck').checked) {
|
||||
startSystemRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
@@ -2796,7 +2880,7 @@
|
||||
}
|
||||
|
||||
function renderDockerContainers(containers) {
|
||||
const list = document.getElementById('dockerContainersList');
|
||||
const list = document.getElementById('docker-containers-grid');
|
||||
if (containers.length === 0) {
|
||||
list.innerHTML = '<div class="text-gray-500 text-sm py-4 text-center">暂无容器</div>';
|
||||
return;
|
||||
@@ -2828,7 +2912,7 @@
|
||||
}
|
||||
|
||||
function renderDockerImages(images) {
|
||||
const list = document.getElementById('dockerImagesList');
|
||||
const list = document.getElementById('docker-images-grid');
|
||||
if (images.length === 0) {
|
||||
list.innerHTML = '<div class="text-gray-500 text-sm py-4 text-center">暂无镜像</div>';
|
||||
return;
|
||||
@@ -2886,7 +2970,12 @@
|
||||
list.innerHTML = '<div class="text-gray-400 text-xs text-center py-2"><i class="ri-loader-4-line animate-spin"></i> 扫描中...</div>';
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/discovery/services');
|
||||
// 收集前端已注册的自定义服务端口,传给后端排除
|
||||
const customServices = getCustomServices();
|
||||
const customPorts = customServices.map(s => parseInt(s.port)).filter(p => !isNaN(p));
|
||||
const excludeParam = customPorts.length > 0 ? `?exclude_ports=${customPorts.join(',')}` : '';
|
||||
|
||||
const res = await fetch(`/api/discovery/services${excludeParam}`);
|
||||
const data = await res.json();
|
||||
const services = data.services || [];
|
||||
document.getElementById('discoveryCount').textContent = `发现 ${services.length} 个`;
|
||||
@@ -2899,7 +2988,7 @@
|
||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
||||
<span class="text-xs text-cyan-400 font-mono">:${s.port}</span>
|
||||
<span class="text-gray-300 text-sm truncate">${s.suggested_name}</span>
|
||||
<span class="text-gray-500 text-xs">PID ${s.pid}</span>
|
||||
<span class="text-gray-500 text-xs">PID ${s.pid != null ? s.pid : '受权限限制'}</span>
|
||||
<span class="text-gray-500 text-xs truncate hidden md:inline">${(s.cmdline || '').substring(0, 60)}</span>
|
||||
</div>
|
||||
<button onclick="quickAddService(${s.port}, '${s.suggested_name.replace(/'/g, "\\'")}', '${s.suggested_type}', '${(s.cwd || '').replace(/'/g, "\\'")}')" class="btn bg-green-600/50 hover:bg-green-600 px-2 py-1 rounded text-xs shrink-0 ml-2">
|
||||
|
||||
24
watchdog.sh
Executable file
24
watchdog.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# project-panel 守护脚本 - 进程挂了自动重启
|
||||
SERVICE_DIR="/home/openclaw/.openclaw/workspace-hz4th_coder/works/project-panel"
|
||||
PYTHON="/home/hz1/miniconda3/envs/openclaw/bin/python3.12"
|
||||
PORT=16022
|
||||
LOG="$SERVICE_DIR/logs/watchdog.log"
|
||||
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 守护进程启动,监控端口 $PORT" >> "$LOG"
|
||||
|
||||
while true; do
|
||||
if ! ss -tlnp 2>/dev/null | grep -q ":$PORT "; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 检测到 $PORT 端口离线,重启服务..." >> "$LOG"
|
||||
cd "$SERVICE_DIR"
|
||||
mkdir -p logs
|
||||
nohup "$PYTHON" app.py > logs/app.log 2>&1 &
|
||||
sleep 3
|
||||
if ss -tlnp 2>/dev/null | grep -q ":$PORT "; then
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 服务重启成功" >> "$LOG"
|
||||
else
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] 服务重启失败!" >> "$LOG"
|
||||
fi
|
||||
fi
|
||||
sleep 10
|
||||
done
|
||||
Reference in New Issue
Block a user