add web
This commit is contained in:
292
app.py
292
app.py
@@ -4,11 +4,12 @@
|
||||
import os
|
||||
import logging
|
||||
from typing import Dict, Tuple, Any
|
||||
import json
|
||||
|
||||
|
||||
os.environ.setdefault('FLASK_RUN_HOST', '0.0.0.0')
|
||||
|
||||
from flask import Flask, request, jsonify
|
||||
from flask import Flask, request, jsonify, send_from_directory
|
||||
import yaml
|
||||
from webhook.handler import GiteaWebhookHandler
|
||||
from scanner.python_scanner import PythonScanner
|
||||
@@ -17,6 +18,8 @@ from scanner.security_scanner import SecurityScanner
|
||||
from scanner.ai_reviewer import AIReviewer
|
||||
from report.generator import ReportGenerator
|
||||
from notify.feishu import FeishuNotifier
|
||||
from gitea_client import GiteaClient
|
||||
from db import PRScanDB
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
@@ -47,6 +50,7 @@ security_scanner = SecurityScanner(config.get('scanner', {}))
|
||||
ai_reviewer = AIReviewer(config.get('ai', {}))
|
||||
report_generator = ReportGenerator(config.get('report', {}))
|
||||
feishu_notifier = FeishuNotifier(config['feishu'])
|
||||
gitea_client = GiteaClient(config['gitea'])
|
||||
|
||||
|
||||
@app.route('/')
|
||||
@@ -171,12 +175,6 @@ def handle_gitea_webhook():
|
||||
def handle_pull_request(payload: Dict[str, Any]) -> Tuple[Dict, int]:
|
||||
"""
|
||||
处理 Pull Request 事件
|
||||
|
||||
Args:
|
||||
payload: Webhook payload
|
||||
|
||||
Returns:
|
||||
JSON 响应和状态码
|
||||
"""
|
||||
try:
|
||||
# 解析 PR 事件
|
||||
@@ -244,12 +242,25 @@ def handle_pull_request(payload: Dict[str, Any]) -> Tuple[Dict, int]:
|
||||
author=author,
|
||||
scan_results=scan_results,
|
||||
pr_url=pr_url,
|
||||
target_branch=target_branch
|
||||
target_branch=target_branch,
|
||||
pr_number=pr_number
|
||||
)
|
||||
|
||||
# 发送飞书通知
|
||||
feishu_notifier.send_report(report)
|
||||
|
||||
# 保存扫描结果到数据库
|
||||
pr_info_for_db = {
|
||||
'repo_name': repo_name,
|
||||
'pr_number': pr_number,
|
||||
'pr_title': pr_title,
|
||||
'pr_url': pr_url,
|
||||
'source_branch': source_branch,
|
||||
'target_branch': target_branch,
|
||||
'author': author
|
||||
}
|
||||
PRScanDB.save_pr_scan(pr_info_for_db, scan_results, report.get('file_path'))
|
||||
|
||||
logger.info(f'PR #{pr_number} 扫描完成')
|
||||
|
||||
except Exception as e:
|
||||
@@ -306,10 +317,273 @@ def manual_scan():
|
||||
}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'手动扫描失败: {str(e)}', exc_info=True)
|
||||
logger.error(f'手动扫描失败: {str(e)}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/feishu/card_action', methods=['POST'])
|
||||
def handle_feishu_card_action():
|
||||
"""处理飞书卡片按钮点击事件"""
|
||||
try:
|
||||
payload = request.json
|
||||
logger.info(f'收到飞书卡片回调: {payload}')
|
||||
|
||||
# 处理 URL 验证请求
|
||||
challenge = payload.get('challenge')
|
||||
if challenge:
|
||||
logger.info('处理 URL 验证请求')
|
||||
return jsonify({'challenge': challenge}), 200
|
||||
|
||||
# 解析回调数据
|
||||
action_data = payload.get('action', {})
|
||||
if not action_data:
|
||||
action_data = payload.get('value', {})
|
||||
|
||||
action_type = action_data.get('action')
|
||||
owner = action_data.get('owner')
|
||||
repo = action_data.get('repo')
|
||||
pr_number = action_data.get('pr_number')
|
||||
pr_url = action_data.get('pr_url')
|
||||
|
||||
if not all([action_type, owner, repo, pr_number]):
|
||||
logger.error('卡片回调数据不完整')
|
||||
return jsonify({'error': 'Missing required parameters'}), 400
|
||||
|
||||
logger.info(f'执行操作: {action_type}, PR: {owner}/{repo}#{pr_number}')
|
||||
|
||||
# 执行对应操作
|
||||
if action_type == 'merge':
|
||||
success = gitea_client.merge_pull_request(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
pr_number=int(pr_number),
|
||||
merge_message=f'通过飞书机器人合并 PR #{pr_number}'
|
||||
)
|
||||
result_message = '✅ **已合并 PR**' if success else '❌ **合并失败**'
|
||||
elif action_type == 'close':
|
||||
success = gitea_client.close_pull_request(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
pr_number=int(pr_number)
|
||||
)
|
||||
result_message = '✅ **已关闭 PR(取消合并)**' if success else '❌ **关闭失败**'
|
||||
else:
|
||||
result_message = f'⚠️ **未知操作: {action_type}**'
|
||||
|
||||
# 发送操作结果到飞书
|
||||
result_text = f"{result_message}\n\n**PR:** {owner}/{repo}#{pr_number}\n**链接:** [查看PR]({pr_url})"
|
||||
feishu_notifier.send_simple_message('PR 操作结果', result_text)
|
||||
|
||||
return jsonify({'status': 'ok', 'message': result_message}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'处理飞书卡片回调失败: {str(e)}', exc_info=True)
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/feishu/webhook', methods=['POST'])
|
||||
def handle_feishu_webhook():
|
||||
"""处理飞书开放平台的验证回调"""
|
||||
try:
|
||||
payload = request.json
|
||||
|
||||
# 处理验证请求
|
||||
challenge = payload.get('challenge')
|
||||
if challenge:
|
||||
return jsonify({'challenge': challenge}), 200
|
||||
|
||||
# 处理消息事件
|
||||
event_type = payload.get('type')
|
||||
if event_type == 'url_verification':
|
||||
return jsonify({'challenge': payload.get('challenge')}), 200
|
||||
|
||||
logger.info(f'收到飞书事件: {event_type}')
|
||||
return jsonify({'status': 'ok'}), 200
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'处理飞书 Webhook 失败: {str(e)}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================
|
||||
# 扫描管理平台 API
|
||||
# ============================================
|
||||
|
||||
@app.route('/api/prs')
|
||||
def api_get_prs():
|
||||
"""获取所有 PR 列表"""
|
||||
try:
|
||||
state = request.args.get('state')
|
||||
prs = PRScanDB.get_all_prs(state=state)
|
||||
|
||||
# 转换 scan_result 字符串为对象
|
||||
for pr in prs:
|
||||
if pr.get('scan_result') and isinstance(pr['scan_result'], str):
|
||||
try:
|
||||
pr['scan_result'] = json.loads(pr['scan_result'])
|
||||
except:
|
||||
pass
|
||||
if pr.get('ai_review') and isinstance(pr['ai_review'], str):
|
||||
try:
|
||||
pr['ai_review'] = json.loads(pr['ai_review'])
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify(prs)
|
||||
except Exception as e:
|
||||
logger.error(f'获取 PR 列表失败: {str(e)}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prs/<int:pr_id>')
|
||||
def api_get_pr(pr_id):
|
||||
"""获取单个 PR 详情"""
|
||||
try:
|
||||
pr = PRScanDB.get_pr_by_id(pr_id)
|
||||
if not pr:
|
||||
return jsonify({'error': 'PR not found'}), 404
|
||||
|
||||
# 转换 JSON 字段
|
||||
if pr.get('scan_result') and isinstance(pr['scan_result'], str):
|
||||
try:
|
||||
pr['scan_result'] = json.loads(pr['scan_result'])
|
||||
except:
|
||||
pass
|
||||
if pr.get('ai_review') and isinstance(pr['ai_review'], str):
|
||||
try:
|
||||
pr['ai_review'] = json.loads(pr['ai_review'])
|
||||
except:
|
||||
pass
|
||||
|
||||
return jsonify(pr)
|
||||
except Exception as e:
|
||||
logger.error(f'获取 PR 详情失败: {str(e)}')
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prs/<int:pr_id>/merge', methods=['POST'])
|
||||
def api_merge_pr(pr_id):
|
||||
"""合并 PR"""
|
||||
try:
|
||||
pr = PRScanDB.get_pr_by_id(pr_id)
|
||||
if not pr:
|
||||
return jsonify({'success': False, 'message': 'PR not found'}), 404
|
||||
|
||||
logger.info(f"合并 PR - 数据库记录: {pr}")
|
||||
|
||||
if pr['state'] != 'open':
|
||||
return jsonify({'success': False, 'message': 'PR 状态不是 open'}), 400
|
||||
|
||||
# 解析仓库名
|
||||
repo_name = pr['repo_name']
|
||||
logger.info(f"仓库名称: {repo_name}")
|
||||
|
||||
if '/' in repo_name:
|
||||
owner, repo = repo_name.split('/')
|
||||
else:
|
||||
owner = ''
|
||||
repo = repo_name
|
||||
|
||||
logger.info(f"owner: {owner}, repo: {repo}, pr_number: {pr['pr_number']}")
|
||||
|
||||
# 先检查 PR 状态
|
||||
pr_info = gitea_client.get_pull_request(owner, repo, pr['pr_number'])
|
||||
if not pr_info:
|
||||
return jsonify({'success': False, 'message': '无法获取 PR 信息,请检查仓库名称是否正确'}), 400
|
||||
|
||||
logger.info(f"PR 信息: state={pr_info.get('state')}, mergeable={pr_info.get('mergeable')}")
|
||||
|
||||
if pr_info.get('state') != 'open':
|
||||
return jsonify({'success': False, 'message': f'PR 状态是 {pr_info.get("state")}, 不是 open'}), 400
|
||||
|
||||
# 调用 Gitea API 合并
|
||||
success = gitea_client.merge_pull_request(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
pr_number=pr['pr_number'],
|
||||
merge_message=f'通过管理平台合并 PR #{pr["pr_number"]}'
|
||||
)
|
||||
|
||||
if success:
|
||||
# 更新数据库状态
|
||||
PRScanDB.update_pr_state(pr_id, 'merged', merged_by='admin')
|
||||
|
||||
# 发送飞书通知
|
||||
result_text = f"✅ **PR 已通过管理平台合并**\n\n**PR:** {repo_name}#{pr['pr_number']}\n**标题:** {pr['pr_title']}\n**合并人:** 管理员"
|
||||
feishu_notifier.send_simple_message('PR 合并', result_text)
|
||||
|
||||
return jsonify({'success': True, 'message': 'PR 已合并'})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': '合并失败'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'合并 PR 失败: {str(e)}')
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
@app.route('/api/prs/<int:pr_id>/close', methods=['POST'])
|
||||
def api_close_pr(pr_id):
|
||||
"""关闭 PR"""
|
||||
try:
|
||||
pr = PRScanDB.get_pr_by_id(pr_id)
|
||||
if not pr:
|
||||
return jsonify({'success': False, 'message': 'PR not found'}), 404
|
||||
|
||||
if pr['state'] != 'open':
|
||||
return jsonify({'success': False, 'message': 'PR 状态不是 open'}), 400
|
||||
|
||||
# 解析仓库名
|
||||
repo_name = pr['repo_name']
|
||||
if '/' in repo_name:
|
||||
owner, repo = repo_name.split('/')
|
||||
else:
|
||||
owner = ''
|
||||
repo = repo_name
|
||||
|
||||
# 调用 Gitea API 关闭
|
||||
success = gitea_client.close_pull_request(
|
||||
owner=owner,
|
||||
repo=repo,
|
||||
pr_number=pr['pr_number']
|
||||
)
|
||||
|
||||
if success:
|
||||
# 更新数据库状态
|
||||
PRScanDB.update_pr_state(pr_id, 'closed')
|
||||
|
||||
# 发送飞书通知
|
||||
result_text = f"❌ **PR 已被管理平台拒绝**\n\n**PR:** {repo_name}#{pr['pr_number']}\n**标题:** {pr['pr_title']}"
|
||||
feishu_notifier.send_simple_message('PR 拒绝', result_text)
|
||||
|
||||
return jsonify({'success': True, 'message': 'PR 已关闭'})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': '关闭失败'}), 500
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'关闭 PR 失败: {str(e)}')
|
||||
return jsonify({'success': False, 'message': str(e)}), 500
|
||||
|
||||
|
||||
# ============================================
|
||||
# 扫描管理平台页面
|
||||
# ============================================
|
||||
|
||||
# 获取 web 目录的绝对路径
|
||||
WEB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'web')
|
||||
|
||||
|
||||
@app.route('/dashboard')
|
||||
def dashboard():
|
||||
"""扫描管理平台首页"""
|
||||
return send_from_directory(WEB_DIR, 'index.html')
|
||||
|
||||
|
||||
@app.route('/web/<path:filename>')
|
||||
def serve_static(filename):
|
||||
"""提供静态文件服务"""
|
||||
return send_from_directory(WEB_DIR, filename)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# 强制监听所有网络接口
|
||||
host = "0.0.0.0"
|
||||
|
||||
Reference in New Issue
Block a user