#!/usr/bin/env python3 """ 智队中枢 Webhook 服务 接收 Gitea Webhook 请求,触发自动更新 """ import os import hmac import hashlib import subprocess import logging from flask import Flask, request, jsonify from datetime import datetime app = Flask(__name__) # 配置 WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', 'pit-router-webhook-secret-2026') UPDATE_SCRIPT = '/www/wwwroot/pit-router/auto-update.sh' LOG_FILE = '/var/log/pit-router-webhook.log' # 日志配置 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(LOG_FILE), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) def verify_signature(payload: bytes, signature: str) -> bool: """验证 Gitea Webhook 签名""" if not signature: return False expected = 'sha256=' + hmac.new( WEBHOOK_SECRET.encode(), payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, signature) def log_request(event: str, data: dict): """记录请求日志""" ref = data.get('ref', '') commits = data.get('commits', []) pusher = data.get('pusher', {}).get('login', 'unknown') logger.info(f"Event: {event} | Ref: {ref} | Pusher: {pusher} | Commits: {len(commits)}") for commit in commits[:3]: # 只记录前3个 commit msg = commit.get('message', '').split('\n')[0][:50] logger.info(f" - {commit.get('id', '')[:7]}: {msg}") @app.route('/health', methods=['GET']) def health(): """健康检查端点""" return jsonify({'status': 'ok', 'service': 'pit-router-webhook'}) @app.route('/webhook/pit-router', methods=['POST']) def handle_webhook(): """处理 Gitea Webhook""" try: # 1. 验证签名 signature = request.headers.get('X-Gitea-Signature', '') if not verify_signature(request.data, signature): logger.warning("Invalid signature") return jsonify({'error': 'Invalid signature'}), 401 # 2. 解析事件 event = request.headers.get('X-Gitea-Event', '') data = request.json or {} # 记录请求 log_request(event, data) # 3. 只处理 push 事件 if event != 'push': logger.info(f"Event '{event}' ignored") return jsonify({'message': f'Event {event} ignored'}), 200 # 4. 只处理 main 分支 ref = data.get('ref', '') if ref != 'refs/heads/main': logger.info(f"Branch '{ref}' ignored") return jsonify({'message': f'Branch {ref} ignored'}), 200 # 5. 执行更新脚本 logger.info("🚀 Triggering auto-update...") result = subprocess.run( [UPDATE_SCRIPT], capture_output=True, text=True, timeout=600 # 10 分钟超时 ) # 记录输出 if result.stdout: logger.info(f"Update output:\n{result.stdout[-2000:]}") # 只记录最后 2000 字符 if result.stderr: logger.error(f"Update error:\n{result.stderr[-1000:]}") if result.returncode == 0: logger.info("✅ Update completed successfully") return jsonify({ 'status': 'success', 'message': 'Update completed', 'timestamp': datetime.utcnow().isoformat() }) else: logger.error(f"❌ Update failed with code {result.returncode}") return jsonify({ 'status': 'error', 'message': 'Update failed', 'details': result.stderr[-500:] if result.stderr else 'Unknown error' }), 500 except subprocess.TimeoutExpired: logger.error("Update timeout") return jsonify({'error': 'Update timeout'}), 500 except Exception as e: logger.exception(f"Unexpected error: {e}") return jsonify({'error': str(e)}), 500 @app.route('/webhook/test', methods=['GET']) def test_webhook(): """测试端点(手动触发更新)""" try: logger.info("🧪 Manual test trigger") result = subprocess.run( [UPDATE_SCRIPT], capture_output=True, text=True, timeout=600 ) return jsonify({ 'status': 'success' if result.returncode == 0 else 'error', 'output': result.stdout[-1000:], 'error': result.stderr[-500:] if result.stderr else None }) except Exception as e: return jsonify({'error': str(e)}), 500 if __name__ == '__main__': logger.info("🚀 Starting PIT Router Webhook Server on port 5001") app.run(host='0.0.0.0', port=5001, debug=False)