#!/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//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//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//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/', 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//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/', 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})