Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
070044f131 | ||
|
|
201d31d25e | ||
|
|
40213494df | ||
|
|
a155e8fcd9 | ||
|
|
0c54af8d26 | ||
|
|
f7722be539 | ||
|
|
779f42c98c | ||
|
|
525620b0f8 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+446224
-54
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
[2026-06-01 20:29:22] 守护进程启动,监控端口 16022
|
||||
+424
-22
@@ -3,36 +3,416 @@
|
||||
"name": "hz1-PowerEdge-R730xd",
|
||||
"description": "黄庄1号服务器 - PowerEdge R730xd",
|
||||
"python_path": "/home/hz1/miniconda3/envs/openclaw/bin/python3.12",
|
||||
"updated": "2026-06-01"
|
||||
"updated": "2026-08-24"
|
||||
},
|
||||
"projects": [
|
||||
{
|
||||
"id": "pdf-translate-web",
|
||||
"name": "PDF翻译助手",
|
||||
"type": "web",
|
||||
"ports": [16021],
|
||||
"directory": "works/pdf-translate-web",
|
||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'python.*pdf-translate-web.*app.py' || true",
|
||||
"health_url": "http://localhost:16021/",
|
||||
"description": "英文PDF翻译中文网站,支持用户系统、会员体系",
|
||||
"admin_url": "http://localhost:16021/admin",
|
||||
"git_repo": "http://121.40.164.32:12007/coder/pdf-translate-web",
|
||||
"version": "v2.0.0"
|
||||
"id": "param-hub-python",
|
||||
"name": "参数中心",
|
||||
"ports": [
|
||||
16041
|
||||
],
|
||||
"directory": "param-hub-python",
|
||||
"start_cmd": "mkdir -p logs && nohup python3 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'param-hub-python.*app.py' || true",
|
||||
"health_url": "http://localhost:16041/",
|
||||
"admin_url": "http://localhost:16041/admin",
|
||||
"description": "参数配置管理平台,多业务线参数统一管理",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/param-hub-python.git",
|
||||
"version": "v2.1.1",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "param-auto-manager",
|
||||
"name": "参数自动化管理",
|
||||
"ports": [
|
||||
16043
|
||||
],
|
||||
"directory": "param-auto-manager",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16043/",
|
||||
"admin_url": "",
|
||||
"description": "参数数据自动化管理系统,自动同步/校验/告警",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/param-auto-manager.git",
|
||||
"version": "v2.1.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "llama-cmd-gen",
|
||||
"name": "Llama命令生成器",
|
||||
"ports": [
|
||||
16052
|
||||
],
|
||||
"directory": "llama-cmd-gen",
|
||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'llama-cmd-gen.*app.py' || true",
|
||||
"health_url": "http://localhost:16052/",
|
||||
"admin_url": "http://localhost:16052/admin",
|
||||
"description": "Llama 模型推理命令生成器,GPU/量化/参数智能匹配",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/llama-cmd-gen.git",
|
||||
"version": "v2.0.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "webtest-agent",
|
||||
"name": "网页测试智能体",
|
||||
"ports": [
|
||||
16061
|
||||
],
|
||||
"directory": "webtest-agent",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16061/",
|
||||
"admin_url": "",
|
||||
"description": "AI 网页测试智能体,LLM 驱动浏览器自动化测试(agent-browser)",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/webtest-agent.git",
|
||||
"version": "v1.1.1",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "project-panel",
|
||||
"name": "项目服务管理面板",
|
||||
"type": "web",
|
||||
"ports": [16022],
|
||||
"directory": "works/project-panel",
|
||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'python.*project-panel.*app.py' || true",
|
||||
"ports": [
|
||||
16022
|
||||
],
|
||||
"directory": "project-panel",
|
||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'project-panel.*app.py' || true",
|
||||
"health_url": "http://localhost:16022/",
|
||||
"description": "统一管理所有 Web 服务项目,支持 Docker 监控、系统资源监控、Cron 管理",
|
||||
"admin_url": "http://localhost:16022",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/project-panel",
|
||||
"version": "v3.0.0"
|
||||
"description": "统一管理所有 Web 服务项目,支持 Docker 监控、系统资源监控、Cron 管理",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/project-panel.git",
|
||||
"version": "v3.4.1",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "project-panel-dev",
|
||||
"name": "项目面板(开发版)",
|
||||
"ports": [
|
||||
16023
|
||||
],
|
||||
"directory": "project-panel-dev",
|
||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'project-panel-dev.*app.py' || true",
|
||||
"health_url": "http://localhost:16023/",
|
||||
"admin_url": "http://localhost:16023",
|
||||
"description": "项目服务管理面板开发版(新功能预览)",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/project-panel-dev.git",
|
||||
"version": "v3.7.5",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "universal-crawler",
|
||||
"name": "通用爬虫系统",
|
||||
"ports": [
|
||||
16062
|
||||
],
|
||||
"directory": "universal-crawler",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16062/",
|
||||
"admin_url": "http://localhost:16062/admin",
|
||||
"description": "批量/定时/自动网页爬取,反爬处理 + 邮件通知",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/universal-crawler.git",
|
||||
"version": "v1.4.3",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "ai-worker-platform",
|
||||
"name": "AI Worker 平台",
|
||||
"ports": [
|
||||
16071
|
||||
],
|
||||
"directory": "ai-worker-platform",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16071/",
|
||||
"admin_url": "http://localhost:16071/admin",
|
||||
"description": "AI Worker 项目管理:任务看板/DAG编排/多Agent协作/评估/企业版",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/ai-worker-platform.git",
|
||||
"version": "v3.4.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "companion-assistant",
|
||||
"name": "随身助手",
|
||||
"ports": [
|
||||
16080
|
||||
],
|
||||
"directory": "companion-assistant",
|
||||
"start_cmd": "cd backend && ./start.sh",
|
||||
"stop_cmd": "cd backend && ./start.sh stop",
|
||||
"health_url": "http://localhost:16080/",
|
||||
"admin_url": "http://localhost:16080/#/admin",
|
||||
"description": "AI 随身助手:流式对话/智能体/多类型项目/语音/附件",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/companion-assistant.git",
|
||||
"version": "v0.2.1",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "nba-fan-hub",
|
||||
"name": "NBA球迷大全",
|
||||
"ports": [
|
||||
16090
|
||||
],
|
||||
"directory": "nba-fan-hub",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16090/",
|
||||
"admin_url": "http://localhost:16090/admin",
|
||||
"description": "NBA 数据问答 + 球队/球员/比赛/新闻/人物/排名六大浏览页",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/nba-fan-hub.git",
|
||||
"version": "v1.2.13",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "data-chart-tool",
|
||||
"name": "数据可视化图表生成器",
|
||||
"ports": [
|
||||
16016
|
||||
],
|
||||
"directory": "data-chart-tool",
|
||||
"start_cmd": "bash keepalive.sh",
|
||||
"stop_cmd": "pkill -f 'data-chart-tool.*server.js' || true",
|
||||
"health_url": "http://localhost:16016/",
|
||||
"admin_url": "",
|
||||
"description": "柱状/折线/混合/饼图/雷达图/双Y轴/表格/多图合并,服务端出图",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/data-chart-tool.git",
|
||||
"version": "v1.20.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "stock-advisor",
|
||||
"name": "智能荐股系统",
|
||||
"ports": [
|
||||
16095
|
||||
],
|
||||
"directory": "stock-advisor",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16095/",
|
||||
"admin_url": "http://localhost:16095/admin",
|
||||
"description": "六因子评分 + AI 深度研报 + RAG 资讯 + 量化策略回测 + 舆情监控",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/stock-advisor.git",
|
||||
"version": "v1.8.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "news-tracker",
|
||||
"name": "新闻智能跟踪",
|
||||
"ports": [
|
||||
16100
|
||||
],
|
||||
"directory": "news-tracker",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16100/",
|
||||
"admin_url": "",
|
||||
"description": "自动采集→打分→DeepSeek 深析→重要资讯实时/每日汇总邮件",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/news-tracker.git",
|
||||
"version": "v1.0.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "pdf-to-markdown",
|
||||
"name": "PDF→Markdown 转换",
|
||||
"ports": [
|
||||
16082
|
||||
],
|
||||
"directory": "pdf-to-markdown",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16082/",
|
||||
"admin_url": "",
|
||||
"description": "PDF 转 Markdown:文本页/影印页/混合页自适应 + 图片裁剪 + 表格转录",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/pdf-to-markdown.git",
|
||||
"version": "v1.0.2",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "llm-speed-tester",
|
||||
"name": "LLM 速度测试台",
|
||||
"ports": [
|
||||
16097
|
||||
],
|
||||
"directory": "llm-speed-tester",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16097/",
|
||||
"admin_url": "",
|
||||
"description": "多提供商 LLM 速度基准:TTFT/预填充/解码 tok/s + Excel/JSON 导出",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/llm-speed-tester.git",
|
||||
"version": "v2.1.1",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "tech-blog",
|
||||
"name": "技术博客系统",
|
||||
"ports": [
|
||||
16012
|
||||
],
|
||||
"directory": "tech-blog",
|
||||
"start_cmd": "mkdir -p logs && nohup python3 run.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'tech-blog.*run.py' || true",
|
||||
"health_url": "http://localhost:16012/",
|
||||
"admin_url": "http://localhost:16012/admin",
|
||||
"description": "Flask 技术文章展示 + 后台管理(文章/分类/标签/仪表盘)",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/tech-blog.git",
|
||||
"version": "v1.1.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "worldcup-predictor",
|
||||
"name": "世界杯预测系统",
|
||||
"ports": [
|
||||
16015
|
||||
],
|
||||
"directory": "worldcup-predictor",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "pkill -f 'worldcup-predictor.*app.py' || true",
|
||||
"health_url": "http://localhost:16015/",
|
||||
"admin_url": "http://localhost:16015/admin",
|
||||
"description": "世界杯比赛多算法预测对比 + 管理后台",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/worldcup-predictor.git",
|
||||
"version": "v1.0.5",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "remote-host-agent",
|
||||
"name": "远程主机代理",
|
||||
"ports": [
|
||||
16018
|
||||
],
|
||||
"directory": "remote-host-agent",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16018/api/health",
|
||||
"admin_url": "",
|
||||
"description": "机器B(无固定IP)监控/控制:HTTP上报 + 命令长轮询",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/remote-host-agent.git",
|
||||
"version": "v1.0.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "llm-forum",
|
||||
"name": "LLM 论坛",
|
||||
"ports": [
|
||||
16037
|
||||
],
|
||||
"directory": "llm-forum",
|
||||
"start_cmd": "mkdir -p logs && nohup node server.js > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'llm-forum.*server.js' || true",
|
||||
"health_url": "http://localhost:16037/",
|
||||
"admin_url": "http://localhost:16037/admin",
|
||||
"description": "大模型交流论坛:主题/回复/Markdown/分类/管理后台",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/llm-forum.git",
|
||||
"version": "v1.0.2",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "music-platform",
|
||||
"name": "音乐展示平台",
|
||||
"ports": [
|
||||
16031
|
||||
],
|
||||
"directory": "music-platform",
|
||||
"start_cmd": "mkdir -p logs && nohup node server.js > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'music-platform.*server.js' || true",
|
||||
"health_url": "http://localhost:16031/",
|
||||
"admin_url": "http://localhost:16031/admin",
|
||||
"description": "音乐平台:播放/歌词同步/搜索/歌手专辑歌单 + 后台管理",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/music-platform.git",
|
||||
"version": "v1.0.1",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "video-platform",
|
||||
"name": "短视频展示平台",
|
||||
"ports": [
|
||||
16035
|
||||
],
|
||||
"directory": "video-platform",
|
||||
"start_cmd": "./start.sh",
|
||||
"stop_cmd": "./start.sh stop",
|
||||
"health_url": "http://localhost:16035/",
|
||||
"admin_url": "http://localhost:16035/admin",
|
||||
"description": "短视频平台:弹幕(WS实时)/点赞/分类 + 后台管理",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/video-platform.git",
|
||||
"version": "v1.0.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "web-capture-api",
|
||||
"name": "网页截图API",
|
||||
"ports": [
|
||||
16025
|
||||
],
|
||||
"directory": "web-capture-api",
|
||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'web-capture-api.*app.py' || true",
|
||||
"health_url": "http://localhost:16025/",
|
||||
"admin_url": "",
|
||||
"description": "网页截图/HTML 提取 REST API + Web 界面",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/web-capture-api.git",
|
||||
"version": "v1.4.2",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "agent-sessions-viewer",
|
||||
"name": "会话历史查看器",
|
||||
"ports": [
|
||||
16039
|
||||
],
|
||||
"directory": "agent-sessions-viewer",
|
||||
"start_cmd": "mkdir -p logs && nohup node server.js > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'agent-sessions-viewer.*server.js' || true",
|
||||
"health_url": "http://localhost:16039/",
|
||||
"admin_url": "http://localhost:16039/admin",
|
||||
"description": "OpenClaw 智能体历史会话/消息查看器",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/agent-sessions-viewer.git",
|
||||
"version": "v1.1.0",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
},
|
||||
{
|
||||
"id": "pdf-translate-web",
|
||||
"name": "PDF翻译助手",
|
||||
"ports": [
|
||||
16021
|
||||
],
|
||||
"directory": "pdf-translate-web",
|
||||
"start_cmd": "mkdir -p logs && nohup /home/hz1/miniconda3/envs/openclaw/bin/python3.12 app.py > logs/app.log 2>&1 & disown",
|
||||
"stop_cmd": "pkill -f 'pdf-translate-web.*app.py' || true",
|
||||
"health_url": "http://localhost:16021/",
|
||||
"admin_url": "http://localhost:16021/admin",
|
||||
"description": "英文PDF翻译中文网站,支持用户系统、会员体系",
|
||||
"git_repo": "http://121.40.164.32:12007/hz4th_coder/pdf-translate-web.git",
|
||||
"version": "v2.10.1",
|
||||
"log_file": "logs/app.log",
|
||||
"type": "web"
|
||||
}
|
||||
],
|
||||
"external_services": [
|
||||
@@ -40,9 +420,31 @@
|
||||
"id": "meilisearch",
|
||||
"name": "Meilisearch 搜索引擎",
|
||||
"type": "external",
|
||||
"ports": [16001],
|
||||
"ports": [
|
||||
16001
|
||||
],
|
||||
"health_url": "http://localhost:16001/health",
|
||||
"description": "高性能搜索引擎,只读监控"
|
||||
},
|
||||
{
|
||||
"id": "chroma",
|
||||
"name": "Chroma 向量库",
|
||||
"type": "external",
|
||||
"ports": [
|
||||
16010
|
||||
],
|
||||
"health_url": "http://localhost:16010/",
|
||||
"description": "向量数据库(RAG 项目共用)"
|
||||
},
|
||||
{
|
||||
"id": "embedding",
|
||||
"name": "Embedding 服务",
|
||||
"type": "external",
|
||||
"ports": [
|
||||
16011
|
||||
],
|
||||
"health_url": "http://localhost:16011/",
|
||||
"description": "bge-large-zh-v1.5 向量化 + rerank 精排"
|
||||
}
|
||||
]
|
||||
}
|
||||
+12
-9
@@ -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,
|
||||
@@ -219,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,
|
||||
|
||||
+54
-1
@@ -68,9 +68,11 @@ def register_project_routes(app):
|
||||
|
||||
if project.get('start_cmds'):
|
||||
for name, info in project['start_cmds'].items():
|
||||
log_message(f"[API-START] 项目={project_id} 命令={info['cmd']} 目录={cwd}")
|
||||
run_command(info['cmd'], cwd, project_id, f'start-{name}')
|
||||
else:
|
||||
cmd = project.get('start_cmd', 'python3 app.py')
|
||||
log_message(f"[API-START] 项目={project_id} 命令={cmd} 目录={cwd}")
|
||||
run_command(cmd, cwd, project_id, 'start')
|
||||
|
||||
return jsonify({'message': '服务启动中...'})
|
||||
@@ -128,7 +130,9 @@ def register_project_routes(app):
|
||||
'directory': data.get('directory', ''),
|
||||
'start_cmd': data.get('start_cmd', ''),
|
||||
'stop_cmd': data.get('stop_cmd', ''),
|
||||
'log_file': data.get('log_file', ''),
|
||||
'health_url': data.get('health_url', ''),
|
||||
'admin_url': data.get('admin_url', ''),
|
||||
'description': data.get('description', ''),
|
||||
'git_repo': data.get('git_repo', ''),
|
||||
'version': data.get('version', 'v1.0.0')
|
||||
@@ -152,7 +156,7 @@ def register_project_routes(app):
|
||||
return jsonify({'error': '项目不存在'}), 404
|
||||
|
||||
# 更新字段
|
||||
for key in ['name', 'ports', 'directory', 'start_cmd', 'stop_cmd', 'health_url', 'description', 'git_repo', 'version']:
|
||||
for key in ['name', 'ports', 'directory', 'log_file', 'start_cmd', 'stop_cmd', 'health_url', 'admin_url', 'description', 'git_repo', 'version']:
|
||||
if key in data:
|
||||
project[key] = data[key]
|
||||
|
||||
@@ -162,6 +166,55 @@ def register_project_routes(app):
|
||||
|
||||
return jsonify({'message': '项目更新成功'})
|
||||
|
||||
@app.route('/api/projects/<project_id>/log')
|
||||
def api_project_log(project_id):
|
||||
"""获取项目日志"""
|
||||
projects = load_projects()
|
||||
project = next((p for p in projects if p['id'] == project_id), None)
|
||||
if not project:
|
||||
return jsonify({'error': '项目不存在'}), 404
|
||||
|
||||
lines = request.args.get('lines', 200, type=int)
|
||||
lines = max(1, min(lines, 5000)) # 限制 1-5000 行
|
||||
|
||||
log_file = project.get('log_file', '')
|
||||
if not log_file:
|
||||
# 默认:项目目录下的 logs/app.log
|
||||
cwd = project.get('directory', '')
|
||||
if cwd:
|
||||
from config import WORKSPACE_DIR
|
||||
import os
|
||||
abs_dir = cwd if cwd.startswith('/') else os.path.join(WORKSPACE_DIR, cwd)
|
||||
log_file = os.path.join(abs_dir, 'logs', 'app.log')
|
||||
|
||||
# 支持相对路径和绝对路径
|
||||
import os
|
||||
if not log_file.startswith('/'):
|
||||
cwd = project.get('directory', '')
|
||||
if cwd:
|
||||
from config import WORKSPACE_DIR
|
||||
abs_dir = cwd if cwd.startswith('/') else os.path.join(WORKSPACE_DIR, cwd)
|
||||
log_file = os.path.join(abs_dir, log_file)
|
||||
|
||||
if not os.path.exists(log_file):
|
||||
return jsonify({'log': f'日志文件不存在: {log_file}'})
|
||||
|
||||
try:
|
||||
# 读取最后 N 行(高效方式)
|
||||
with open(log_file, 'r', encoding='utf-8', errors='replace') as f:
|
||||
all_lines = f.readlines()
|
||||
tail_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
|
||||
content = ''.join(tail_lines)
|
||||
|
||||
return jsonify({
|
||||
'log': content if content else '(日志为空)',
|
||||
'file': log_file,
|
||||
'total_lines': len(all_lines),
|
||||
'shown_lines': len(tail_lines)
|
||||
})
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'读取日志失败: {str(e)}'}), 500
|
||||
|
||||
@app.route('/api/projects/<project_id>', methods=['DELETE'])
|
||||
def api_delete_project(project_id):
|
||||
"""删除项目"""
|
||||
|
||||
+2583
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+83
File diff suppressed because one or more lines are too long
+484
-55
@@ -5,8 +5,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>项目服务管理面板 v3.3.1</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">
|
||||
<script src="/static/vendor/tailwindcss.js"></script>
|
||||
<link href="/static/vendor/remixicon/remixicon.css" rel="stylesheet">
|
||||
<style>
|
||||
body { background: #0f172a; }
|
||||
.card { background: #1e293b; border: 1px solid #334155; }
|
||||
@@ -28,6 +28,12 @@
|
||||
.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-search { padding: 8px 12px; border-radius: 8px; background: #1e293b; border: 1px solid #334155; color: #f1f5f9; font-size: 14px; outline: none; min-width: 200px; transition: border-color 0.2s; }
|
||||
.filter-search:focus { border-color: #3b82f6; }
|
||||
.filter-search::placeholder { color: #64748b; }
|
||||
.filter-divider { width: 1px; height: 24px; background: #334155; margin: 0 4px; }
|
||||
.filter-bar-btn[data-status].active { background: #8b5cf6; border-color: #8b5cf6; }
|
||||
.filter-bar-btn.sort-active { background: #0ea5e9; border-color: #0ea5e9; color: #fff; }
|
||||
.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; }
|
||||
@@ -67,6 +73,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">
|
||||
@@ -168,11 +177,18 @@
|
||||
|
||||
<!-- 筛选器(顶部固定) -->
|
||||
<div class="filter-bar">
|
||||
<input type="text" id="projectSearch" class="filter-search" placeholder="🔍 搜索服务名称/描述/端口..." oninput="setSearch(this.value)" />
|
||||
<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>
|
||||
<span class="filter-divider"></span>
|
||||
<button onclick="setStatusFilter('')" class="filter-bar-btn" data-status=""><i class="ri-globe-line"></i> 全部状态</button>
|
||||
<button onclick="setStatusFilter('online')" class="filter-bar-btn" data-status="online"><i class="ri-wifi-line"></i> 在线中</button>
|
||||
<button onclick="setStatusFilter('admin')" class="filter-bar-btn" data-status="admin"><i class="ri-settings-3-line"></i> 有后台</button>
|
||||
<span class="filter-divider"></span>
|
||||
<button onclick="togglePortSort()" id="portSortBtn" class="filter-bar-btn" title="按端口号排序"><i class="ri-sort-asc"></i><span>端口排序</span></button>
|
||||
<button onclick="showAddServiceModal()" class="filter-bar-btn add-btn"><i class="ri-add-circle-line"></i> 新增服务</button>
|
||||
</div>
|
||||
|
||||
@@ -555,20 +571,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>
|
||||
@@ -698,15 +714,16 @@
|
||||
</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>
|
||||
<h3 id="addServiceModalTitle" 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)">
|
||||
<input type="hidden" id="editServiceId">
|
||||
<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、监控面板">
|
||||
@@ -909,6 +926,83 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑项目配置模态框 -->
|
||||
<div id="editProjectModal" class="modal-overlay hidden">
|
||||
<div class="card rounded-xl modal-content p-6">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 id="editProjectModalTitle" class="font-bold text-lg">编辑项目配置</h3>
|
||||
<button onclick="closeEditProjectModal()" class="text-gray-400 hover:text-white"><i class="ri-close-line text-xl"></i></button>
|
||||
</div>
|
||||
|
||||
<form id="editProjectForm" onsubmit="saveProjectEdit(event)">
|
||||
<input type="hidden" id="editProjectId">
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">项目名称</label>
|
||||
<input type="text" id="editProjectName" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">项目目录 <span class="text-xs text-gray-500">(相对或绝对路径)</span></label>
|
||||
<input type="text" id="editProjectDir" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="相对:works/my-project 或 绝对:/home/user/project">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">端口(逗号分隔)</label>
|
||||
<input type="text" id="editProjectPorts" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:16022,16023">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">健康检查URL</label>
|
||||
<input type="text" id="editProjectHealthUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://localhost:16022/">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">后台URL</label>
|
||||
<input type="text" id="editProjectAdminUrl" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://localhost:16022/admin">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">Git 仓库地址</label>
|
||||
<input type="text" id="editProjectGitRepo" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="例如:http://.../project.git">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">版本</label>
|
||||
<input type="text" id="editProjectVersion" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg" placeholder="v1.0.0">
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">📁 日志文件路径 <span class="text-xs text-gray-500">(相对或绝对路径)</span></label>
|
||||
<input type="text" id="editProjectLogFile" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg font-mono text-sm" placeholder="相对:logs/app.log 或绝对路径。留空则默认 项目目录/logs/app.log">
|
||||
<p class="text-xs text-gray-500 mt-1">服务日志文件的位置,点击「日志」按钮时读取此文件</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="block text-gray-400 text-sm mb-1">📋 启动命令</label>
|
||||
<textarea id="editProjectStartCmd" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg font-mono text-sm" rows="3" placeholder="例如:nohup python3 app.py > logs/app.log 2>&1 & disown"></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">服务启动时执行的命令</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-400 text-sm mb-1">🛑 停止命令</label>
|
||||
<textarea id="editProjectStopCmd" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg font-mono text-sm" rows="2" placeholder="例如:pkill -f 'python.*project.*app.py' || true"></textarea>
|
||||
<p class="text-xs text-gray-500 mt-1">服务停止时执行的命令</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-gray-400 text-sm mb-1">描述</label>
|
||||
<input type="text" id="editProjectDesc" class="w-full bg-gray-700 text-gray-200 px-3 py-2 rounded-lg">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" onclick="closeEditProjectModal()" class="btn bg-gray-600 hover:bg-gray-700 px-4 py-2 rounded-lg">取消</button>
|
||||
<button type="submit" class="btn bg-blue-600 hover:bg-blue-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>
|
||||
@@ -920,6 +1014,9 @@
|
||||
let cronTasks = [];
|
||||
let currentTab = 'projects';
|
||||
let currentFilter = 'all';
|
||||
let searchKeyword = '';
|
||||
let statusFilter = ''; // '' = all, 'online' = running/partial, 'admin' = has admin_url
|
||||
let portSort = ''; // '' = none, 'asc', 'desc'
|
||||
let externalIp = localStorage.getItem('externalIp') || '192.168.2.17';
|
||||
let enableLogs = localStorage.getItem('enableLogs') === 'true';
|
||||
|
||||
@@ -929,11 +1026,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 +1089,12 @@
|
||||
document.getElementById('processListSection').classList.add('hidden');
|
||||
}
|
||||
|
||||
// 离开系统Tab时关闭非实时刷新定时器
|
||||
if (tab !== 'system' && systemRefreshTimer) {
|
||||
clearInterval(systemRefreshTimer);
|
||||
systemRefreshTimer = null;
|
||||
}
|
||||
|
||||
if (tab === 'cron') {
|
||||
loadCronTasks();
|
||||
}
|
||||
@@ -965,6 +1103,8 @@
|
||||
loadDesktopNotifySetting();
|
||||
renderEmailRules();
|
||||
loadSystemStats();
|
||||
// 启动系统资源定时刷新(按页面刷新间隔)
|
||||
startSystemRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1281,14 +1421,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 +1440,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;
|
||||
@@ -1341,6 +1498,16 @@
|
||||
const res = await fetch('/api/projects');
|
||||
const data = await res.json();
|
||||
projects = data.projects;
|
||||
|
||||
// 加载外部服务列表
|
||||
try {
|
||||
const extRes = await fetch('/api/external-services');
|
||||
const extData = await extRes.json();
|
||||
window._externalServices = extData.services || [];
|
||||
} catch (e) {
|
||||
window._externalServices = [];
|
||||
}
|
||||
|
||||
renderProjects();
|
||||
updateStats();
|
||||
updateConnectionStatus(true);
|
||||
@@ -1375,11 +1542,40 @@
|
||||
function filterType(type) {
|
||||
currentFilter = type;
|
||||
document.querySelectorAll('.filter-bar-btn').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.type === type);
|
||||
if (btn.dataset.type !== undefined) {
|
||||
btn.classList.toggle('active', btn.dataset.type === type);
|
||||
}
|
||||
});
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function setSearch(val) {
|
||||
searchKeyword = (val || '').trim().toLowerCase();
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function setStatusFilter(status) {
|
||||
statusFilter = status;
|
||||
document.querySelectorAll('.filter-bar-btn[data-status]').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.status === status);
|
||||
});
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function togglePortSort() {
|
||||
if (!portSort) portSort = 'asc';
|
||||
else if (portSort === 'asc') portSort = 'desc';
|
||||
else portSort = '';
|
||||
const btn = document.getElementById('portSortBtn');
|
||||
btn.classList.toggle('sort-active', !!portSort);
|
||||
const icon = btn.querySelector('i');
|
||||
const label = btn.querySelector('span');
|
||||
if (portSort === 'asc') { icon.className = 'ri-sort-asc'; label.textContent = '端口↑'; }
|
||||
else if (portSort === 'desc') { icon.className = 'ri-sort-desc'; label.textContent = '端口↓'; }
|
||||
else { icon.className = 'ri-sort-asc'; label.textContent = '端口排序'; }
|
||||
renderProjects();
|
||||
}
|
||||
|
||||
function renderProjects() {
|
||||
const list = document.getElementById('projectsList');
|
||||
|
||||
@@ -1387,10 +1583,43 @@
|
||||
const customServices = getCustomServices();
|
||||
const allProjects = [...projects, ...customServices];
|
||||
|
||||
const filtered = currentFilter === 'all' ? allProjects : allProjects.filter(p => p.type === currentFilter || (currentFilter === 'web' && p.isCustom));
|
||||
let filtered = currentFilter === 'all' ? allProjects : allProjects.filter(p => p.type === currentFilter || (currentFilter === 'web' && p.isCustom));
|
||||
|
||||
// 关键字搜索:名称/描述/id/端口
|
||||
if (searchKeyword) {
|
||||
filtered = filtered.filter(p => {
|
||||
const haystack = [
|
||||
p.name || '',
|
||||
p.description || '',
|
||||
p.id || '',
|
||||
...(p.ports || []),
|
||||
p.url || ''
|
||||
].join(' ').toLowerCase();
|
||||
return haystack.includes(searchKeyword);
|
||||
});
|
||||
}
|
||||
|
||||
// 状态筛选:在线中(running/partial)/ 有后台(admin_url)
|
||||
if (statusFilter === 'online') {
|
||||
filtered = filtered.filter(p => {
|
||||
const st = p.status?.status;
|
||||
return st === 'running' || st === 'partial';
|
||||
});
|
||||
} else if (statusFilter === 'admin') {
|
||||
filtered = filtered.filter(p => !!p.admin_url);
|
||||
}
|
||||
|
||||
// 端口排序:升/降序
|
||||
if (portSort) {
|
||||
filtered = filtered.slice().sort((a, b) => {
|
||||
const pa = a.ports && a.ports.length ? a.ports[0] : 0;
|
||||
const pb = b.ports && b.ports.length ? b.ports[0] : 0;
|
||||
return portSort === 'asc' ? pa - pb : pb - pa;
|
||||
});
|
||||
}
|
||||
|
||||
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>';
|
||||
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>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1419,23 +1648,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() {
|
||||
@@ -1466,7 +1710,8 @@
|
||||
<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>
|
||||
<button onclick="editCustomService('${p.id}')" class="text-blue-400 hover:text-blue-300 ml-1" title="编辑服务"><i class="ri-edit-line"></i></button>
|
||||
<button onclick="deleteCustomService('${p.id}')" class="text-red-400 hover:text-red-300 ml-1" title="删除服务"><i class="ri-delete-bin-line"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1512,6 +1757,7 @@
|
||||
<button onclick="restartProject('${p.id}')" class="btn bg-yellow-600 hover:bg-yellow-700 px-2 py-0.5 rounded text-xs">重启</button>
|
||||
` : `<button onclick="startProject('${p.id}')" class="btn bg-green-600 hover:bg-green-700 px-2 py-0.5 rounded text-xs">启动</button>`}
|
||||
<button onclick="viewLog('${p.id}')" class="btn bg-gray-600 hover:bg-gray-700 px-2 py-0.5 rounded text-xs">日志</button>
|
||||
<button onclick="editProject('${p.id}')" class="btn bg-blue-600 hover:bg-blue-700 px-2 py-0.5 rounded text-xs">编辑</button>
|
||||
</div>
|
||||
<button onclick="toggleServiceActive('${p.id}')" class="text-xs ${isActive ? 'text-green-400 hover:text-green-300' : 'text-gray-400 hover:text-gray-300'}" title="${isActive ? '点击归档' : '点击激活'}">
|
||||
<i class="ri-${isActive ? 'checkbox-circle' : 'archive'}-line"></i>
|
||||
@@ -1741,7 +1987,7 @@
|
||||
return Array.from(checkboxes).map(cb => cb.value);
|
||||
}
|
||||
|
||||
function onResourceChange() {
|
||||
function onResourceChange(preselectedServiceId) {
|
||||
const selected = getSelectedResources();
|
||||
const unitEl = document.getElementById('emailRuleThresholdUnit');
|
||||
const thresholdEl = document.getElementById('emailRuleThreshold');
|
||||
@@ -1755,7 +2001,7 @@
|
||||
// 显示/隐藏服务选择器
|
||||
if (hasServiceDown) {
|
||||
serviceSelector.style.display = 'block';
|
||||
populateServiceSelector();
|
||||
populateServiceSelector(preselectedServiceId);
|
||||
// 服务下线使用分钟作为阈值单位
|
||||
unitEl.textContent = '分钟';
|
||||
thresholdEl.value = 5;
|
||||
@@ -1794,17 +2040,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
function populateServiceSelector() {
|
||||
function populateServiceSelector(preselectedId) {
|
||||
const sel = document.getElementById('emailRuleServiceId');
|
||||
const currentVal = sel.value;
|
||||
// 收集所有web类型服务
|
||||
const webServices = projects.filter(p => p.type === 'web');
|
||||
let options = '<option value="">-- 请选择要监控的服务 --</option>';
|
||||
webServices.forEach(p => {
|
||||
const portsStr = p.ports ? p.ports.join(',') : '';
|
||||
const selected = p.id === currentVal ? ' selected' : '';
|
||||
options += `<option value="${p.id}"${selected}>${p.name} (端口: ${portsStr})</option>`;
|
||||
const currentVal = preselectedId || sel.value;
|
||||
|
||||
// 收集所有可监控的服务
|
||||
const allServices = [];
|
||||
|
||||
// 1. Web 类型项目
|
||||
const webProjects = projects.filter(p => p.type === 'web');
|
||||
webProjects.forEach(p => {
|
||||
allServices.push({
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
ports: p.ports || [],
|
||||
type: 'web',
|
||||
description: p.description || ''
|
||||
});
|
||||
});
|
||||
|
||||
// 2. 自定义服务(localStorage)
|
||||
const customServices = getCustomServices();
|
||||
customServices.forEach(s => {
|
||||
const port = s.port ? [parseInt(s.port)] : [];
|
||||
allServices.push({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
ports: port,
|
||||
type: 'custom',
|
||||
description: s.description || '自定义外部服务'
|
||||
});
|
||||
});
|
||||
|
||||
// 3. 外部服务(从 projects.json 中的 external_services)
|
||||
const externalServices = window._externalServices || [];
|
||||
externalServices.forEach(s => {
|
||||
allServices.push({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
ports: s.ports || [],
|
||||
type: 'external',
|
||||
description: s.description || ''
|
||||
});
|
||||
});
|
||||
|
||||
const typeLabels = { web: 'Web服务', custom: '自定义', external: '外部' };
|
||||
let options = '<option value="">-- 请选择要监控的服务 --</option>';
|
||||
allServices.forEach(s => {
|
||||
const portsStr = s.ports.length > 0 ? s.ports.join(',') : '无端口';
|
||||
const label = s.type !== 'web' ? ` [${typeLabels[s.type]}]` : '';
|
||||
const selected = s.id === currentVal ? ' selected' : '';
|
||||
options += `<option value="${s.id}"${selected}>${s.name}${label} (端口: ${portsStr})</option>`;
|
||||
});
|
||||
|
||||
if (allServices.length === 0) {
|
||||
options += '<option value="" disabled>暂无可用服务</option>';
|
||||
}
|
||||
|
||||
sel.innerHTML = options;
|
||||
}
|
||||
|
||||
@@ -1868,11 +2160,8 @@
|
||||
const cb = document.querySelector(`.email-rule-resource[value="${r}"]`);
|
||||
if (cb) cb.checked = true;
|
||||
});
|
||||
onResourceChange();
|
||||
// 服务下线相关
|
||||
if (rule.serviceId) {
|
||||
document.getElementById('emailRuleServiceId').value = rule.serviceId;
|
||||
}
|
||||
onResourceChange(rule.serviceId);
|
||||
// 服务下线相关 - serviceId 已通过 onResourceChange 传入
|
||||
document.getElementById('emailRuleThreshold').value = rule.threshold;
|
||||
document.getElementById('emailRuleTriggerType').value = rule.triggerType || 'single';
|
||||
document.getElementById('emailRuleTriggerParam').value = rule.triggerParam || 3;
|
||||
@@ -2183,6 +2472,8 @@
|
||||
}
|
||||
|
||||
function showAddServiceModal() {
|
||||
document.getElementById('addServiceModalTitle').textContent = '新增自定义服务';
|
||||
document.getElementById('editServiceId').value = '';
|
||||
document.getElementById('serviceName').value = '';
|
||||
document.getElementById('servicePort').value = '';
|
||||
document.getElementById('servicePath').value = '';
|
||||
@@ -2193,6 +2484,36 @@
|
||||
document.getElementById('addServiceModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function editCustomService(serviceId) {
|
||||
const services = getCustomServices();
|
||||
const svc = services.find(s => s.id === serviceId);
|
||||
if (!svc) return;
|
||||
|
||||
document.getElementById('addServiceModalTitle').textContent = `编辑自定义服务: ${svc.name}`;
|
||||
document.getElementById('editServiceId').value = svc.id;
|
||||
document.getElementById('serviceName').value = svc.name || '';
|
||||
document.getElementById('servicePort').value = svc.port || '';
|
||||
|
||||
// 尝试从 url 中解析 path
|
||||
let path = '';
|
||||
if (svc.url && svc.port) {
|
||||
const prefix = `http://${externalIp}:${svc.port}`;
|
||||
if (svc.url.startsWith(prefix)) {
|
||||
path = svc.url.substring(prefix.length);
|
||||
}
|
||||
}
|
||||
document.getElementById('servicePath').value = path;
|
||||
document.getElementById('serviceFullUrl').value = svc.url || '';
|
||||
document.getElementById('serviceAdminUrl').value = svc.admin_url || '';
|
||||
document.getElementById('serviceDesc').value = svc.description || '';
|
||||
|
||||
if (svc.port) {
|
||||
updateServicePreview();
|
||||
}
|
||||
|
||||
document.getElementById('addServiceModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeAddServiceModal() {
|
||||
document.getElementById('addServiceModal').classList.add('hidden');
|
||||
}
|
||||
@@ -2212,6 +2533,7 @@
|
||||
function saveCustomService(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const editId = document.getElementById('editServiceId').value;
|
||||
const name = document.getElementById('serviceName').value.trim();
|
||||
const port = document.getElementById('servicePort').value.trim();
|
||||
const path = document.getElementById('servicePath').value.trim();
|
||||
@@ -2235,18 +2557,35 @@
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (editId) {
|
||||
// 编辑模式:更新已有服务
|
||||
const idx = services.findIndex(s => s.id === editId);
|
||||
if (idx === -1) {
|
||||
alert('服务不存在');
|
||||
return;
|
||||
}
|
||||
services[idx].name = name;
|
||||
services[idx].port = port || '';
|
||||
services[idx].url = mainUrl;
|
||||
services[idx].admin_url = adminUrl || null;
|
||||
services[idx].description = desc || '自定义外部服务';
|
||||
} else {
|
||||
// 新增模式
|
||||
const newService = {
|
||||
id: 'custom_' + Date.now(),
|
||||
name: name,
|
||||
port: port || '',
|
||||
url: mainUrl,
|
||||
admin_url: adminUrl || null,
|
||||
description: desc || '自定义外部服务',
|
||||
type: 'custom',
|
||||
isCustom: true
|
||||
};
|
||||
services.push(newService);
|
||||
}
|
||||
|
||||
saveCustomServices(services);
|
||||
closeAddServiceModal();
|
||||
loadProjects();
|
||||
}
|
||||
@@ -2314,16 +2653,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function viewLog(id) {
|
||||
async function viewLog(id, lines = 200) {
|
||||
const project = projects.find(p => p.id === id);
|
||||
document.getElementById('logTitle').textContent = `${project?.name || id} - 日志`;
|
||||
document.getElementById('logTitle').textContent = `${project?.name || id} - 日志 (最近${lines}行)`;
|
||||
document.getElementById('logContent').textContent = '加载中...';
|
||||
document.getElementById('logModal').classList.remove('hidden');
|
||||
document.getElementById('logModal').classList.add('flex');
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/projects/${id}/log`);
|
||||
const res = await fetch(`/api/projects/${id}/log?lines=${lines}`);
|
||||
const data = await res.json();
|
||||
document.getElementById('logContent').textContent = data.log || '暂无日志';
|
||||
let display = data.log || '暂无日志';
|
||||
if (data.file) {
|
||||
display = `📁 日志文件: ${data.file}\n📊 共 ${data.total_lines} 行,显示最近 ${data.shown_lines} 行\n${'─'.repeat(60)}\n\n${display}`;
|
||||
}
|
||||
document.getElementById('logContent').textContent = display;
|
||||
} catch (e) {
|
||||
document.getElementById('logContent').textContent = '获取日志失败: ' + e.message;
|
||||
}
|
||||
@@ -2334,6 +2678,86 @@
|
||||
document.getElementById('logModal').classList.remove('flex');
|
||||
}
|
||||
|
||||
// ==================== 项目编辑 ====================
|
||||
|
||||
function editProject(id) {
|
||||
const project = projects.find(p => p.id === id);
|
||||
if (!project || project.type !== 'web') return;
|
||||
|
||||
document.getElementById('editProjectModalTitle').textContent = `${project.name} - 编辑配置`;
|
||||
document.getElementById('editProjectId').value = project.id;
|
||||
document.getElementById('editProjectName').value = project.name || '';
|
||||
document.getElementById('editProjectDir').value = project.directory || '';
|
||||
document.getElementById('editProjectPorts').value = (project.ports || []).join(',');
|
||||
document.getElementById('editProjectHealthUrl').value = project.health_url || '';
|
||||
document.getElementById('editProjectAdminUrl').value = project.admin_url || '';
|
||||
document.getElementById('editProjectGitRepo').value = project.git_repo || '';
|
||||
document.getElementById('editProjectVersion').value = project.version || '';
|
||||
document.getElementById('editProjectLogFile').value = project.log_file || '';
|
||||
document.getElementById('editProjectStartCmd').value = project.start_cmd || '';
|
||||
document.getElementById('editProjectStopCmd').value = project.stop_cmd || '';
|
||||
document.getElementById('editProjectDesc').value = project.description || '';
|
||||
|
||||
document.getElementById('editProjectModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeEditProjectModal() {
|
||||
document.getElementById('editProjectModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async function saveProjectEdit(event) {
|
||||
event.preventDefault();
|
||||
|
||||
const id = document.getElementById('editProjectId').value;
|
||||
const name = document.getElementById('editProjectName').value.trim();
|
||||
const directory = document.getElementById('editProjectDir').value.trim();
|
||||
const portsStr = document.getElementById('editProjectPorts').value.trim();
|
||||
const healthUrl = document.getElementById('editProjectHealthUrl').value.trim();
|
||||
const adminUrl = document.getElementById('editProjectAdminUrl').value.trim();
|
||||
const gitRepo = document.getElementById('editProjectGitRepo').value.trim();
|
||||
const version = document.getElementById('editProjectVersion').value.trim();
|
||||
const logFile = document.getElementById('editProjectLogFile').value.trim();
|
||||
const startCmd = document.getElementById('editProjectStartCmd').value.trim();
|
||||
const stopCmd = document.getElementById('editProjectStopCmd').value.trim();
|
||||
const description = document.getElementById('editProjectDesc').value.trim();
|
||||
|
||||
if (!name) { alert('请输入项目名称'); return; }
|
||||
|
||||
const ports = portsStr ? portsStr.split(',').map(p => parseInt(p.trim())).filter(p => !isNaN(p)) : [];
|
||||
|
||||
const data = {
|
||||
name,
|
||||
ports,
|
||||
directory,
|
||||
start_cmd: startCmd,
|
||||
stop_cmd: stopCmd,
|
||||
health_url: healthUrl,
|
||||
admin_url: adminUrl,
|
||||
git_repo: gitRepo,
|
||||
log_file: logFile,
|
||||
version,
|
||||
description
|
||||
};
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/projects/${id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
const result = await res.json();
|
||||
if (result.error) {
|
||||
alert('保存失败: ' + result.error);
|
||||
} else {
|
||||
alert(result.message || '保存成功');
|
||||
closeEditProjectModal();
|
||||
loadProjects();
|
||||
}
|
||||
} catch (e) {
|
||||
alert('保存失败: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Cron 管理 ====================
|
||||
|
||||
async function loadCronTasks() {
|
||||
@@ -2750,6 +3174,11 @@
|
||||
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = setInterval(() => { if (currentTab === 'projects') loadProjects(); }, seconds * 1000);
|
||||
|
||||
// 如果当前在系统Tab且非实时模式,更新系统刷新间隔
|
||||
if (currentTab === 'system' && !document.getElementById('realtimeCheck').checked) {
|
||||
startSystemRefresh();
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(() => {
|
||||
@@ -2796,7 +3225,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 +3257,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;
|
||||
@@ -2904,7 +3333,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">
|
||||
|
||||
@@ -50,9 +50,11 @@ def load_projects_full():
|
||||
|
||||
|
||||
def save_projects(projects):
|
||||
"""保存项目配置"""
|
||||
"""保存项目配置(保留 server 和 external_services)"""
|
||||
full_data = load_projects_full()
|
||||
full_data['projects'] = projects
|
||||
with open(PROJECTS_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump({'projects': projects}, f, ensure_ascii=False, indent=2)
|
||||
json.dump(full_data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
|
||||
def check_port(port):
|
||||
|
||||
Executable
+24
@@ -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