Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 55cc408881 | |||
| ca7527918d | |||
| 7f7f71a286 | |||
| 8579d58890 | |||
| f8a32ab6be | |||
| 56975f9b0d | |||
| b2900febf9 | |||
| 17e946ae56 | |||
| c320066b63 | |||
| 854b6b6f82 | |||
| 6d8410ac28 |
848
app.py
848
app.py
@@ -1,8 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
项目服务管理面板 v2.1.0
|
||||
项目服务管理面板 v2.2.0
|
||||
端口: 19013
|
||||
修复: 后台链接使用 externalIp 替代 localhost
|
||||
新增: Web服务卡片添加自定义链接入口(+按钮)
|
||||
新增: 自定义新增外部web服务功能(新增服务按钮)
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -19,6 +20,16 @@ from flask import Flask, render_template_string, jsonify, request, g
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
# 系统资源监控
|
||||
try:
|
||||
import psutil
|
||||
import time
|
||||
HAS_PSUTIL = True
|
||||
# 网速计算用的上次数据
|
||||
_last_net_stats = {'bytes_sent': 0, 'bytes_recv': 0, 'time': 0}
|
||||
except ImportError:
|
||||
HAS_PSUTIL = False
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -693,6 +704,101 @@ def api_cron_sync():
|
||||
return jsonify({'message': f'同步完成,新增 {synced_count} 个任务'})
|
||||
|
||||
|
||||
# ==================== 系统资源监控 API ====================
|
||||
|
||||
@app.route('/api/system/stats')
|
||||
def api_system_stats():
|
||||
"""获取系统资源信息"""
|
||||
if not HAS_PSUTIL:
|
||||
return jsonify({'error': 'psutil未安装', 'available': False})
|
||||
|
||||
try:
|
||||
# CPU
|
||||
cpu_percent = psutil.cpu_percent(interval=0.5)
|
||||
cpu_count = psutil.cpu_count(logical=False) or psutil.cpu_count()
|
||||
cpu_count_logical = psutil.cpu_count(logical=True)
|
||||
|
||||
# 内存
|
||||
mem = psutil.virtual_memory()
|
||||
mem_total_gb = round(mem.total / (1024**3), 2)
|
||||
mem_used_gb = round(mem.used / (1024**3), 2)
|
||||
mem_available_gb = round(mem.available / (1024**3), 2)
|
||||
mem_percent = mem.percent
|
||||
|
||||
# 磁盘
|
||||
disk = psutil.disk_usage('/')
|
||||
disk_total_gb = round(disk.total / (1024**3), 2)
|
||||
disk_used_gb = round(disk.used / (1024**3), 2)
|
||||
disk_free_gb = round(disk.free / (1024**3), 2)
|
||||
disk_percent = disk.percent
|
||||
|
||||
# 网络流量和网速计算
|
||||
net = psutil.net_io_counters()
|
||||
current_time = time.time()
|
||||
|
||||
# 计算网速 (bytes/s)
|
||||
time_diff = current_time - _last_net_stats['time'] if _last_net_stats['time'] > 0 else 1
|
||||
sent_speed = (net.bytes_sent - _last_net_stats['bytes_sent']) / time_diff if time_diff > 0 else 0
|
||||
recv_speed = (net.bytes_recv - _last_net_stats['bytes_recv']) / time_diff if time_diff > 0 else 0
|
||||
|
||||
# 保存当前数据用于下次计算
|
||||
_last_net_stats['bytes_sent'] = net.bytes_sent
|
||||
_last_net_stats['bytes_recv'] = net.bytes_recv
|
||||
_last_net_stats['time'] = current_time
|
||||
|
||||
# 转换为 KB/s 或 MB/s
|
||||
sent_speed_kb = sent_speed / 1024
|
||||
recv_speed_kb = recv_speed / 1024
|
||||
sent_speed_display = f"{sent_speed_kb:.1f} KB/s" if sent_speed_kb < 1024 else f"{sent_speed_kb/1024:.2f} MB/s"
|
||||
recv_speed_display = f"{recv_speed_kb:.1f} KB/s" if recv_speed_kb < 1024 else f"{recv_speed_kb/1024:.2f} MB/s"
|
||||
|
||||
net_sent_mb = round(net.bytes_sent / (1024**2), 2)
|
||||
net_recv_mb = round(net.bytes_recv / (1024**2), 2)
|
||||
|
||||
# 系统启动时间
|
||||
boot_time = datetime.fromtimestamp(psutil.boot_time()).strftime('%Y-%m-%d %H:%M:%S')
|
||||
uptime_seconds = int(datetime.now().timestamp() - psutil.boot_time())
|
||||
uptime_hours = uptime_seconds // 3600
|
||||
uptime_minutes = (uptime_seconds % 3600) // 60
|
||||
|
||||
# 进程数
|
||||
process_count = len(psutil.pids())
|
||||
|
||||
return jsonify({
|
||||
'available': True,
|
||||
'cpu': {
|
||||
'percent': cpu_percent,
|
||||
'count': cpu_count,
|
||||
'logical': cpu_count_logical
|
||||
},
|
||||
'memory': {
|
||||
'total_gb': mem_total_gb,
|
||||
'used_gb': mem_used_gb,
|
||||
'available_gb': mem_available_gb,
|
||||
'percent': mem_percent
|
||||
},
|
||||
'disk': {
|
||||
'total_gb': disk_total_gb,
|
||||
'used_gb': disk_used_gb,
|
||||
'free_gb': disk_free_gb,
|
||||
'percent': disk_percent
|
||||
},
|
||||
'network': {
|
||||
'sent_mb': net_sent_mb,
|
||||
'recv_mb': net_recv_mb,
|
||||
'sent_speed': sent_speed_display,
|
||||
'recv_speed': recv_speed_display
|
||||
},
|
||||
'system': {
|
||||
'boot_time': boot_time,
|
||||
'uptime': f'{uptime_hours}小时{uptime_minutes}分钟',
|
||||
'process_count': process_count
|
||||
}
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': str(e), 'available': False})
|
||||
|
||||
|
||||
def guess_cron_name(command):
|
||||
"""从命令推断任务名称"""
|
||||
keywords = {
|
||||
@@ -934,7 +1040,7 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>项目服务管理面板 v2.1</title>
|
||||
<title>项目服务管理面板 v2.8</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
|
||||
@@ -958,6 +1064,32 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
.nav-btn:hover { background: #475569; transform: scale(1.1); }
|
||||
.nav-btn i { font-size: 20px; color: #94a3b8; }
|
||||
.nav-btn:hover i { color: #f1f5f9; }
|
||||
.filter-bar { position: sticky; top: 0; z-index: 50; display: flex; gap: 8px; padding: 12px 0; background: #0f172a; margin-bottom: 16px; flex-wrap: wrap; }
|
||||
.filter-bar-btn { display: flex; align-items: center; gap: 6px; padding: 8px 16px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; color: #94a3b8; font-size: 14px; cursor: pointer; transition: all 0.2s; }
|
||||
.filter-bar-btn:hover { background: #334155; color: #f1f5f9; }
|
||||
.filter-bar-btn.active { background: #3b82f6; border-color: #3b82f6; color: #fff; }
|
||||
.filter-bar-btn.add-btn { background: #22c55e; border-color: #22c55e; color: #fff; }
|
||||
.filter-bar-btn.add-btn:hover { background: #16a34a; }
|
||||
.realtime-toggle { display: flex; align-items: center; gap: 8px; padding: 8px 16px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; }
|
||||
.realtime-toggle.active { background: #22c55e/20; border-color: #22c55e; }
|
||||
.speed-badge { display: inline-flex; align-items: center; gap: 4px; padding: 4px 12px; border-radius: 6px; background: #334155; font-size: 13px; }
|
||||
.speed-badge.upload { color: #f97316; }
|
||||
.speed-badge.download { color: #3b82f6; }
|
||||
.threshold-section { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 16px; margin-bottom: 16px; }
|
||||
.threshold-item { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid #334155; }
|
||||
.threshold-item:last-child { border-bottom: none; }
|
||||
.threshold-label { flex: 1; display: flex; align-items: center; gap: 8px; }
|
||||
.threshold-input { width: 60px; background: #334155; border: 1px solid #475569; border-radius: 6px; padding: 4px 8px; color: #f1f5f9; text-align: center; }
|
||||
.threshold-input:focus { outline: none; border-color: #3b82f6; }
|
||||
.threshold-icon { font-size: 18px; }
|
||||
.threshold-warning { color: #f97316; }
|
||||
.alert-popup { position: fixed; top: 20px; right: 20px; z-index: 1000; background: linear-gradient(135deg, #dc2626 0%, #b91c1c 100%); border-radius: 12px; padding: 16px 20px; color: white; box-shadow: 0 10px 40px rgba(0,0,0,0.3); animation: slideIn 0.3s ease-out; max-width: 320px; }
|
||||
.alert-popup .alert-title { font-weight: bold; font-size: 16px; margin-bottom: 8px; display: flex; align-items: center; gap: 8px; }
|
||||
.alert-popup .alert-content { font-size: 14px; }
|
||||
.alert-popup .alert-close { position: absolute; top: 8px; right: 12px; cursor: pointer; opacity: 0.7; }
|
||||
.alert-popup .alert-close:hover { opacity: 1; }
|
||||
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
|
||||
@keyframes slideOut { from { transform: translateX(0); opacity: 1; } to { transform: translateX(100%); opacity: 0; } }
|
||||
.tab-btn { border-bottom: 2px solid transparent; }
|
||||
.tab-btn.active { border-bottom-color: #3b82f6; }
|
||||
.cron-card { transition: all 0.2s; }
|
||||
@@ -1009,6 +1141,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
<button onclick="switchTab('projects')" class="tab-btn active px-4 py-2 text-gray-300 hover:text-white" data-tab="projects">
|
||||
<i class="ri-apps-line"></i> 项目服务
|
||||
</button>
|
||||
<button onclick="switchTab('system')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="system">
|
||||
<i class="ri-dashboard-2-line"></i> 系统资源
|
||||
</button>
|
||||
<button onclick="switchTab('cron')" class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="cron">
|
||||
<i class="ri-timer-line"></i> Cron 管理
|
||||
</button>
|
||||
@@ -1056,12 +1191,14 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选器 -->
|
||||
<div class="flex gap-2 mb-4">
|
||||
<button onclick="filterType('all')" class="filter-btn px-3 py-1 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm" data-type="all">全部</button>
|
||||
<button onclick="filterType('web')" class="filter-btn px-3 py-1 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm" data-type="web">Web服务</button>
|
||||
<button onclick="filterType('cli')" class="filter-btn px-3 py-1 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm" data-type="cli">CLI工具</button>
|
||||
<button onclick="filterType('extension')" class="filter-btn px-3 py-1 rounded-lg bg-gray-700 hover:bg-gray-600 text-sm" data-type="extension">插件</button>
|
||||
<!-- 筛选器(顶部固定) -->
|
||||
<div class="filter-bar">
|
||||
<button onclick="filterType('all')" class="filter-bar-btn active" data-type="all"><i class="ri-apps-line"></i> 全部</button>
|
||||
<button onclick="filterType('web')" class="filter-bar-btn" data-type="web"><i class="ri-server-line"></i> Web服务</button>
|
||||
<button onclick="filterType('custom')" class="filter-bar-btn" data-type="custom"><i class="ri-global-line"></i> 自定义</button>
|
||||
<button onclick="filterType('cli')" class="filter-bar-btn" data-type="cli"><i class="ri-terminal-box-line"></i> CLI工具</button>
|
||||
<button onclick="filterType('extension')" class="filter-bar-btn" data-type="extension"><i class="ri-chrome-line"></i> 插件</button>
|
||||
<button onclick="showAddServiceModal()" class="filter-bar-btn add-btn"><i class="ri-add-circle-line"></i> 新增服务</button>
|
||||
</div>
|
||||
|
||||
<!-- 项目列表 -->
|
||||
@@ -1070,6 +1207,178 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 系统资源 Tab -->
|
||||
<div id="systemTab" class="hidden">
|
||||
<!-- 系统概览 -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="card rounded-xl p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">系统运行时间</p>
|
||||
<p id="sysUptime" class="text-lg font-bold text-green-400">-</p>
|
||||
</div>
|
||||
<i class="ri-time-line text-3xl text-green-400 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-xl p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">启动时间</p>
|
||||
<p id="sysBootTime" class="text-lg font-bold text-blue-400">-</p>
|
||||
</div>
|
||||
<i class="ri-calendar-line text-3xl text-blue-400 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-xl p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">进程数</p>
|
||||
<p id="sysProcessCount" class="text-lg font-bold text-yellow-400">-</p>
|
||||
</div>
|
||||
<i class="ri-terminal-line text-3xl text-yellow-400 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card rounded-xl p-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm">CPU核心</p>
|
||||
<p id="sysCpuCount" class="text-lg font-bold text-purple-400">-</p>
|
||||
</div>
|
||||
<i class="ri-cpu-line text-3xl text-purple-400 opacity-50"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CPU & 内存 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="card rounded-xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg flex items-center gap-2"><i class="ri-cpu-line text-blue-400"></i> CPU 使用率</h3>
|
||||
<span id="cpuPercent" class="text-2xl font-bold text-blue-400">-</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-4 mb-2">
|
||||
<div id="cpuBar" class="bg-blue-500 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">物理核心: <span id="cpuPhysical">-</span> | 逻辑核心: <span id="cpuLogical">-</span></p>
|
||||
</div>
|
||||
<div class="card rounded-xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg flex items-center gap-2"><i class="ri-memory-card-line text-green-400"></i> 内存使用</h3>
|
||||
<span id="memPercent" class="text-2xl font-bold text-green-400">-</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-4 mb-2">
|
||||
<div id="memBar" class="bg-green-500 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">已用: <span id="memUsed">-</span> GB / 总量: <span id="memTotal">-</span> GB | 可用: <span id="memAvailable">-</span> GB</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 磁盘 & 网络 -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
|
||||
<div class="card rounded-xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg flex items-center gap-2"><i class="ri-hard-drive-2-line text-orange-400"></i> 磁盘使用</h3>
|
||||
<span id="diskPercent" class="text-2xl font-bold text-orange-400">-</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-700 rounded-full h-4 mb-2">
|
||||
<div id="diskBar" class="bg-orange-500 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
<p class="text-gray-400 text-sm">已用: <span id="diskUsed">-</span> GB / 总量: <span id="diskTotal">-</span> GB | 剩余: <span id="diskFree">-</span> GB</p>
|
||||
</div>
|
||||
<div class="card rounded-xl p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg flex items-center gap-2"><i class="ri-wifi-line text-cyan-400"></i> 网络流量</h3>
|
||||
</div>
|
||||
<!-- 实时网速 -->
|
||||
<div class="flex gap-3 mb-4">
|
||||
<span class="speed-badge upload"><i class="ri-upload-line"></i> ↑ <span id="netSentSpeed">-</span></span>
|
||||
<span class="speed-badge download"><i class="ri-download-line"></i> ↓ <span id="netRecvSpeed">-</span></span>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm mb-1">累计发送</p>
|
||||
<p id="netSent" class="text-xl font-bold text-cyan-400">-</p>
|
||||
<p class="text-gray-500 text-xs">MB</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-gray-400 text-sm mb-1">累计接收</p>
|
||||
<p id="netRecv" class="text-xl font-bold text-purple-400">-</p>
|
||||
<p class="text-gray-500 text-xs">MB</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 阈值监控设置 -->
|
||||
<div class="threshold-section">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-bold text-gray-200 flex items-center gap-2">
|
||||
<i class="ri-alarm-warning-line text-orange-400"></i> 监控阈值设置
|
||||
</h3>
|
||||
<span class="text-xs text-gray-500">超过阈值将弹窗警告</span>
|
||||
</div>
|
||||
<div class="threshold-item">
|
||||
<div class="threshold-label">
|
||||
<i class="ri-cpu-line threshold-icon text-blue-400"></i>
|
||||
<span class="text-gray-300 text-sm">CPU 使用率</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" id="thresholdCpu" class="threshold-input" min="0" max="100" value="80" onchange="saveThresholds()">
|
||||
<span class="text-gray-500 text-xs">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="threshold-item">
|
||||
<div class="threshold-label">
|
||||
<i class="ri-memory-card-line threshold-icon text-green-400"></i>
|
||||
<span class="text-gray-300 text-sm">内存使用率</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" id="thresholdMemory" class="threshold-input" min="0" max="100" value="85" onchange="saveThresholds()">
|
||||
<span class="text-gray-500 text-xs">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="threshold-item">
|
||||
<div class="threshold-label">
|
||||
<i class="ri-hard-drive-2-line threshold-icon text-orange-400"></i>
|
||||
<span class="text-gray-300 text-sm">磁盘使用率</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" id="thresholdDisk" class="threshold-input" min="0" max="100" value="90" onchange="saveThresholds()">
|
||||
<span class="text-gray-500 text-xs">%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="threshold-item">
|
||||
<div class="threshold-label">
|
||||
<i class="ri-timer-line threshold-icon text-purple-400"></i>
|
||||
<span class="text-gray-300 text-sm">警告间隔</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<input type="number" id="thresholdInterval" class="threshold-input" min="10" max="300" value="60" onchange="saveThresholds()">
|
||||
<span class="text-gray-500 text-xs">秒</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制按钮 -->
|
||||
<div class="flex gap-3 mb-4 items-center">
|
||||
<button onclick="loadSystemStats()" class="btn bg-blue-600 hover:bg-blue-700 px-4 py-2 rounded-lg flex items-center gap-2">
|
||||
<i class="ri-refresh-line"></i> 刷新数据
|
||||
</button>
|
||||
<div class="realtime-toggle" id="realtimeToggle">
|
||||
<input type="checkbox" id="realtimeCheck" onchange="toggleRealtime()" class="w-4 h-4 cursor-pointer">
|
||||
<label for="realtimeCheck" class="text-gray-300 text-sm cursor-pointer">实时监控</label>
|
||||
<select id="realtimeIntervalSelect" class="bg-gray-700 text-gray-200 px-2 py-1 rounded text-xs ml-2 cursor-pointer" onchange="updateRealtimeInterval()">
|
||||
<option value="1">1秒</option>
|
||||
<option value="2" selected>2秒</option>
|
||||
<option value="3">3秒</option>
|
||||
<option value="5">5秒</option>
|
||||
<option value="10">10秒</option>
|
||||
</select>
|
||||
<span id="realtimeIndicator" class="text-xs text-green-400 ml-1 hidden">●</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Cron 管理 Tab -->
|
||||
<div id="cronTab" class="hidden">
|
||||
<!-- Cron 统计 -->
|
||||
@@ -1218,6 +1527,96 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 自定义链接模态框 -->
|
||||
<div id="linkModal" class="modal-overlay hidden">
|
||||
<div class="card rounded-xl modal-content p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 id="linkModalTitle" class="font-bold text-lg">添加自定义链接</h3>
|
||||
<button onclick="closeLinkModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
||||
</div>
|
||||
|
||||
<form id="linkForm" onsubmit="saveCustomLink(event)">
|
||||
<input type="hidden" id="linkProjectId">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">链接名称 *</label>
|
||||
<input type="text" id="linkName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:API文档、管理后台">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">端口(使用统一IP)</label>
|
||||
<input type="text" id="linkPort" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:19001" onchange="updateLinkPreview()">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">路径(可选)</label>
|
||||
<input type="text" id="linkPath" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:/admin /api/docs" onchange="updateLinkPreview()">
|
||||
<p id="linkPreview" class="text-sm text-cyan-400 mt-1"></p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-400 text-sm mb-1">完整URL(不填则用端口生成)</label>
|
||||
<input type="text" id="linkFullUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://192.168.2.17:19000/docs">
|
||||
</div>
|
||||
|
||||
<div id="existingLinks" class="mb-4"></div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" onclick="closeLinkModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
|
||||
<button type="submit" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg">添加</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增自定义服务模态框 -->
|
||||
<div id="addServiceModal" class="modal-overlay hidden">
|
||||
<div class="card rounded-xl modal-content p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="font-bold text-lg">新增自定义服务</h3>
|
||||
<button onclick="closeAddServiceModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
||||
</div>
|
||||
|
||||
<form id="addServiceForm" onsubmit="saveCustomService(event)">
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">服务名称 *</label>
|
||||
<input type="text" id="serviceName" required class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:外部API、监控面板">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">端口(使用统一IP)</label>
|
||||
<input type="text" id="servicePort" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:8080" onchange="updateServicePreview()">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">路径(可选)</label>
|
||||
<input type="text" id="servicePath" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:/admin" onchange="updateServicePreview()">
|
||||
<p id="servicePreview" class="text-sm text-cyan-400 mt-1"></p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">完整URL(不填则用端口生成)</label>
|
||||
<input type="text" id="serviceFullUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://external.com:8080">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">后台URL(可选)</label>
|
||||
<input type="text" id="serviceAdminUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://external.com:8080/admin">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">描述</label>
|
||||
<input type="text" id="serviceDesc" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="简要描述">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" onclick="closeAddServiceModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
|
||||
<button type="submit" class="btn bg-green-600 hover:bg-green-700 px-4 py-2 rounded-lg">添加</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导航按钮 -->
|
||||
<div class="nav-buttons">
|
||||
<button onclick="scrollToTop()" class="nav-btn" title="回到顶部"><i class="ri-arrow-up-line"></i></button>
|
||||
@@ -1242,11 +1641,184 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
btn.classList.toggle('active', btn.dataset.tab === tab);
|
||||
});
|
||||
document.getElementById('projectsTab').classList.toggle('hidden', tab !== 'projects');
|
||||
document.getElementById('systemTab').classList.toggle('hidden', tab !== 'system');
|
||||
document.getElementById('cronTab').classList.toggle('hidden', tab !== 'cron');
|
||||
|
||||
// 切换Tab时关闭实时监控
|
||||
if (tab !== 'system' && realtimeTimer) {
|
||||
clearInterval(realtimeTimer);
|
||||
realtimeTimer = null;
|
||||
document.getElementById('realtimeCheck').checked = false;
|
||||
document.getElementById('realtimeToggle').classList.remove('active');
|
||||
document.getElementById('realtimeIndicator').classList.add('hidden');
|
||||
}
|
||||
|
||||
if (tab === 'cron') {
|
||||
loadCronTasks();
|
||||
}
|
||||
if (tab === 'system') {
|
||||
loadThresholds();
|
||||
loadSystemStats();
|
||||
}
|
||||
}
|
||||
|
||||
// 系统资源加载
|
||||
let realtimeTimer = null;
|
||||
let realtimeInterval = 2; // 秒
|
||||
let lastAlertTime = 0; // 上次警告时间
|
||||
let thresholds = { cpu: 80, memory: 85, disk: 90, interval: 60 };
|
||||
|
||||
// 加载阈值设置
|
||||
function loadThresholds() {
|
||||
const saved = localStorage.getItem('systemThresholds');
|
||||
if (saved) {
|
||||
thresholds = JSON.parse(saved);
|
||||
document.getElementById('thresholdCpu').value = thresholds.cpu || 80;
|
||||
document.getElementById('thresholdMemory').value = thresholds.memory || 85;
|
||||
document.getElementById('thresholdDisk').value = thresholds.disk || 90;
|
||||
document.getElementById('thresholdInterval').value = thresholds.interval || 60;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存阈值设置
|
||||
function saveThresholds() {
|
||||
thresholds.cpu = parseInt(document.getElementById('thresholdCpu').value) || 80;
|
||||
thresholds.memory = parseInt(document.getElementById('thresholdMemory').value) || 85;
|
||||
thresholds.disk = parseInt(document.getElementById('thresholdDisk').value) || 90;
|
||||
thresholds.interval = parseInt(document.getElementById('thresholdInterval').value) || 60;
|
||||
localStorage.setItem('systemThresholds', JSON.stringify(thresholds));
|
||||
}
|
||||
|
||||
// 检查阈值并弹窗警告
|
||||
function checkThresholds(data) {
|
||||
const now = Date.now();
|
||||
const intervalMs = thresholds.interval * 1000;
|
||||
|
||||
// 检查是否超过间隔时间
|
||||
if (now - lastAlertTime < intervalMs) return;
|
||||
|
||||
const warnings = [];
|
||||
|
||||
if (data.cpu.percent >= thresholds.cpu) {
|
||||
warnings.push(`CPU使用率 ${data.cpu.percent.toFixed(1)}% 超过阈值 ${thresholds.cpu}%`);
|
||||
}
|
||||
if (data.memory.percent >= thresholds.memory) {
|
||||
warnings.push(`内存使用率 ${data.memory.percent.toFixed(1)}% 超过阈值 ${thresholds.memory}%`);
|
||||
}
|
||||
if (data.disk.percent >= thresholds.disk) {
|
||||
warnings.push(`磁盘使用率 ${data.disk.percent.toFixed(1)}% 超过阈值 ${thresholds.disk}%`);
|
||||
}
|
||||
|
||||
if (warnings.length > 0) {
|
||||
lastAlertTime = now;
|
||||
showAlertPopup(warnings);
|
||||
}
|
||||
}
|
||||
|
||||
// 显示警告弹窗
|
||||
function showAlertPopup(warnings) {
|
||||
// 移除已有弹窗
|
||||
const existing = document.querySelector('.alert-popup');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'alert-popup';
|
||||
popup.innerHTML = `
|
||||
<span class="alert-close" onclick="this.parentElement.remove()"><i class="ri-close-line"></i></span>
|
||||
<div class="alert-title"><i class="ri-alarm-warning-line"></i> 资源告警</div>
|
||||
<div class="alert-content">${warnings.map(w => `<p>⚠️ ${w}</p>`).join('')}</div>
|
||||
`;
|
||||
document.body.appendChild(popup);
|
||||
|
||||
// 自动关闭(10秒后)
|
||||
setTimeout(() => {
|
||||
popup.style.animation = 'slideOut 0.3s ease-out forwards';
|
||||
setTimeout(() => popup.remove(), 300);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
function updateRealtimeInterval() {
|
||||
realtimeInterval = parseInt(document.getElementById('realtimeIntervalSelect').value) || 2;
|
||||
// 如果正在监控,重新启动定时器
|
||||
if (realtimeTimer) {
|
||||
clearInterval(realtimeTimer);
|
||||
realtimeTimer = setInterval(loadSystemStats, realtimeInterval * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSystemStats() {
|
||||
try {
|
||||
const res = await fetch('/api/system/stats');
|
||||
const data = await res.json();
|
||||
|
||||
if (!data.available) {
|
||||
document.getElementById('sysUptime').textContent = 'psutil未安装';
|
||||
document.getElementById('sysBootTime').textContent = '-';
|
||||
return;
|
||||
}
|
||||
|
||||
// 系统概览
|
||||
document.getElementById('sysUptime').textContent = data.system.uptime;
|
||||
document.getElementById('sysBootTime').textContent = data.system.boot_time;
|
||||
document.getElementById('sysProcessCount').textContent = data.system.process_count;
|
||||
document.getElementById('sysCpuCount').textContent = data.cpu.logical + '核';
|
||||
|
||||
// CPU
|
||||
document.getElementById('cpuPercent').textContent = data.cpu.percent.toFixed(1) + '%';
|
||||
document.getElementById('cpuBar').style.width = data.cpu.percent + '%';
|
||||
document.getElementById('cpuPhysical').textContent = data.cpu.count;
|
||||
document.getElementById('cpuLogical').textContent = data.cpu.logical;
|
||||
|
||||
// 内存
|
||||
document.getElementById('memPercent').textContent = data.memory.percent.toFixed(1) + '%';
|
||||
document.getElementById('memBar').style.width = data.memory.percent + '%';
|
||||
document.getElementById('memUsed').textContent = data.memory.used_gb;
|
||||
document.getElementById('memTotal').textContent = data.memory.total_gb;
|
||||
document.getElementById('memAvailable').textContent = data.memory.available_gb;
|
||||
|
||||
// 磁盘
|
||||
document.getElementById('diskPercent').textContent = data.disk.percent.toFixed(1) + '%';
|
||||
document.getElementById('diskBar').style.width = data.disk.percent + '%';
|
||||
document.getElementById('diskUsed').textContent = data.disk.used_gb;
|
||||
document.getElementById('diskTotal').textContent = data.disk.total_gb;
|
||||
document.getElementById('diskFree').textContent = data.disk.free_gb;
|
||||
|
||||
// 网络 - 累计流量
|
||||
document.getElementById('netSent').textContent = data.network.sent_mb;
|
||||
document.getElementById('netRecv').textContent = data.network.recv_mb;
|
||||
// 实时网速
|
||||
document.getElementById('netSentSpeed').textContent = data.network.sent_speed || '-';
|
||||
document.getElementById('netRecvSpeed').textContent = data.network.recv_speed || '-';
|
||||
|
||||
// 检查阈值
|
||||
checkThresholds(data);
|
||||
|
||||
} catch (e) {
|
||||
console.error('系统资源加载失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleRealtime() {
|
||||
const checked = document.getElementById('realtimeCheck').checked;
|
||||
const toggle = document.getElementById('realtimeToggle');
|
||||
const indicator = document.getElementById('realtimeIndicator');
|
||||
|
||||
if (checked) {
|
||||
toggle.classList.add('active');
|
||||
indicator.classList.remove('hidden');
|
||||
// 立即加载一次
|
||||
loadSystemStats();
|
||||
// 启动定时器
|
||||
realtimeTimer = setInterval(loadSystemStats, realtimeInterval * 1000);
|
||||
} else {
|
||||
toggle.classList.remove('active');
|
||||
indicator.classList.add('hidden');
|
||||
// 停止定时器
|
||||
if (realtimeTimer) {
|
||||
clearInterval(realtimeTimer);
|
||||
realtimeTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// IP 保存
|
||||
@@ -1265,6 +1837,13 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
renderProjects();
|
||||
updateStats();
|
||||
updateConnectionStatus(true);
|
||||
|
||||
// 同时检查系统资源阈值(主页也有效)
|
||||
loadThresholds();
|
||||
fetch('/api/system/stats').then(r => r.json()).then(sysData => {
|
||||
if (sysData.available) checkThresholds(sysData);
|
||||
}).catch(() => {});
|
||||
|
||||
} catch (e) {
|
||||
console.error('加载失败:', e);
|
||||
updateConnectionStatus(false);
|
||||
@@ -1286,16 +1865,20 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
|
||||
function filterType(type) {
|
||||
currentFilter = type;
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => {
|
||||
btn.classList.toggle('bg-blue-600', btn.dataset.type === type);
|
||||
btn.classList.toggle('bg-gray-700', btn.dataset.type !== type);
|
||||
document.querySelectorAll('.filter-bar-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.type === type);
|
||||
});
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function renderProjects() {
|
||||
const list = document.getElementById('projectsList');
|
||||
const filtered = currentFilter === 'all' ? projects : projects.filter(p => p.type === currentFilter);
|
||||
|
||||
// 合合自定义服务到项目列表
|
||||
const customServices = getCustomServices();
|
||||
const allProjects = [...projects, ...customServices];
|
||||
|
||||
const filtered = currentFilter === 'all' ? allProjects : allProjects.filter(p => p.type === currentFilter || (currentFilter === 'web' && p.isCustom));
|
||||
|
||||
if (filtered.length === 0) {
|
||||
list.innerHTML = '<div class="text-center py-12 text-gray-400"><i class="ri-folder-open-line text-4xl"></i><p class="mt-2">暂无项目</p></div>';
|
||||
@@ -1305,13 +1888,15 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
const activeStatus = getActiveStatus();
|
||||
const grouped = {};
|
||||
filtered.forEach(p => {
|
||||
if (!grouped[p.type]) grouped[p.type] = [];
|
||||
grouped[p.type].push(p);
|
||||
const groupType = p.isCustom ? 'custom' : p.type;
|
||||
if (!grouped[groupType]) grouped[groupType] = [];
|
||||
grouped[groupType].push(p);
|
||||
});
|
||||
|
||||
let html = '';
|
||||
const typeNames = {
|
||||
'web': { name: 'Web服务', icon: 'ri-server-line', color: 'blue' },
|
||||
'custom': { name: '自定义服务', icon: 'ri-global-line', color: 'cyan' },
|
||||
'cron': { name: 'Cron任务', icon: 'ri-time-line', color: 'yellow' },
|
||||
'cli': { name: 'CLI工具', icon: 'ri-terminal-box-line', color: 'purple' },
|
||||
'extension': { name: '浏览器插件', icon: 'ri-chrome-line', color: 'pink' }
|
||||
@@ -1357,6 +1942,27 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
}
|
||||
|
||||
function renderProjectCard(p, isActive = true) {
|
||||
// 自定义服务特殊处理
|
||||
if (p.isCustom) {
|
||||
return `
|
||||
<div class="card rounded-lg p-3 hover:border-cyan-500 transition-colors border-cyan-500/30">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="status-dot bg-cyan-400" style="box-shadow: 0 0 8px #22d3ee;" title="外部服务"></div>
|
||||
<h3 class="font-semibold text-sm truncate">${p.name}</h3>
|
||||
</div>
|
||||
<span class="text-xs text-cyan-400">外部</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-xs mb-2 truncate">${p.description || ''}</p>
|
||||
<div class="flex items-center gap-1 text-xs flex-wrap">
|
||||
<a href="${p.url}" target="_blank" class="px-2 py-0.5 rounded bg-cyan-500/20 text-cyan-400 hover:bg-cyan-500/30">访问</a>
|
||||
${p.admin_url ? `<a href="${p.admin_url}" target="_blank" class="text-yellow-400 hover:text-yellow-300 ml-1">后台</a>` : ''}
|
||||
<button onclick="deleteCustomService('${p.id}')" class="text-red-400 hover:text-red-300 ml-2" title="删除服务"><i class="ri-delete-bin-line"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const statusInfo = getStatusInfo(p.status?.status);
|
||||
|
||||
// 处理后台链接:将 localhost 替换为 externalIp
|
||||
@@ -1365,6 +1971,9 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
adminUrl = adminUrl.replace(/localhost/g, externalIp);
|
||||
}
|
||||
|
||||
// 获取自定义链接
|
||||
const customLinks = getCustomLinks(p.id);
|
||||
|
||||
return `
|
||||
<div class="card rounded-lg p-3 hover:border-gray-500 transition-colors">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
@@ -1375,13 +1984,15 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
<span class="text-xs ${statusInfo.textColor}">${statusInfo.text}</span>
|
||||
</div>
|
||||
${p.ports && p.ports.length > 0 ? `
|
||||
<div class="flex items-center gap-1 text-xs mb-2">
|
||||
<div class="flex items-center gap-1 text-xs mb-2 flex-wrap">
|
||||
${p.ports.map(port => {
|
||||
const portStatus = p.status?.ports?.[port];
|
||||
const isRunning = portStatus?.running;
|
||||
return `<a href="http://${externalIp}:${port}" target="_blank" class="px-2 py-0.5 rounded ${isRunning ? 'bg-green-500/20 text-green-400 hover:bg-green-500/30' : 'bg-red-500/20 text-red-400'}">${port}</a>`;
|
||||
}).join('')}
|
||||
${adminUrl ? `<a href="${adminUrl}" target="_blank" class="text-yellow-400 hover:text-yellow-300 ml-1">后台</a>` : ''}
|
||||
${customLinks.map(link => `<a href="${link.url}" target="_blank" class="text-cyan-400 hover:text-cyan-300 ml-1">${link.name}</a>`).join('')}
|
||||
<button onclick="showAddLinkModal('${p.id}', '${p.name}')" class="text-green-400 hover:text-green-300 ml-1" title="添加自定义链接"><i class="ri-add-line"></i></button>
|
||||
</div>
|
||||
` : ''}
|
||||
${p.type === 'web' ? `
|
||||
@@ -1401,6 +2012,211 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// ==================== 自定义链接管理 ====================
|
||||
|
||||
function getCustomLinks(projectId) {
|
||||
const stored = localStorage.getItem('customLinks_' + projectId);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
}
|
||||
|
||||
function saveCustomLinks(projectId, links) {
|
||||
localStorage.setItem('customLinks_' + projectId, JSON.stringify(links));
|
||||
}
|
||||
|
||||
function showAddLinkModal(projectId, projectName) {
|
||||
document.getElementById('linkModalTitle').textContent = `${projectName} - 添加链接`;
|
||||
document.getElementById('linkProjectId').value = projectId;
|
||||
document.getElementById('linkName').value = '';
|
||||
document.getElementById('linkPort').value = '';
|
||||
document.getElementById('linkPath').value = '';
|
||||
document.getElementById('linkFullUrl').value = '';
|
||||
document.getElementById('linkModal').classList.remove('hidden');
|
||||
|
||||
// 显示已有的自定义链接(供删除)
|
||||
const existingLinks = getCustomLinks(projectId);
|
||||
const existingList = document.getElementById('existingLinks');
|
||||
if (existingLinks.length > 0) {
|
||||
existingList.innerHTML = '<div class="text-gray-400 text-sm mb-2">已有链接:</div>' +
|
||||
existingLinks.map(link => `
|
||||
<div class="flex items-center justify-between bg-gray-700 px-2 py-1 rounded mb-1">
|
||||
<span class="text-cyan-400 text-sm">${link.name}: ${link.url}</span>
|
||||
<button onclick="deleteCustomLink('${projectId}', '${link.id}')" class="text-red-400 hover:text-red-300"><i class="ri-delete-bin-line"></i></button>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
existingList.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
function closeLinkModal() {
|
||||
document.getElementById('linkModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function saveCustomLink(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const projectId = document.getElementById('linkProjectId').value;
|
||||
const name = document.getElementById('linkName').value.trim();
|
||||
const port = document.getElementById('linkPort').value.trim();
|
||||
const path = document.getElementById('linkPath').value.trim();
|
||||
const fullUrl = document.getElementById('linkFullUrl').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('请输入链接名称');
|
||||
return;
|
||||
}
|
||||
|
||||
let url;
|
||||
if (fullUrl) {
|
||||
url = fullUrl;
|
||||
} else if (port) {
|
||||
url = `http://${externalIp}:${port}${path || ''}`;
|
||||
} else {
|
||||
alert('请输入端口或完整URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const links = getCustomLinks(projectId);
|
||||
const newLink = {
|
||||
id: Date.now().toString(),
|
||||
name: name,
|
||||
url: url
|
||||
};
|
||||
links.push(newLink);
|
||||
saveCustomLinks(projectId, links);
|
||||
|
||||
closeLinkModal();
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function deleteCustomLink(projectId, linkId) {
|
||||
if (!confirm('确定删除此链接?')) return;
|
||||
|
||||
const links = getCustomLinks(projectId);
|
||||
const filtered = links.filter(l => l.id !== linkId);
|
||||
saveCustomLinks(projectId, filtered);
|
||||
|
||||
// 更新模态框中的显示
|
||||
const existingLinks = getCustomLinks(projectId);
|
||||
const existingList = document.getElementById('existingLinks');
|
||||
if (existingLinks.length > 0) {
|
||||
existingList.innerHTML = '<div class="text-gray-400 text-sm mb-2">已有链接:</div>' +
|
||||
existingLinks.map(link => `
|
||||
<div class="flex items-center justify-between bg-gray-700 px-2 py-1 rounded mb-1">
|
||||
<span class="text-cyan-400 text-sm">${link.name}: ${link.url}</span>
|
||||
<button onclick="deleteCustomLink('${projectId}', '${link.id}')" class="text-red-400 hover:text-red-300"><i class="ri-delete-bin-line"></i></button>
|
||||
</div>
|
||||
`).join('');
|
||||
} else {
|
||||
existingList.innerHTML = '<div class="text-gray-500 text-sm">暂无自定义链接</div>';
|
||||
}
|
||||
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
// 自动生成URL预览
|
||||
function updateLinkPreview() {
|
||||
const port = document.getElementById('linkPort').value.trim();
|
||||
const path = document.getElementById('linkPath').value.trim();
|
||||
const preview = document.getElementById('linkPreview');
|
||||
|
||||
if (port) {
|
||||
preview.textContent = `预览: http://${externalIp}:${port}${path || ''}`;
|
||||
} else {
|
||||
preview.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 自定义服务管理 ====================
|
||||
|
||||
function getCustomServices() {
|
||||
const stored = localStorage.getItem('customServices');
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
}
|
||||
|
||||
function saveCustomServices(services) {
|
||||
localStorage.setItem('customServices', JSON.stringify(services));
|
||||
}
|
||||
|
||||
function showAddServiceModal() {
|
||||
document.getElementById('serviceName').value = '';
|
||||
document.getElementById('servicePort').value = '';
|
||||
document.getElementById('servicePath').value = '';
|
||||
document.getElementById('serviceFullUrl').value = '';
|
||||
document.getElementById('serviceAdminUrl').value = '';
|
||||
document.getElementById('serviceDesc').value = '';
|
||||
document.getElementById('servicePreview').textContent = '';
|
||||
document.getElementById('addServiceModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeAddServiceModal() {
|
||||
document.getElementById('addServiceModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
function updateServicePreview() {
|
||||
const port = document.getElementById('servicePort').value.trim();
|
||||
const path = document.getElementById('servicePath').value.trim();
|
||||
const preview = document.getElementById('servicePreview');
|
||||
|
||||
if (port) {
|
||||
preview.textContent = `预览: http://${externalIp}:${port}${path || ''}`;
|
||||
} else {
|
||||
preview.textContent = '';
|
||||
}
|
||||
}
|
||||
|
||||
function saveCustomService(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const name = document.getElementById('serviceName').value.trim();
|
||||
const port = document.getElementById('servicePort').value.trim();
|
||||
const path = document.getElementById('servicePath').value.trim();
|
||||
const fullUrl = document.getElementById('serviceFullUrl').value.trim();
|
||||
const adminUrl = document.getElementById('serviceAdminUrl').value.trim();
|
||||
const desc = document.getElementById('serviceDesc').value.trim();
|
||||
|
||||
if (!name) {
|
||||
alert('请输入服务名称');
|
||||
return;
|
||||
}
|
||||
|
||||
let mainUrl;
|
||||
if (fullUrl) {
|
||||
mainUrl = fullUrl;
|
||||
} else if (port) {
|
||||
mainUrl = `http://${externalIp}:${port}${path || ''}`;
|
||||
} else {
|
||||
alert('请输入端口或完整URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const services = getCustomServices();
|
||||
const newService = {
|
||||
id: 'custom_' + Date.now(),
|
||||
name: name,
|
||||
url: mainUrl,
|
||||
admin_url: adminUrl || null,
|
||||
description: desc || '自定义外部服务',
|
||||
type: 'custom',
|
||||
isCustom: true
|
||||
};
|
||||
services.push(newService);
|
||||
saveCustomServices(services);
|
||||
|
||||
closeAddServiceModal();
|
||||
loadProjects();
|
||||
}
|
||||
|
||||
function deleteCustomService(serviceId) {
|
||||
if (!confirm('确定删除此自定义服务?')) return;
|
||||
|
||||
const services = getCustomServices();
|
||||
const filtered = services.filter(s => s.id !== serviceId);
|
||||
saveCustomServices(filtered);
|
||||
|
||||
loadProjects();
|
||||
}
|
||||
|
||||
function getStatusInfo(status) {
|
||||
const map = {
|
||||
|
||||
12
cron_backups/crontab_20260420_163055.txt
Normal file
12
cron_backups/crontab_20260420_163055.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
*/30 * * * * openclaw agent --agent zuitoushang --message '执行下心跳任务' >> /home/xian/.openclaw/workspace-zuitoushang/works/heartbeat_logs/$(date +\%Y-\%m-\%d_\%H:\%M:\%S).log 2>&1
|
||||
0 * * * * /usr/bin/python3 /home/xian/.copaw/workspaces/default/active_skills/system-monitor/scripts/monitor.py --alert >> /home/xian/.copaw/workspaces/default/works/system-monitor.log 2>&1
|
||||
0 * * * * /home/xian/.openclaw/workspace-zuitoushang/scripts/cleanup-chrome-zombies.sh
|
||||
0 3 * * * /home/xian/.nvm/versions/node/v24.14.0/bin/node /home/xian/.openclaw/workspace-zuitoushang/scripts/daily-summary.js >> /home/xian/.openclaw/workspace-zuitoushang/works/daily-summary.log 2>&1
|
||||
0 * * * * /usr/bin/python3 /home/xian/.openclaw/workspace-zuitoushang/scripts/cpu-monitor.py >> /home/xian/.openclaw/workspace-zuitoushang/works/cpu-monitor.log 2>&1
|
||||
0 8-22 * * * /usr/bin/python3 /home/xian/.openclaw/workspace-zuitoushang/scripts/disk-monitor.py >> /home/xian/.openclaw/workspace-zuitoushang/works/disk-monitor.log 2>&1
|
||||
*/20 * * * * /usr/bin/python3 /home/xian/.openclaw/workspace-coder/works/service-monitor/monitor.py >> /home/xian/.openclaw/workspace-coder/works/service-monitor/monitor.log 2>&1
|
||||
0 17 * * 1-5 /usr/bin/python3 /home/xian/.openclaw/workspace-coder/works/board-monitor/board_monitor.py report >> /home/xian/.openclaw/workspace-coder/works/board-monitor/report.log 2>&1
|
||||
0 17 * * 1-5 /usr/bin/python3 /home/xian/.openclaw/common/stock_system/cron_daily_fetch.py >> /home/xian/.openclaw/common/stock_system/logs/daily_fetch.log 2>&1
|
||||
0 4 * * * /home/xian/.openclaw/workspace-coder/works/xian-favor/scripts/auto_backup.py >> /tmp/xian-favor-backup.log 2>&1
|
||||
0 12 * * * cd /home/xian/.openclaw/workspace-laoli && /home/xian/.openclaw/workspace-laoli/works/send_coder_stats.sh
|
||||
10 3 * * * openclaw agent --agent zuitoushang --message '总结并记忆昨天和你的会话内容' >> /home/xian/.openclaw/workspace-zuitoushang/works/daily-memory/$(date +\%Y-\%m-\%d).log 2>&1
|
||||
12
cron_backups/crontab_20260420_163242.txt
Normal file
12
cron_backups/crontab_20260420_163242.txt
Normal file
@@ -0,0 +1,12 @@
|
||||
*/30 * * * * openclaw agent --agent zuitoushang --message '执行下心跳任务' >> /home/xian/.openclaw/workspace-zuitoushang/works/heartbeat_logs/$(date +\%Y-\%m-\%d_\%H:\%M:\%S).log 2>&1
|
||||
0 * * * * /usr/bin/python3 /home/xian/.copaw/workspaces/default/active_skills/system-monitor/scripts/monitor.py --alert >> /home/xian/.copaw/workspaces/default/works/system-monitor.log 2>&1
|
||||
0 * * * * /home/xian/.openclaw/workspace-zuitoushang/scripts/cleanup-chrome-zombies.sh
|
||||
0 3 * * * /home/xian/.nvm/versions/node/v24.14.0/bin/node /home/xian/.openclaw/workspace-zuitoushang/scripts/daily-summary.js >> /home/xian/.openclaw/workspace-zuitoushang/works/daily-summary.log 2>&1
|
||||
0 * * * * /usr/bin/python3 /home/xian/.openclaw/workspace-zuitoushang/scripts/cpu-monitor.py >> /home/xian/.openclaw/workspace-zuitoushang/works/cpu-monitor.log 2>&1
|
||||
0 8-22 * * * /usr/bin/python3 /home/xian/.openclaw/workspace-zuitoushang/scripts/disk-monitor.py >> /home/xian/.openclaw/workspace-zuitoushang/works/disk-monitor.log 2>&1
|
||||
*/20 * * * * /usr/bin/python3 /home/xian/.openclaw/workspace-coder/works/service-monitor/monitor.py >> /home/xian/.openclaw/workspace-coder/works/service-monitor/monitor.log 2>&1
|
||||
0 17 * * 1-5 /usr/bin/python3 /home/xian/.openclaw/workspace-coder/works/board-monitor/board_monitor.py report >> /home/xian/.openclaw/workspace-coder/works/board-monitor/report.log 2>&1
|
||||
0 17 * * 1-5 /usr/bin/python3 /home/xian/.openclaw/common/stock_system/cron_daily_fetch.py >> /home/xian/.openclaw/common/stock_system/logs/daily_fetch.log 2>&1
|
||||
0 4 * * * /home/xian/.openclaw/workspace-coder/works/xian-favor/scripts/auto_backup.py >> /tmp/xian-favor-backup.log 2>&1
|
||||
0 12 * * * cd /home/xian/.openclaw/workspace-laoli && /home/xian/.openclaw/workspace-laoli/works/send_coder_stats.sh
|
||||
10 3 * * * openclaw agent --agent zuitoushang --message '总结并记忆昨天和你的会话内容' >> /home/xian/.openclaw/workspace-zuitoushang/works/daily-memory/$(date +\%Y-\%m-\%d).log 2>&1
|
||||
BIN
cron_manager.db
Normal file
BIN
cron_manager.db
Normal file
Binary file not shown.
123
logs/app.log
123
logs/app.log
@@ -1,8 +1,8 @@
|
||||
[2026-04-20 12:09:27] ==================================================
|
||||
[2026-04-20 12:09:27] 项目服务管理面板启动
|
||||
[2026-04-20 12:09:27] 访问地址: http://localhost:19013
|
||||
[2026-04-20 12:09:27] 进程PID: 3701577
|
||||
[2026-04-20 12:09:27] ==================================================
|
||||
[2026-04-23 17:47:44] ==================================================
|
||||
[2026-04-23 17:47:44] 项目服务管理面板 v2.0.0 启动
|
||||
[2026-04-23 17:47:44] 访问地址: http://localhost:19013
|
||||
[2026-04-23 17:47:44] 进程PID: 1138715
|
||||
[2026-04-23 17:47:44] ==================================================
|
||||
* Serving Flask app 'app'
|
||||
* Debug mode: off
|
||||
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
|
||||
@@ -10,95 +10,24 @@ WARNING: This is a development server. Do not use it in a production deployment.
|
||||
* Running on http://127.0.0.1:19013
|
||||
* Running on http://192.168.2.17:19013
|
||||
Press CTRL+C to quit
|
||||
127.0.0.1 - - [20/Apr/2026 12:09:30] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.10 - - [20/Apr/2026 12:09:32] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:09:35] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.10 - - [20/Apr/2026 12:09:36] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:09:37] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:09:38] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:09:46] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:09:48] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:09:53] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:09:55] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:09:56] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:09:58] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:06] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:08] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:16] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:18] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:23] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:25] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:26] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:28] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:30] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.10 - - [20/Apr/2026 12:10:32] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:35] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.10 - - [20/Apr/2026 12:10:36] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:37] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:38] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:47] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:47] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:48] "GET /api/crons HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:48] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:51] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:52] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:54] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:55] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:10:57] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:10:58] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:01] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:02] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:07] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:08] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:11] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:12] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:16] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:18] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:21] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:22] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:24] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:25] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:25] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:27] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:27] "GET /api/projects HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:28] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:30] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:31] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.10 - - [20/Apr/2026 12:11:32] "GET /api/projects HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:32] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:35] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:37] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.10 - - [20/Apr/2026 12:11:37] "GET /api/projects HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:38] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:41] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:42] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:47] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:48] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:51] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:52] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:54] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:55] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:55] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:11:57] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:57] "GET /api/projects HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:11:58] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:01] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:12:02] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:07] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:12:08] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:11] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:12:12] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:16] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:12:18] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:21] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:12:22] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:23] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:12:25] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:26] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:12:28] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:31] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.10 - - [20/Apr/2026 12:12:32] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:35] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [20/Apr/2026 12:12:36] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.10 - - [20/Apr/2026 12:12:37] "GET /api/projects HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [20/Apr/2026 12:12:38] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:47:47] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.14 - - [23/Apr/2026 17:47:48] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:47:49] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:47:55] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [23/Apr/2026 17:47:56] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:47:56] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.14 - - [23/Apr/2026 17:47:58] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:48:05] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:48:06] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [23/Apr/2026 17:48:06] "GET /api/projects HTTP/1.1" 200 -
|
||||
192.168.2.14 - - [23/Apr/2026 17:48:08] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:48:13] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [23/Apr/2026 17:48:14] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:48:15] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [23/Apr/2026 17:48:16] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:48:17] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.14 - - [23/Apr/2026 17:48:18] "GET /api/projects HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:48:25] "GET / HTTP/1.1" 200 -
|
||||
127.0.0.1 - - [23/Apr/2026 17:48:26] "GET / HTTP/1.1" 200 -
|
||||
192.168.2.8 - - [23/Apr/2026 17:48:27] "GET /api/projects HTTP/1.1" 200 -
|
||||
192.168.2.14 - - [23/Apr/2026 17:48:28] "GET /api/projects HTTP/1.1" 200 -
|
||||
|
||||
@@ -4268,3 +4268,15 @@ Directory: /home/xian/.openclaw/common/projects/llm-proxy
|
||||
Command: mkdir -p logs && nohup python3 app.py > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/common/projects/llm-proxy
|
||||
/bin/sh: 1: disown: not found
|
||||
|
||||
==================================================
|
||||
[2026-04-20T23:12:09.500932] start
|
||||
Command: mkdir -p logs && nohup python3 app.py > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/common/projects/llm-proxy
|
||||
/bin/sh: 1: disown: not found
|
||||
|
||||
==================================================
|
||||
[2026-04-21T15:45:11.327216] start
|
||||
Command: mkdir -p logs && nohup python3 app.py > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/common/projects/llm-proxy
|
||||
/bin/sh: 1: disown: not found
|
||||
|
||||
18
logs/param-hub.log
Normal file
18
logs/param-hub.log
Normal file
@@ -0,0 +1,18 @@
|
||||
|
||||
==================================================
|
||||
[2026-04-20T18:18:18.605305] start
|
||||
Command: mkdir -p logs && nohup python3 app.py > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/workspace-coder/works/param-hub-python
|
||||
/bin/sh: 1: disown: not found
|
||||
|
||||
==================================================
|
||||
[2026-04-20T23:12:13.392896] start
|
||||
Command: mkdir -p logs && nohup python3 app.py > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/workspace-coder/works/param-hub-python
|
||||
/bin/sh: 1: disown: not found
|
||||
|
||||
==================================================
|
||||
[2026-04-21T15:45:15.735387] start
|
||||
Command: mkdir -p logs && nohup python3 app.py > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/workspace-coder/works/param-hub-python
|
||||
/bin/sh: 1: disown: not found
|
||||
6
logs/voice-chat-web.log
Normal file
6
logs/voice-chat-web.log
Normal file
@@ -0,0 +1,6 @@
|
||||
|
||||
==================================================
|
||||
[2026-04-22T12:29:26.685865] start
|
||||
Command: mkdir -p logs && MODEL_SERVICE_URL=http://192.168.2.5:12001 nohup python3 main.py > logs/server.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/workspace-coder/works/voice-chat-web
|
||||
/bin/sh: 1: disown: not found
|
||||
@@ -73,3 +73,15 @@ Directory: /home/xian/.openclaw/workspace-coder/works/xian-favor
|
||||
Command: mkdir -p logs && nohup xian_favor serve --port 19014 > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/workspace-coder/works/xian-favor
|
||||
/bin/sh: 1: disown: not found
|
||||
|
||||
==================================================
|
||||
[2026-04-20T23:12:18.766216] start
|
||||
Command: mkdir -p logs && nohup python3 -c "from xian_favor.api import start_server; start_server(port=19014)" > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/workspace-coder/works/xian-favor
|
||||
/bin/sh: 1: disown: not found
|
||||
|
||||
==================================================
|
||||
[2026-04-23T00:57:27.686904] start
|
||||
Command: mkdir -p logs && nohup python3 -c "from xian_favor.api import start_server; start_server(port=19014)" > logs/app.log 2>&1 & disown
|
||||
Directory: /home/xian/.openclaw/workspace-coder/works/xian-favor
|
||||
/bin/sh: 1: disown: not found
|
||||
|
||||
@@ -192,6 +192,30 @@
|
||||
"admin_url": "http://localhost:19004/admin",
|
||||
"git_repo": "http://192.168.2.8:12007/coder/tech-forum",
|
||||
"version": "v1.2.0"
|
||||
},
|
||||
{
|
||||
"id": "image-editor",
|
||||
"name": "图片编辑器",
|
||||
"type": "web",
|
||||
"ports": [19018],
|
||||
"directory": "works/image-editor",
|
||||
"start_cmd": "mkdir -p logs && nohup python3 app.py > logs/app.log 2>&1 & disown",
|
||||
"health_url": "http://localhost:19018/api/health",
|
||||
"description": "前端图片处理:合并、分割、挖孔、圆形切图、文字图片",
|
||||
"git_repo": "http://192.168.2.8:12007/coder/image-editor",
|
||||
"version": "v1.2.1"
|
||||
},
|
||||
{
|
||||
"id": "voice-chat-web",
|
||||
"name": "语音对话网页",
|
||||
"type": "web",
|
||||
"ports": [19019],
|
||||
"directory": "works/voice-chat-web",
|
||||
"start_cmd": "mkdir -p logs && MODEL_SERVICE_URL=http://192.168.2.5:12001 nohup python3 main.py > logs/server.log 2>&1 & disown",
|
||||
"health_url": "http://localhost:19019/api/status",
|
||||
"description": "基于Qwen2-Audio的语音交互网页,支持录音和文字对话",
|
||||
"git_repo": "http://192.168.2.8:12007/coder/voice-chat-web",
|
||||
"version": "v1.2.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user