后端 status 字段从字符串改为结构化对象 {status, ports: {port: {running}}}
前端期望的是 p.status.status 和 p.status.ports[port].running,之前的字符串格式导致:
- 所有服务状态始终显示「未知」
- 所有端口始终显示红色(已停止)
242 lines
8.3 KiB
Python
242 lines
8.3 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
项目管理 API 路由
|
||
"""
|
||
|
||
import json
|
||
from flask import jsonify, request
|
||
from config import PROJECTS_FILE
|
||
from utils import (
|
||
log_message, load_projects, load_projects_full, save_projects,
|
||
check_port, stop_process, run_command
|
||
)
|
||
|
||
|
||
def register_project_routes(app):
|
||
"""注册项目管理相关路由"""
|
||
|
||
@app.route('/api/projects')
|
||
def api_projects():
|
||
"""获取项目列表"""
|
||
projects = load_projects()
|
||
|
||
# 添加状态信息(结构化对象,前端期望 status.status 和 status.ports)
|
||
for project in projects:
|
||
ports = project.get('ports', [])
|
||
port_statuses = {}
|
||
for p in ports:
|
||
port_statuses[str(p)] = {'running': check_port(p)}
|
||
|
||
running_count = sum(1 for ps in port_statuses.values() if ps['running'])
|
||
if running_count == len(ports) and len(ports) > 0:
|
||
overall = 'running'
|
||
elif running_count > 0:
|
||
overall = 'partial'
|
||
else:
|
||
overall = 'stopped'
|
||
|
||
project['status'] = {
|
||
'status': overall,
|
||
'ports': port_statuses
|
||
}
|
||
|
||
return jsonify({
|
||
'projects': projects,
|
||
'server': load_projects_full().get('server', {})
|
||
})
|
||
|
||
@app.route('/api/projects/<project_id>/start', methods=['POST'])
|
||
def api_start(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
|
||
|
||
if project['type'] != 'web':
|
||
return jsonify({'error': '只有Web服务支持启动'}), 400
|
||
|
||
cwd = project.get('directory', '')
|
||
if not cwd:
|
||
return jsonify({'error': '项目目录未配置'}), 400
|
||
|
||
from config import WORKSPACE_DIR
|
||
import os
|
||
cwd = os.path.join(WORKSPACE_DIR, cwd) if not cwd.startswith('/') else cwd
|
||
|
||
if not os.path.exists(cwd):
|
||
return jsonify({'error': f'目录不存在: {cwd}'}), 400
|
||
|
||
if project.get('start_cmds'):
|
||
for name, info in project['start_cmds'].items():
|
||
run_command(info['cmd'], cwd, project_id, f'start-{name}')
|
||
else:
|
||
cmd = project.get('start_cmd', 'python3 app.py')
|
||
run_command(cmd, cwd, project_id, 'start')
|
||
|
||
return jsonify({'message': '服务启动中...'})
|
||
|
||
@app.route('/api/projects/<project_id>/stop', methods=['POST'])
|
||
def api_stop(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
|
||
|
||
if project['type'] != 'web':
|
||
return jsonify({'error': '只有Web服务支持停止'}), 400
|
||
|
||
ports = project.get('ports', [])
|
||
stopped = 0
|
||
for port in ports:
|
||
stopped += stop_process(port)
|
||
|
||
return jsonify({'message': f'已停止 {stopped} 个进程'})
|
||
|
||
@app.route('/api/projects/<project_id>/restart', methods=['POST'])
|
||
def api_restart(project_id):
|
||
"""重启服务"""
|
||
# 先停止
|
||
api_stop(project_id)
|
||
|
||
# 等待1秒
|
||
import time
|
||
time.sleep(1)
|
||
|
||
# 再启动
|
||
return api_start(project_id)
|
||
|
||
@app.route('/api/projects/add', methods=['POST'])
|
||
def api_add_project():
|
||
"""添加新项目"""
|
||
data = request.get_json()
|
||
|
||
if not data.get('id') or not data.get('name') or not data.get('ports'):
|
||
return jsonify({'error': '缺少必要字段'}), 400
|
||
|
||
projects = load_projects()
|
||
|
||
# 检查ID是否已存在
|
||
if any(p['id'] == data['id'] for p in projects):
|
||
return jsonify({'error': '项目ID已存在'}), 400
|
||
|
||
# 添加项目
|
||
project = {
|
||
'id': data['id'],
|
||
'name': data['name'],
|
||
'type': data.get('type', 'web'),
|
||
'ports': data['ports'],
|
||
'directory': data.get('directory', ''),
|
||
'start_cmd': data.get('start_cmd', ''),
|
||
'stop_cmd': data.get('stop_cmd', ''),
|
||
'health_url': data.get('health_url', ''),
|
||
'description': data.get('description', ''),
|
||
'git_repo': data.get('git_repo', ''),
|
||
'version': data.get('version', 'v1.0.0')
|
||
}
|
||
|
||
projects.append(project)
|
||
save_projects(projects)
|
||
|
||
log_message(f"添加项目: {project['name']}")
|
||
|
||
return jsonify({'message': '项目添加成功', 'project': project})
|
||
|
||
@app.route('/api/projects/<project_id>', methods=['PUT'])
|
||
def api_update_project(project_id):
|
||
"""更新项目配置"""
|
||
data = request.get_json()
|
||
projects = load_projects()
|
||
|
||
project = next((p for p in projects if p['id'] == project_id), None)
|
||
if not project:
|
||
return jsonify({'error': '项目不存在'}), 404
|
||
|
||
# 更新字段
|
||
for key in ['name', 'ports', 'directory', 'start_cmd', 'stop_cmd', 'health_url', 'description', 'git_repo', 'version']:
|
||
if key in data:
|
||
project[key] = data[key]
|
||
|
||
save_projects(projects)
|
||
|
||
log_message(f"更新项目: {project['name']}")
|
||
|
||
return jsonify({'message': '项目更新成功'})
|
||
|
||
@app.route('/api/projects/<project_id>', methods=['DELETE'])
|
||
def api_delete_project(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
|
||
|
||
projects = [p for p in projects if p['id'] != project_id]
|
||
save_projects(projects)
|
||
|
||
log_message(f"删除项目: {project['name']}")
|
||
|
||
return jsonify({'message': '项目删除成功'})
|
||
|
||
@app.route('/api/external-services')
|
||
def api_external_services():
|
||
"""获取外部服务列表"""
|
||
data = load_projects_full()
|
||
services = data.get('external_services', [])
|
||
|
||
# 添加状态(结构化对象,前端期望 status.status 和 status.ports)
|
||
for service in services:
|
||
ports = service.get('ports', [])
|
||
port_statuses = {}
|
||
for p in ports:
|
||
port_statuses[str(p)] = {'running': check_port(p)}
|
||
|
||
running_count = sum(1 for ps in port_statuses.values() if ps['running'])
|
||
if running_count == len(ports) and len(ports) > 0:
|
||
overall = 'running'
|
||
elif running_count > 0:
|
||
overall = 'partial'
|
||
else:
|
||
overall = 'stopped'
|
||
|
||
service['status'] = {
|
||
'status': overall,
|
||
'ports': port_statuses
|
||
}
|
||
|
||
return jsonify({'services': services})
|
||
|
||
@app.route('/api/external-services/add', methods=['POST'])
|
||
def api_add_external_service():
|
||
"""添加外部服务"""
|
||
data = request.get_json()
|
||
|
||
if not data.get('id') or not data.get('name') or not data.get('ports'):
|
||
return jsonify({'error': '缺少必要字段'}), 400
|
||
|
||
full_data = load_projects_full()
|
||
services = full_data.get('external_services', [])
|
||
|
||
# 检查ID是否已存在
|
||
if any(s['id'] == data['id'] for s in services):
|
||
return jsonify({'error': '服务ID已存在'}), 400
|
||
|
||
# 添加服务
|
||
service = {
|
||
'id': data['id'],
|
||
'name': data['name'],
|
||
'type': 'external',
|
||
'ports': data['ports'],
|
||
'health_url': data.get('health_url', ''),
|
||
'description': data.get('description', '')
|
||
}
|
||
|
||
services.append(service)
|
||
full_data['external_services'] = services
|
||
|
||
with open(PROJECTS_FILE, 'w', encoding='utf-8') as f:
|
||
json.dump(full_data, f, ensure_ascii=False, indent=2)
|
||
|
||
log_message(f"添加外部服务: {service['name']}")
|
||
|
||
return jsonify({'message': '服务添加成功', 'service': service}) |