295 lines
11 KiB
Python
295 lines
11 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():
|
||
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': '服务启动中...'})
|
||
|
||
@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', ''),
|
||
'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')
|
||
}
|
||
|
||
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', 'log_file', 'start_cmd', 'stop_cmd', 'health_url', 'admin_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>/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):
|
||
"""删除项目"""
|
||
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}) |