Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
070044f131 | ||
|
|
201d31d25e | ||
|
|
40213494df | ||
|
|
a155e8fcd9 | ||
|
|
0c54af8d26 | ||
|
|
f7722be539 | ||
|
|
779f42c98c | ||
|
|
525620b0f8 | ||
|
|
ebc8d687d0 | ||
|
|
294526b403 | ||
|
|
78a4a3f1aa | ||
|
|
27a84de317 | ||
|
|
89b5a69325 | ||
|
|
23810cf180 | ||
|
|
16400b0359 |
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+3
-2
@@ -1,9 +1,10 @@
|
||||
{
|
||||
"thresholds": {
|
||||
"cpu": 75,
|
||||
"memory": 80,
|
||||
"disk": 85,
|
||||
"interval": 60
|
||||
"interval": 60,
|
||||
"memory": 80,
|
||||
"cpu_temp": 70
|
||||
},
|
||||
"web_settings": {
|
||||
"external_ip": "121.40.164.32",
|
||||
|
||||
@@ -13,6 +13,8 @@ from routes_system import register_system_routes
|
||||
from routes_config import register_config_routes
|
||||
from routes_email import register_email_routes
|
||||
from routes_project import register_project_routes
|
||||
from routes_docker import register_docker_routes
|
||||
from routes_discovery import register_discovery_routes
|
||||
from templates import get_html_template
|
||||
|
||||
|
||||
@@ -31,6 +33,8 @@ def init_app():
|
||||
register_config_routes(app)
|
||||
register_email_routes(app)
|
||||
register_project_routes(app)
|
||||
register_docker_routes(app)
|
||||
register_discovery_routes(app)
|
||||
|
||||
# 主页路由
|
||||
@app.route('/')
|
||||
|
||||
+446228
-30330
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 精排"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
服务发现 API 路由 - 扫描本机未注册的服务
|
||||
方案C: 端口扫描 + 进程信息增强
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
import subprocess
|
||||
from flask import jsonify, request
|
||||
from utils import log_message
|
||||
from config import PROJECTS_FILE
|
||||
|
||||
|
||||
# 系统保留/常见基础服务端口,不纳入发现
|
||||
SYSTEM_PORTS = {
|
||||
22, # SSH
|
||||
25, # SMTP
|
||||
53, # DNS
|
||||
80, # HTTP (nginx/apache 前端)
|
||||
111, # portmapper
|
||||
135, # RPC
|
||||
137, # NetBIOS
|
||||
138, # NetBIOS
|
||||
139, # NetBIOS
|
||||
443, # HTTPS
|
||||
445, # SMB
|
||||
587, # SMTP submission
|
||||
631, # CUPS
|
||||
993, # IMAPS
|
||||
995, # POP3S
|
||||
3306, # MySQL
|
||||
3389, # RDP
|
||||
5432, # PostgreSQL
|
||||
6379, # Redis
|
||||
8080, # 常见代理
|
||||
8443, # 常见 HTTPS 代理
|
||||
9090, # Prometheus
|
||||
3000, # 常见开发端口
|
||||
4200, # Angular dev
|
||||
5000, # Flask dev
|
||||
8000, # 常见开发
|
||||
9000, # 常见开发
|
||||
}
|
||||
|
||||
|
||||
def _get_listening_ports():
|
||||
"""获取所有TCP监听端口及进程信息"""
|
||||
services = []
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['ss', '-tlnp'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
for line in result.stdout.split('\n'):
|
||||
line = line.strip()
|
||||
if not line or 'State' in line or 'Local' in line:
|
||||
continue
|
||||
|
||||
# 解析: LISTEN 0 128 0.0.0.0:16022 0.0.0.0:* users:(("python3.12",pid=123,fd=3))
|
||||
if '127.0.0.1' in line and '0.0.0.0' not in line.split()[3] and '*' not in line.split()[3]:
|
||||
# 跳过仅监听 localhost 的
|
||||
pass
|
||||
|
||||
# 提取端口和pid
|
||||
addr_part = line.split()[3] if len(line.split()) > 3 else ''
|
||||
port_match = re.search(r':(\d+)$', addr_part)
|
||||
if not port_match:
|
||||
continue
|
||||
port = int(port_match.group(1))
|
||||
|
||||
pid_match = re.search(r'pid=(\d+)', line)
|
||||
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 '未知进程'
|
||||
|
||||
services.append({
|
||||
'port': port,
|
||||
'pid': pid,
|
||||
'process': proc_name,
|
||||
})
|
||||
except Exception as e:
|
||||
log_message(f"端口扫描失败: {e}")
|
||||
|
||||
return services
|
||||
|
||||
|
||||
def _get_process_cmdline(pid):
|
||||
"""读取进程命令行"""
|
||||
try:
|
||||
cmdline_path = f'/proc/{pid}/cmdline'
|
||||
if os.path.exists(cmdline_path):
|
||||
with open(cmdline_path, 'rb') as f:
|
||||
raw = f.read()
|
||||
# cmdline 以 \0 分隔
|
||||
args = raw.split(b'\x00')
|
||||
# 解码,跳过空字符串
|
||||
cmd = ' '.join(a.decode('utf-8', errors='replace') for a in args if a)
|
||||
return cmd
|
||||
except:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
def _get_process_cwd(pid):
|
||||
"""读取进程工作目录"""
|
||||
try:
|
||||
cwd_path = f'/proc/{pid}/cwd'
|
||||
if os.path.exists(cwd_path):
|
||||
return os.readlink(cwd_path)
|
||||
except:
|
||||
pass
|
||||
return ''
|
||||
|
||||
|
||||
def _guess_service_name(pid, proc_name, cmdline):
|
||||
"""根据进程信息推测服务名"""
|
||||
# 已知框架特征
|
||||
patterns = [
|
||||
(r'python.*?app\.py', 'Python Web 服务'),
|
||||
(r'python.*?main\.py', 'Python 服务'),
|
||||
(r'python.*?server\.py', 'Python Server'),
|
||||
(r'node\s+.*?(server|app|index)\.js', 'Node.js 服务'),
|
||||
(r'npm\s+(start|run)', 'Node.js 服务'),
|
||||
(r'java\s+-jar\s+(\S+)', 'Java 服务'),
|
||||
(r'gunicorn', 'Gunicorn 服务'),
|
||||
(r'uvicorn', 'Uvicorn 服务'),
|
||||
(r'nginx', 'Nginx'),
|
||||
(r'php-fpm', 'PHP-FPM'),
|
||||
(r'mysqld', 'MySQL'),
|
||||
(r'redis-server', 'Redis'),
|
||||
(r'docker-proxy', 'Docker 代理'),
|
||||
(r'containerd', 'containerd'),
|
||||
]
|
||||
|
||||
for pattern, name in patterns:
|
||||
if re.search(pattern, cmdline, re.IGNORECASE):
|
||||
return name
|
||||
|
||||
# 默认使用进程名
|
||||
return proc_name
|
||||
|
||||
|
||||
def _guess_service_type(cmdline):
|
||||
"""推测服务类型"""
|
||||
if re.search(r'(python|gunicorn|uvicorn|flask|django)', cmdline, re.IGNORECASE):
|
||||
return 'web'
|
||||
if re.search(r'(node|npm|next|express)', cmdline, re.IGNORECASE):
|
||||
return 'web'
|
||||
if re.search(r'(java|spring|tomcat)', cmdline, re.IGNORECASE):
|
||||
return 'web'
|
||||
if re.search(r'(mysqld|postgres|redis|mongo)', cmdline, re.IGNORECASE):
|
||||
return 'external'
|
||||
return 'external'
|
||||
|
||||
|
||||
def register_discovery_routes(app):
|
||||
"""注册服务发现路由"""
|
||||
|
||||
@app.route('/api/discovery/services')
|
||||
def api_discovery_services():
|
||||
"""扫描本机未注册的新服务"""
|
||||
# 1. 收集已知端口
|
||||
known_ports = set()
|
||||
try:
|
||||
with open(PROJECTS_FILE, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
for proj in data.get('projects', []):
|
||||
for p in proj.get('ports', []):
|
||||
known_ports.add(p)
|
||||
for svc in data.get('external_services', []):
|
||||
for p in svc.get('ports', []):
|
||||
known_ports.add(p)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 2. 前端传入的额外排除端口(自定义服务等)
|
||||
exclude_str = request.args.get('exclude_ports', '')
|
||||
if exclude_str:
|
||||
try:
|
||||
for p in exclude_str.split(','):
|
||||
known_ports.add(int(p.strip()))
|
||||
except:
|
||||
pass
|
||||
|
||||
# 3. 获取 Docker 容器端口
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['docker', 'ps', '--format', '{{.Ports}}'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
for line in result.stdout.split('\n'):
|
||||
# 匹配 0.0.0.0:16001->7700/tcp 格式
|
||||
for m in re.finditer(r':(\d+)->', line):
|
||||
known_ports.add(int(m.group(1)))
|
||||
except:
|
||||
pass
|
||||
|
||||
# 3. 扫描所有监听端口
|
||||
all_services = _get_listening_ports()
|
||||
|
||||
# 4. 筛选未注册的
|
||||
undiscovered = []
|
||||
seen_ports = set()
|
||||
|
||||
for svc in all_services:
|
||||
port = svc['port']
|
||||
pid = svc['pid']
|
||||
|
||||
# 跳过已知/系统端口
|
||||
if port in known_ports or port in SYSTEM_PORTS:
|
||||
continue
|
||||
if port in seen_ports:
|
||||
continue
|
||||
seen_ports.add(port)
|
||||
|
||||
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,
|
||||
'pid': pid,
|
||||
'process': svc['process'],
|
||||
'cmdline': cmdline[:200] if cmdline else '',
|
||||
'cwd': cwd,
|
||||
'suggested_name': name,
|
||||
'suggested_type': svc_type,
|
||||
})
|
||||
|
||||
# 按端口排序
|
||||
undiscovered.sort(key=lambda x: x['port'])
|
||||
|
||||
return jsonify({
|
||||
'services': undiscovered,
|
||||
'count': len(undiscovered),
|
||||
'known_ports': len(known_ports),
|
||||
})
|
||||
@@ -0,0 +1,243 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Docker 监控 API 路由
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import json
|
||||
from flask import jsonify, request
|
||||
from utils import log_message
|
||||
|
||||
|
||||
def _run_docker(cmd, timeout=10):
|
||||
"""执行 docker 命令并返回解析结果"""
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
||||
if result.returncode != 0:
|
||||
return None, result.stderr.strip()
|
||||
return result.stdout.strip(), None
|
||||
except subprocess.TimeoutExpired:
|
||||
return None, '命令超时'
|
||||
except FileNotFoundError:
|
||||
return None, 'Docker 未安装'
|
||||
except Exception as e:
|
||||
return None, str(e)
|
||||
|
||||
|
||||
def register_docker_routes(app):
|
||||
"""注册 Docker 监控相关路由"""
|
||||
|
||||
@app.route('/api/docker/containers')
|
||||
def api_docker_containers():
|
||||
"""获取所有容器列表(含资源使用)"""
|
||||
# 获取容器列表
|
||||
fmt = '{{.ID}}|{{.Names}}|{{.Image}}|{{.State}}|{{.Status}}|{{.Ports}}|{{.CreatedAt}}|{{.RunningFor}}'
|
||||
out, err = _run_docker(['docker', 'ps', '-a', '--format', fmt, '--no-trunc'])
|
||||
if out is None:
|
||||
return jsonify({'error': err, 'containers': []})
|
||||
|
||||
containers = []
|
||||
for line in out.split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split('|')
|
||||
if len(parts) < 8:
|
||||
continue
|
||||
cid = parts[0][:12] # 缩短ID
|
||||
containers.append({
|
||||
'id': cid,
|
||||
'name': parts[1],
|
||||
'image': parts[2],
|
||||
'state': parts[3],
|
||||
'status': parts[4],
|
||||
'ports': parts[5],
|
||||
'created': parts[6],
|
||||
'runningFor': parts[7]
|
||||
})
|
||||
|
||||
# 批量获取运行中容器的stats
|
||||
running = [c for c in containers if c['state'] == 'running']
|
||||
if running:
|
||||
try:
|
||||
stats_out, _ = _run_docker([
|
||||
'docker', 'stats', '--no-stream', '--no-trunc',
|
||||
'--format', '{{.Name}}|{{.CPUPerc}}|{{.MemPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}'
|
||||
], timeout=15)
|
||||
if stats_out:
|
||||
stats_map = {}
|
||||
for line in stats_out.split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
sp = line.split('|')
|
||||
if len(sp) >= 7:
|
||||
stats_map[sp[0]] = {
|
||||
'cpuPercent': sp[1],
|
||||
'memPercent': sp[2],
|
||||
'memUsage': sp[3],
|
||||
'netIO': sp[4],
|
||||
'blockIO': sp[5],
|
||||
'pids': sp[6]
|
||||
}
|
||||
for c in containers:
|
||||
if c['name'] in stats_map:
|
||||
c['stats'] = stats_map[c['name']]
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify({'containers': containers})
|
||||
|
||||
@app.route('/api/docker/container/<container_id>/stats')
|
||||
def api_docker_container_stats(container_id):
|
||||
"""获取单个容器实时资源统计"""
|
||||
out, err = _run_docker([
|
||||
'docker', 'stats', '--no-stream', '--no-trunc',
|
||||
'--format', '{{.CPUPerc}}|{{.MemPerc}}|{{.MemUsage}}|{{.NetIO}}|{{.BlockIO}}|{{.PIDs}}',
|
||||
container_id
|
||||
], timeout=10)
|
||||
if out is None:
|
||||
return jsonify({'error': err}), 500
|
||||
if not out:
|
||||
return jsonify({'error': '容器不存在或未运行'}), 404
|
||||
|
||||
parts = out.split('|')
|
||||
return jsonify({
|
||||
'cpuPercent': parts[0] if len(parts) > 0 else '0%',
|
||||
'memPercent': parts[1] if len(parts) > 1 else '0%',
|
||||
'memUsage': parts[2] if len(parts) > 2 else '',
|
||||
'netIO': parts[3] if len(parts) > 3 else '',
|
||||
'blockIO': parts[4] if len(parts) > 4 else '',
|
||||
'pids': parts[5] if len(parts) > 5 else '0'
|
||||
})
|
||||
|
||||
@app.route('/api/docker/images')
|
||||
def api_docker_images():
|
||||
"""获取所有镜像列表"""
|
||||
fmt = '{{.Repository}}|{{.Tag}}|{{.ID}}|{{.Size}}|{{.CreatedAt}}|{{.Containers}}'
|
||||
out, err = _run_docker(['docker', 'images', '--format', fmt, '--no-trunc'])
|
||||
if out is None:
|
||||
return jsonify({'error': err, 'images': []})
|
||||
|
||||
images = []
|
||||
for line in out.split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split('|')
|
||||
if len(parts) < 6:
|
||||
continue
|
||||
repo = parts[0] if parts[0] != '<none>' else ''
|
||||
tag = parts[1] if parts[1] != '<none>' else ''
|
||||
images.append({
|
||||
'repository': repo,
|
||||
'tag': tag,
|
||||
'id': parts[2][:12],
|
||||
'size': parts[3],
|
||||
'created': parts[4],
|
||||
'containers': parts[5],
|
||||
'dangling': not repo and not tag
|
||||
})
|
||||
|
||||
return jsonify({'images': images})
|
||||
|
||||
@app.route('/api/docker/system')
|
||||
def api_docker_system():
|
||||
"""获取 Docker 系统信息"""
|
||||
# 版本信息
|
||||
ver_out, _ = _run_docker(['docker', 'version', '--format', '{{.Server.Version}}|{{.Server.Os}}|{{.Server.Arch}}'], timeout=5)
|
||||
|
||||
# 磁盘使用
|
||||
df_out, _ = _run_docker(['docker', 'system', 'df', '--format', '{{.Type}}|{{.TotalCount}}|{{.Active}}|{{.Size}}|{{.Reclaimable}}'], timeout=10)
|
||||
|
||||
system_info = {
|
||||
'version': '',
|
||||
'os': '',
|
||||
'arch': '',
|
||||
'diskUsage': []
|
||||
}
|
||||
|
||||
if ver_out:
|
||||
ver_parts = ver_out.split('|')
|
||||
if len(ver_parts) >= 3:
|
||||
system_info.update({
|
||||
'version': ver_parts[0],
|
||||
'os': ver_parts[1],
|
||||
'arch': ver_parts[2]
|
||||
})
|
||||
|
||||
if df_out:
|
||||
for line in df_out.split('\n'):
|
||||
if not line.strip():
|
||||
continue
|
||||
parts = line.split('|')
|
||||
if len(parts) >= 5:
|
||||
system_info['diskUsage'].append({
|
||||
'type': parts[0],
|
||||
'total': parts[1],
|
||||
'active': parts[2],
|
||||
'size': parts[3],
|
||||
'reclaimable': parts[4]
|
||||
})
|
||||
|
||||
return jsonify(system_info)
|
||||
|
||||
@app.route('/api/docker/container/<container_id>/logs')
|
||||
def api_docker_container_logs(container_id):
|
||||
"""获取容器日志(最近200行)"""
|
||||
lines = int(request.args.get('lines', 200))
|
||||
lines = max(10, min(1000, lines))
|
||||
|
||||
out, err = _run_docker(['docker', 'logs', '--tail', str(lines), container_id], timeout=10)
|
||||
if out is None:
|
||||
return jsonify({'error': err}), 500
|
||||
|
||||
return jsonify({'logs': out, 'lines': lines})
|
||||
|
||||
@app.route('/api/docker/container/<container_id>/start', methods=['POST'])
|
||||
def api_docker_container_start(container_id):
|
||||
"""启动容器"""
|
||||
out, err = _run_docker(['docker', 'start', container_id])
|
||||
if out is None and err:
|
||||
return jsonify({'error': err}), 500
|
||||
log_message(f"Docker 容器启动: {container_id}")
|
||||
return jsonify({'message': f'容器 {container_id} 已启动'})
|
||||
|
||||
@app.route('/api/docker/container/<container_id>/stop', methods=['POST'])
|
||||
def api_docker_container_stop(container_id):
|
||||
"""停止容器"""
|
||||
out, err = _run_docker(['docker', 'stop', container_id])
|
||||
if out is None and err:
|
||||
return jsonify({'error': err}), 500
|
||||
log_message(f"Docker 容器停止: {container_id}")
|
||||
return jsonify({'message': f'容器 {container_id} 已停止'})
|
||||
|
||||
@app.route('/api/docker/container/<container_id>/restart', methods=['POST'])
|
||||
def api_docker_container_restart(container_id):
|
||||
"""重启容器"""
|
||||
out, err = _run_docker(['docker', 'restart', container_id])
|
||||
if out is None and err:
|
||||
return jsonify({'error': err}), 500
|
||||
log_message(f"Docker 容器重启: {container_id}")
|
||||
return jsonify({'message': f'容器 {container_id} 已重启'})
|
||||
|
||||
@app.route('/api/docker/prune', methods=['POST'])
|
||||
def api_docker_prune():
|
||||
"""清理Docker资源"""
|
||||
target = request.json.get('target', 'all') if request.json else 'all'
|
||||
results = {}
|
||||
|
||||
# 清理停止的容器
|
||||
if target in ('all', 'containers'):
|
||||
out, _ = _run_docker(['docker', 'container', 'prune', '-f'], timeout=30)
|
||||
results['containers'] = out or '已清理'
|
||||
|
||||
# 清理悬空镜像
|
||||
if target in ('all', 'images'):
|
||||
out, _ = _run_docker(['docker', 'image', 'prune', '-f'], timeout=60)
|
||||
results['images'] = out or '已清理'
|
||||
|
||||
# 清理构建缓存
|
||||
if target in ('all', 'build'):
|
||||
out, _ = _run_docker(['docker', 'builder', 'prune', '-f'], timeout=60)
|
||||
results['build'] = out or '已清理'
|
||||
|
||||
log_message(f"Docker 资源清理完成: {target}")
|
||||
return jsonify({'message': '清理完成', 'results': results})
|
||||
+17
-8
@@ -18,7 +18,9 @@ def register_email_routes(app):
|
||||
def api_email_send():
|
||||
"""发送邮件通知"""
|
||||
data = request.get_json()
|
||||
log_message(f"📨 收到邮件请求: {data}")
|
||||
if not data:
|
||||
log_message(f"❌ 无效数据: content-type={request.content_type}")
|
||||
return jsonify({'error': '无效数据'}), 400
|
||||
|
||||
to_email = data.get('to')
|
||||
@@ -26,33 +28,40 @@ def register_email_routes(app):
|
||||
body = data.get('body')
|
||||
|
||||
if not to_email or not subject or not body:
|
||||
log_message(f"❌ 缺少参数: to={to_email}, subject={subject}, body_len={len(body) if body else 0}")
|
||||
return jsonify({'error': '缺少必要参数'}), 400
|
||||
|
||||
try:
|
||||
# SMTP配置 (从环境变量或默认值)
|
||||
smtp_host = os.environ.get('SMTP_HOST', 'mail.tphai.com')
|
||||
smtp_port = int(os.environ.get('SMTP_PORT', 587))
|
||||
smtp_user = os.environ.get('SMTP_USER', 'favor@tphai.com')
|
||||
smtp_pass = os.environ.get('SMTP_PASS', 'favor@!')
|
||||
smtp_user = os.environ.get('SMTP_USER', 'hz4th_coder@tphai.com')
|
||||
smtp_pass = os.environ.get('SMTP_PASS', 'hz4th_coder@!')
|
||||
|
||||
# 创建邮件
|
||||
log_message(f"📧 准备发送邮件: to={to_email}, subject={subject}, user={smtp_user}")
|
||||
|
||||
# 创建邮件(完全对齐 send_email.py 的方式)
|
||||
from email.utils import formataddr, formatdate
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = smtp_user
|
||||
msg['From'] = formataddr(('黄庄4号程序员', smtp_user))
|
||||
msg['To'] = to_email
|
||||
msg['Subject'] = subject
|
||||
msg['Date'] = formatdate(localtime=True)
|
||||
|
||||
# 添加正文
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
|
||||
# 发送邮件
|
||||
# 发送邮件(无SSL模式,完全对齐 send_email.py)
|
||||
server = smtplib.SMTP(smtp_host, smtp_port)
|
||||
server.ehlo()
|
||||
server.login(smtp_user, smtp_pass)
|
||||
server.send_message(msg)
|
||||
# 使用 sendmail 而非 send_message,与已验证成功的脚本一致
|
||||
server.sendmail(smtp_user, [to_email], msg.as_string())
|
||||
server.quit()
|
||||
|
||||
log_message(f"邮件发送成功: {subject} -> {to_email}")
|
||||
log_message(f"✅ 邮件发送成功: {subject} -> {to_email}")
|
||||
|
||||
return jsonify({'message': '邮件发送成功'})
|
||||
except Exception as e:
|
||||
log_message(f"邮件发送失败: {e}")
|
||||
log_message(f"❌ 邮件发送失败: {e}")
|
||||
return jsonify({'error': str(e)}), 500
|
||||
+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
@@ -23,6 +23,10 @@ def get_html_template():
|
||||
template_content = template_content.replace('v3.4.0', APP_VERSION)
|
||||
template_content = template_content.replace('v3.4.1', APP_VERSION) # 预留未来版本
|
||||
|
||||
# 替换默认IP地址
|
||||
template_content = template_content.replace("'192.168.2.17'", "'121.40.164.32'")
|
||||
template_content = template_content.replace('"192.168.2.17"', '"121.40.164.32"')
|
||||
|
||||
return template_content
|
||||
except:
|
||||
# 如果文件不存在,返回基本模板
|
||||
|
||||
+864
-51
File diff suppressed because it is too large
Load Diff
@@ -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