forked from Lesin/reposync
771 lines
31 KiB
Python
771 lines
31 KiB
Python
from fastapi import Request, HTTPException, WebSocket
|
||
from fastapi.responses import PlainTextResponse, HTMLResponse
|
||
from src.base.config import get_config
|
||
from src.router import WEBHOOK
|
||
from src.dao.issue_sync import SyncProjectDAO
|
||
from typing import Dict, Any, Optional
|
||
import requests
|
||
from datetime import datetime
|
||
import json
|
||
import hmac
|
||
import hashlib
|
||
from loguru import logger
|
||
from fastapi.responses import PlainTextResponse
|
||
import os
|
||
from src.utils.websocket_logger_fixed import manager, test_websocket_logging
|
||
|
||
# 设置日志配置
|
||
logger.add("logs/app.log", rotation="500 MB", retention="10 days")
|
||
|
||
def detect_webhook_source(data: dict, headers: dict) -> str:
|
||
"""检测webhook来源平台"""
|
||
# 通过请求头判断
|
||
if headers.get("X-Gitee-Token"):
|
||
return "Gitee"
|
||
elif headers.get("X-Hub-Signature-256"):
|
||
return "GitHub"
|
||
elif headers.get("X-Gitlink-Signature"):
|
||
return "GitLink"
|
||
|
||
# 通过数据结构判断
|
||
if "repository" in data:
|
||
if data["repository"].get("owner", {}).get("avatar_url"):
|
||
avatar_url = data["repository"]["owner"]["avatar_url"]
|
||
if "gitee" in avatar_url:
|
||
return "Gitee"
|
||
elif "github" in avatar_url:
|
||
return "GitHub"
|
||
elif "project" in data:
|
||
return "GitLink"
|
||
|
||
return "Unknown"
|
||
|
||
def get_webhook_event_info(data: dict, source: str) -> dict:
|
||
"""获取webhook事件详细信息"""
|
||
event_info = {
|
||
"source": source,
|
||
"event_type": "unknown",
|
||
"action": data.get("action", "unknown"),
|
||
"repository": "unknown",
|
||
"user": "unknown",
|
||
"title": "",
|
||
"number": 0
|
||
}
|
||
|
||
try:
|
||
if source in ["GitHub", "Gitee"]:
|
||
# GitHub/Gitee格式
|
||
repo = data.get("repository", {})
|
||
event_info["repository"] = repo.get("full_name", repo.get("name", "unknown"))
|
||
|
||
if "issue" in data:
|
||
event_info["event_type"] = "issue"
|
||
issue = data["issue"]
|
||
event_info["title"] = issue.get("title", "")
|
||
event_info["number"] = issue.get("number", 0)
|
||
event_info["user"] = issue.get("user", {}).get("login", "unknown")
|
||
|
||
elif "pull_request" in data:
|
||
event_info["event_type"] = "pull_request"
|
||
pr = data["pull_request"]
|
||
event_info["title"] = pr.get("title", "")
|
||
event_info["number"] = pr.get("number", 0)
|
||
event_info["user"] = pr.get("user", {}).get("login", "unknown")
|
||
|
||
elif "comment" in data:
|
||
event_info["event_type"] = "comment"
|
||
comment = data["comment"]
|
||
event_info["user"] = comment.get("user", {}).get("login", "unknown")
|
||
|
||
elif source == "GitLink":
|
||
# GitLink格式
|
||
project = data.get("project", {})
|
||
event_info["repository"] = project.get("name", "unknown")
|
||
|
||
if "issue" in data:
|
||
event_info["event_type"] = "issue"
|
||
issue = data["issue"]
|
||
event_info["title"] = issue.get("subject", "")
|
||
event_info["number"] = data.get("number", 0)
|
||
event_info["user"] = issue.get("author", {}).get("name", "unknown")
|
||
|
||
elif "pull_request" in data:
|
||
event_info["event_type"] = "pull_request"
|
||
pr = data["pull_request"]
|
||
event_info["title"] = pr.get("title", "")
|
||
event_info["number"] = pr.get("number", 0)
|
||
event_info["user"] = pr.get("user", {}).get("name", "unknown")
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ 解析webhook事件信息失败: {e}")
|
||
|
||
return event_info
|
||
|
||
async def find_project_by_repo_url(repo_url: str) -> Optional[int]:
|
||
"""
|
||
根据仓库URL查找对应的项目ID
|
||
"""
|
||
if not repo_url:
|
||
return None
|
||
|
||
try:
|
||
project_dao = SyncProjectDAO()
|
||
projects = await project_dao.list_all()
|
||
|
||
# 标准化输入的repo_url
|
||
normalized_input = repo_url.lower().rstrip('/').replace('.git', '')
|
||
|
||
for project in projects:
|
||
# 检查各个平台的仓库地址
|
||
for repo_field in [project.github, project.gitee, project.gitlink]:
|
||
if repo_field:
|
||
normalized_repo = repo_field.lower().rstrip('/').replace('.git', '')
|
||
if normalized_input == normalized_repo or normalized_input in normalized_repo:
|
||
logger.info(f"🎯 找到匹配项目: ID={project.id}, 名称={project.name}")
|
||
return project.id
|
||
|
||
logger.warning(f"⚠️ 未找到匹配的项目,仓库URL: {repo_url}")
|
||
return None
|
||
except Exception as e:
|
||
logger.error(f"❌ 查找项目失败: {e}")
|
||
return None
|
||
|
||
async def get_project_id_from_webhook(data: dict) -> int:
|
||
"""
|
||
从webhook数据中提取项目ID
|
||
"""
|
||
repo_url = None
|
||
|
||
try:
|
||
# 从不同平台的webhook数据中提取仓库URL
|
||
if "repository" in data:
|
||
# GitHub/Gitee格式
|
||
if "html_url" in data["repository"]:
|
||
repo_url = data["repository"]["html_url"]
|
||
elif "url" in data["repository"]:
|
||
repo_url = data["repository"]["url"]
|
||
elif "clone_url" in data["repository"]:
|
||
repo_url = data["repository"]["clone_url"]
|
||
elif "project" in data:
|
||
# GitLink格式
|
||
project = data["project"]
|
||
if "html_url" in project:
|
||
repo_url = project["html_url"]
|
||
elif "git_url" in project:
|
||
repo_url = project["git_url"]
|
||
else:
|
||
# 如果没有直接的URL,尝试构建URL
|
||
project_name = project.get("name")
|
||
project_owner = project.get("owner", {}).get("login") or project.get("owner", {}).get("name")
|
||
if project_name and project_owner:
|
||
repo_url = f"https://www.gitlink.org.cn/{project_owner}/{project_name}"
|
||
logger.info(f"🔍 从项目信息构建GitLink URL: {repo_url}")
|
||
|
||
if repo_url:
|
||
logger.info(f"🔍 尝试通过仓库URL匹配项目: {repo_url}")
|
||
project_id = await find_project_by_repo_url(repo_url)
|
||
if project_id:
|
||
logger.info(f"✅ 成功匹配到项目ID: {project_id}")
|
||
return project_id
|
||
else:
|
||
logger.warning(f"⚠️ 未找到匹配的项目: {repo_url}")
|
||
else:
|
||
logger.warning("⚠️ 无法从webhook数据中提取仓库URL")
|
||
|
||
# 如果找不到匹配的项目,使用默认项目ID=1
|
||
logger.warning(f"🔄 使用默认项目ID=1,这可能会导致同步失败")
|
||
return 1
|
||
except Exception as e:
|
||
logger.error(f"❌ 项目ID提取失败: {str(e)}")
|
||
return 1
|
||
|
||
async def get_sync_repo(project_id: int = 1):
|
||
"""获取同步仓库配置"""
|
||
config = await get_config(project_id)
|
||
return (
|
||
config.get("gitlink", {}).get("token"),
|
||
config.get("gitlink", {}).get("repo"),
|
||
config.get("gitlink", {}).get("owner"),
|
||
config.get("gitee", {}).get("token"),
|
||
config.get("gitee", {}).get("repo"),
|
||
config.get("gitee", {}).get("owner"),
|
||
config.get("github", {}).get("token"),
|
||
config.get("github", {}).get("repo"),
|
||
config.get("github", {}).get("owner")
|
||
)
|
||
|
||
async def verify_webhook_signature(request: Request, body: bytes, source: str, project_id: int = 1) -> None:
|
||
"""验证webhook签名"""
|
||
config = await get_config(project_id)
|
||
|
||
# 获取请求头中的签名
|
||
gitee_signature = request.headers.get("X-Gitee-Token")
|
||
github_signature = request.headers.get("X-Hub-Signature-256")
|
||
gitlink_signature = request.headers.get("X-Gitlink-Signature")
|
||
|
||
# 根据来源平台验证签名
|
||
if source == "Gitee":
|
||
gitee_webhook_secret = config.get("gitee", {}).get("webhook_secret")
|
||
if not gitee_webhook_secret:
|
||
logger.info("🔓 Gitee webhook签名验证跳过(未配置secret)")
|
||
return
|
||
if gitee_signature != gitee_webhook_secret:
|
||
logger.error("❌ Gitee webhook签名验证失败")
|
||
raise HTTPException(status_code=401, detail="Invalid Gitee webhook signature")
|
||
logger.info("✅ Gitee webhook签名验证成功")
|
||
|
||
elif source == "GitHub":
|
||
github_webhook_secret = config.get("github", {}).get("webhook_secret")
|
||
if not github_webhook_secret:
|
||
logger.info("🔓 GitHub webhook签名验证跳过(未配置secret)")
|
||
return
|
||
|
||
# GitHub使用HMAC-SHA256签名验证
|
||
expected_signature = hmac.new(
|
||
github_webhook_secret.encode('utf-8'),
|
||
body,
|
||
hashlib.sha256
|
||
).hexdigest()
|
||
expected_signature = f"sha256={expected_signature}"
|
||
|
||
if not hmac.compare_digest(github_signature, expected_signature):
|
||
logger.error("❌ GitHub webhook签名验证失败")
|
||
raise HTTPException(status_code=401, detail="Invalid GitHub webhook signature")
|
||
logger.info("✅ GitHub webhook签名验证成功")
|
||
|
||
elif source == "GitLink":
|
||
gitlink_webhook_secret = config.get("gitlink", {}).get("webhook_secret")
|
||
if not gitlink_webhook_secret:
|
||
logger.info("🔓 GitLink webhook签名验证跳过(未配置secret)")
|
||
return
|
||
if gitlink_signature != gitlink_webhook_secret:
|
||
logger.error("❌ GitLink webhook签名验证失败")
|
||
raise HTTPException(status_code=401, detail="Invalid GitLink webhook signature")
|
||
logger.info("✅ GitLink webhook签名验证成功")
|
||
|
||
else:
|
||
logger.warning(f"⚠️ 未知的webhook来源: {source}")
|
||
raise HTTPException(status_code=401, detail="Unknown webhook source")
|
||
|
||
async def sync_issue_to_gitee(data: dict, token: str, owner: str, repo: str) -> dict:
|
||
"""同步issue到Gitee"""
|
||
try:
|
||
issue = data["issue"]
|
||
title = issue["subject"]
|
||
body = issue.get("description", "")
|
||
|
||
# 在Gitee上创建issue
|
||
url = f'https://gitee.com/api/v5/repos/{owner}/issues'
|
||
payload = {
|
||
"access_token": token,
|
||
"repo": repo,
|
||
"title": title,
|
||
"body": body
|
||
}
|
||
headers = {
|
||
'Content-Type': 'application/json;charset=UTF-8'
|
||
}
|
||
|
||
logger.info(f"📤 向Gitee发送请求: POST {url}")
|
||
response = requests.post(url, headers=headers, json=payload)
|
||
response.raise_for_status()
|
||
|
||
result = response.json()
|
||
return {
|
||
"issue_number": result.get("number"),
|
||
"url": result.get("html_url")
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"❌ Gitee同步失败: {str(e)}")
|
||
raise
|
||
|
||
async def sync_issue_to_github(data: dict, token: str, owner: str, repo: str) -> dict:
|
||
"""同步issue到GitHub"""
|
||
try:
|
||
issue = data["issue"]
|
||
title = issue["subject"]
|
||
body = issue.get("description", "")
|
||
|
||
# 在GitHub上创建issue
|
||
url = f"https://api.github.com/repos/{owner}/{repo}/issues"
|
||
payload = {
|
||
"title": title,
|
||
"body": body,
|
||
"labels": ["sync issue"]
|
||
}
|
||
headers = {
|
||
'Authorization': f'Bearer {token}',
|
||
'User-Agent': 'Apifox/1.0.0 (https://apifox.com)',
|
||
'Content-Type': 'application/json'
|
||
}
|
||
|
||
logger.info(f"📤 向GitHub发送请求: POST {url}")
|
||
response = requests.post(url, headers=headers, json=payload)
|
||
response.raise_for_status()
|
||
|
||
result = response.json()
|
||
return {
|
||
"issue_number": result.get("number"),
|
||
"url": result.get("html_url")
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"❌ GitHub同步失败: {str(e)}")
|
||
raise
|
||
|
||
async def sync_issue_to_gitlink(data: dict, token: str, owner: str, repo: str) -> dict:
|
||
"""同步issue到GitLink"""
|
||
try:
|
||
# 从Gitee的webhook数据中提取issue信息
|
||
issue = data.get("issue", {})
|
||
title = issue.get("title", "")
|
||
body = issue.get("body", "")
|
||
|
||
# 构造GitLink API请求
|
||
url = f"https://www.gitlink.org.cn/api/v1/repos/{owner}/{repo}/issues"
|
||
headers = {
|
||
'Authorization': f'Bearer {token}',
|
||
'Content-Type': 'application/json'
|
||
}
|
||
payload = {
|
||
"subject": title,
|
||
"description": body,
|
||
"labels": ["sync issue"]
|
||
}
|
||
|
||
logger.info(f"📤 向GitLink发送请求: POST {url}")
|
||
response = requests.post(url, headers=headers, json=payload)
|
||
response.raise_for_status()
|
||
|
||
result = response.json()
|
||
return {
|
||
"issue_number": result.get("number"),
|
||
"url": result.get("html_url")
|
||
}
|
||
except Exception as e:
|
||
logger.error(f"❌ GitLink同步失败: {str(e)}")
|
||
raise
|
||
|
||
@WEBHOOK.post("/issue")
|
||
async def issue_webhook(request: Request):
|
||
"""处理issue相关的webhook事件 - 自动识别项目"""
|
||
start_time = datetime.now()
|
||
try:
|
||
logger.info("🚀 =========================")
|
||
logger.info("🚀 收到Issue Webhook请求")
|
||
logger.info("🚀 =========================")
|
||
|
||
# 先读取请求体
|
||
body = await request.body()
|
||
|
||
# 解析JSON数据以获取项目信息
|
||
try:
|
||
data = json.loads(body.decode('utf-8'))
|
||
except json.JSONDecodeError as e:
|
||
logger.error(f"❌ JSON解析失败: {str(e)}")
|
||
return {"status": "error", "message": "Invalid JSON data"}
|
||
|
||
# 检测webhook来源和事件信息
|
||
source = detect_webhook_source(data, dict(request.headers))
|
||
event_info = get_webhook_event_info(data, source)
|
||
|
||
# 记录事件信息
|
||
logger.info(f"📡 来源平台: {event_info['source']}")
|
||
logger.info(f"📝 事件类型: {event_info['event_type']}")
|
||
logger.info(f"🎯 动作: {event_info['action']}")
|
||
logger.info(f"📁 仓库: {event_info['repository']}")
|
||
logger.info(f"👤 用户: {event_info['user']}")
|
||
logger.info(f"🏷️ 标题: {event_info['title']}")
|
||
logger.info(f"🔢 编号: #{event_info['number']}")
|
||
|
||
# 自动查找对应的项目ID
|
||
project_id = await get_project_id_from_webhook(data)
|
||
logger.info(f"🆔 使用项目ID: {project_id}")
|
||
|
||
if project_id == 1:
|
||
logger.warning("⚠️ 使用默认项目ID,请检查项目配置")
|
||
|
||
# 验证签名
|
||
await verify_webhook_signature(request, body, event_info["source"], project_id)
|
||
|
||
# 获取仓库配置
|
||
row = await get_sync_repo(project_id)
|
||
gitlink_token, gitlink_repo, gitlink_owner = row[0:3]
|
||
gitee_token, gitee_repo, gitee_owner = row[3:6]
|
||
github_token, github_repo, github_owner = row[6:9]
|
||
|
||
# 验证配置完整性
|
||
if event_info["source"] == "GitLink":
|
||
if not all([gitlink_token, gitlink_repo, gitlink_owner]):
|
||
logger.error("❌ GitLink配置不完整")
|
||
return {"status": "error", "message": "GitLink configuration incomplete"}
|
||
elif event_info["source"] == "Gitee":
|
||
if not all([gitee_token, gitee_repo, gitee_owner]):
|
||
logger.error("❌ Gitee配置不完整")
|
||
return {"status": "error", "message": "Gitee configuration incomplete"}
|
||
elif event_info["source"] == "GitHub":
|
||
if not all([github_token, github_repo, github_owner]):
|
||
logger.error("❌ GitHub配置不完整")
|
||
return {"status": "error", "message": "GitHub configuration incomplete"}
|
||
|
||
# 处理issue事件
|
||
if event_info["event_type"] != "issue":
|
||
logger.warning(f"⚠️ 非issue事件: {event_info['event_type']}")
|
||
return {"status": "ignored", "message": "Not an issue event"}
|
||
|
||
if event_info["action"] not in ["opened", "edited", "closed", "reopened"]:
|
||
logger.info(f"ℹ️ 忽略的issue动作: {event_info['action']}")
|
||
return {"status": "ignored", "message": f"Ignored issue action: {event_info['action']}"}
|
||
|
||
# 同步处理
|
||
sync_results = []
|
||
|
||
# 从GitLink同步到其他平台
|
||
if event_info["source"] == "GitLink" and event_info["action"] == "opened":
|
||
logger.info("🔄 开始处理GitLink Issue事件")
|
||
logger.info("📝 创建Issue: #{} - {}", event_info["number"], event_info["title"])
|
||
logger.info("🔄 开始同步到Gitee和GitHub...")
|
||
|
||
# 同步到Gitee
|
||
if all([gitee_token, gitee_repo, gitee_owner]):
|
||
try:
|
||
logger.info("📤 正在向Gitee创建Issue...")
|
||
gitee_result = await sync_issue_to_gitee(data, gitee_token, gitee_owner, gitee_repo)
|
||
sync_results.append({"platform": "Gitee", "status": "success", "result": gitee_result})
|
||
logger.info("✅ Gitee Issue创建成功")
|
||
except Exception as e:
|
||
logger.error(f"❌ Gitee同步失败: {str(e)}")
|
||
sync_results.append({"platform": "Gitee", "status": "error", "error": str(e)})
|
||
|
||
# 同步到GitHub
|
||
if all([github_token, github_repo, github_owner]):
|
||
try:
|
||
logger.info("📤 正在向GitHub创建Issue...")
|
||
github_result = await sync_issue_to_github(data, github_token, github_owner, github_repo)
|
||
sync_results.append({"platform": "GitHub", "status": "success", "result": github_result})
|
||
logger.info("✅ GitHub Issue创建成功")
|
||
except Exception as e:
|
||
logger.error(f"❌ GitHub同步失败: {str(e)}")
|
||
sync_results.append({"platform": "GitHub", "status": "error", "error": str(e)})
|
||
|
||
# 从Gitee同步到其他平台
|
||
elif event_info["source"] == "Gitee" and event_info["action"] in ["open", "opened"]:
|
||
logger.info("🔄 开始处理Gitee Issue事件")
|
||
logger.info("📝 创建Issue: #{} - {}", event_info["number"], event_info["title"])
|
||
logger.info("🔄 开始同步到GitLink和GitHub...")
|
||
|
||
# 同步到GitLink
|
||
if all([gitlink_token, gitlink_repo, gitlink_owner]):
|
||
try:
|
||
logger.info("📤 正在向GitLink创建Issue...")
|
||
gitlink_result = await sync_issue_to_gitlink(data, gitlink_token, gitlink_owner, gitlink_repo)
|
||
sync_results.append({"platform": "GitLink", "status": "success", "result": gitlink_result})
|
||
logger.info("✅ GitLink Issue创建成功")
|
||
except Exception as e:
|
||
logger.error(f"❌ GitLink同步失败: {str(e)}")
|
||
sync_results.append({"platform": "GitLink", "status": "error", "error": str(e)})
|
||
|
||
# 同步到GitHub(只有在GitHub配置存在时才同步)
|
||
if all([github_token, github_repo, github_owner]):
|
||
try:
|
||
logger.info("📤 正在向GitHub创建Issue...")
|
||
github_result = await sync_issue_to_github(data, github_token, github_owner, github_repo)
|
||
sync_results.append({"platform": "GitHub", "status": "success", "result": github_result})
|
||
logger.info("✅ GitHub Issue创建成功")
|
||
except Exception as e:
|
||
logger.error(f"❌ GitHub同步失败: {str(e)}")
|
||
sync_results.append({"platform": "GitHub", "status": "error", "error": str(e)})
|
||
|
||
# 记录处理时间
|
||
end_time = datetime.now()
|
||
process_time = (end_time - start_time).total_seconds()
|
||
logger.info("🎉 =========================")
|
||
logger.info(f"🎉 处理完成! 耗时: {process_time:.2f}秒")
|
||
logger.info("🎉 =========================")
|
||
|
||
return {
|
||
"status": "success",
|
||
"message": "Webhook processed successfully",
|
||
"process_time": f"{process_time:.2f}s",
|
||
"sync_results": sync_results
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"❌ Webhook处理失败: {str(e)}")
|
||
return {"status": "error", "message": f"Webhook processing failed: {str(e)}"}
|
||
|
||
@WEBHOOK.post("/pr")
|
||
async def pr_webhook(request: Request):
|
||
"""处理pull request相关的webhook事件 - 自动识别项目"""
|
||
start_time = datetime.now()
|
||
logger.info("🚀 =========================")
|
||
logger.info("🚀 收到PR Webhook请求")
|
||
logger.info("🚀 =========================")
|
||
|
||
# 先读取请求体
|
||
body = await request.body()
|
||
|
||
# 解析JSON数据以获取项目信息
|
||
data = json.loads(body.decode('utf-8'))
|
||
|
||
# 检测webhook来源和事件信息
|
||
source = detect_webhook_source(data, dict(request.headers))
|
||
event_info = get_webhook_event_info(data, source)
|
||
|
||
logger.info(f"📡 来源平台: {event_info['source']}")
|
||
logger.info(f"📝 事件类型: {event_info['event_type']}")
|
||
logger.info(f"🎯 动作: {event_info['action']}")
|
||
logger.info(f"📁 仓库: {event_info['repository']}")
|
||
logger.info(f"👤 用户: {event_info['user']}")
|
||
logger.info(f"🏷️ 标题: {event_info['title']}")
|
||
logger.info(f"🔢 编号: #{event_info['number']}")
|
||
|
||
# 自动查找对应的项目ID
|
||
project_id = await get_project_id_from_webhook(data)
|
||
logger.info(f"🆔 使用项目ID: {project_id}")
|
||
|
||
# 验证签名
|
||
await verify_webhook_signature(request, body, event_info["source"], project_id)
|
||
|
||
# 获取仓库配置
|
||
row = await get_sync_repo(project_id)
|
||
gitlink_token = row[0]
|
||
gitlink_repo = row[1]
|
||
gitlink_owner = row[2]
|
||
gitee_token = row[3]
|
||
gitee_repo = row[4]
|
||
gitee_owner = row[5]
|
||
github_token = row[6]
|
||
github_repo = row[7]
|
||
github_owner = row[8]
|
||
|
||
# 检测来源平台
|
||
if data["repository"]["owner"]["avatar_url"] and "gitee" in data["repository"]["owner"]["avatar_url"]:
|
||
flag = "gitee"
|
||
elif data["repository"]["owner"]["avatar_url"] and "github" in data["repository"]["owner"]["avatar_url"]:
|
||
flag = "github"
|
||
else:
|
||
flag = "gitlink"
|
||
|
||
logger.info(f"🔍 检测到平台: {flag}")
|
||
|
||
# 处理来自不同平台的PR事件 - 添加基础日志
|
||
try:
|
||
if flag == "gitee":
|
||
logger.info("🔄 开始处理Gitee PR事件")
|
||
# 现有的Gitee PR处理逻辑...
|
||
elif flag == "gitlink":
|
||
logger.info("🔄 开始处理GitLink PR事件")
|
||
# 现有的GitLink PR处理逻辑...
|
||
elif flag == "github":
|
||
logger.info("🔄 开始处理GitHub PR事件")
|
||
# 现有的GitHub PR处理逻辑...
|
||
except Exception as e:
|
||
logger.error(f"❌ PR处理失败: {str(e)}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
# 在文件的最后,"未开通的服务"返回之前添加日志
|
||
logger.warning("⚠️ =========================")
|
||
logger.warning("⚠️ 未开通的服务或未支持的PR事件")
|
||
logger.warning(f"⚠️ 平台: {flag}")
|
||
logger.warning(f"⚠️ 动作: {data.get('action', 'unknown')}")
|
||
logger.warning("⚠️ =========================")
|
||
|
||
return {"message": "未开通的服务"}
|
||
|
||
@WEBHOOK.websocket("/admin/logs/ws")
|
||
async def websocket_logs(websocket: WebSocket):
|
||
"""WebSocket实时日志推送"""
|
||
await manager.connect(websocket)
|
||
try:
|
||
while True:
|
||
# 保持连接
|
||
await websocket.receive_text()
|
||
except:
|
||
manager.disconnect(websocket)
|
||
|
||
@WEBHOOK.get("/admin/logs/viewer", response_class=HTMLResponse)
|
||
async def logs_viewer():
|
||
"""日志查看器页面"""
|
||
html_content = """
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<title>实时日志查看器</title>
|
||
<meta charset="utf-8">
|
||
<style>
|
||
body {
|
||
font-family: 'Courier New', monospace;
|
||
margin: 0;
|
||
padding: 20px;
|
||
background-color: #1e1e1e;
|
||
color: #fff;
|
||
}
|
||
.header {
|
||
background-color: #333;
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
.controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
}
|
||
button {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
}
|
||
.btn-primary { background-color: #007bff; color: white; }
|
||
.btn-danger { background-color: #dc3545; color: white; }
|
||
.btn-warning { background-color: #ffc107; color: black; }
|
||
.log-container {
|
||
height: 70vh;
|
||
overflow-y: auto;
|
||
background-color: #000;
|
||
border: 1px solid #333;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
}
|
||
.log-entry {
|
||
margin-bottom: 5px;
|
||
padding: 3px 0;
|
||
word-wrap: break-word;
|
||
}
|
||
.log-timestamp { color: #888; }
|
||
.log-level { font-weight: bold; margin: 0 10px; }
|
||
.log-message { color: #fff; }
|
||
.level-INFO { color: #17a2b8; }
|
||
.level-ERROR { color: #dc3545; }
|
||
.level-WARNING { color: #ffc107; }
|
||
.level-DEBUG { color: #6c757d; }
|
||
.level-STDOUT { color: #28a745; }
|
||
.level-STDERR { color: #fd7e14; }
|
||
.status {
|
||
padding: 5px 10px;
|
||
border-radius: 3px;
|
||
font-size: 12px;
|
||
margin-left: 10px;
|
||
}
|
||
.status-connected { background-color: #28a745; }
|
||
.status-disconnected { background-color: #dc3545; }
|
||
.filter-container {
|
||
margin-bottom: 10px;
|
||
display: flex;
|
||
gap: 10px;
|
||
align-items: center;
|
||
}
|
||
select, input {
|
||
padding: 5px;
|
||
border: 1px solid #555;
|
||
border-radius: 3px;
|
||
background-color: #333;
|
||
color: #fff;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="header">
|
||
<h1>🚀 FastAPI 实时日志查看器</h1>
|
||
<div class="controls">
|
||
<span id="status" class="status status-disconnected">未连接</span>
|
||
<button class="btn-primary" onclick="connect()">连接</button>
|
||
<button class="btn-danger" onclick="disconnect()">断开</button>
|
||
<button class="btn-warning" onclick="clearLogs()">清空</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="filter-container">
|
||
<label>过滤级别:</label>
|
||
<select id="levelFilter" onchange="filterLogs()">
|
||
<option value="">全部</option>
|
||
<option value="INFO">INFO</option>
|
||
<option value="ERROR">ERROR</option>
|
||
<option value="WARNING">WARNING</option>
|
||
<option value="DEBUG">DEBUG</option>
|
||
<option value="STDOUT">STDOUT</option>
|
||
<option value="STDERR">STDERR</option>
|
||
</select>
|
||
|
||
<label>搜索:</label>
|
||
<input type="text" id="searchInput" placeholder="输入关键词搜索..." oninput="filterLogs()">
|
||
|
||
<label>自动滚动:</label>
|
||
<input type="checkbox" id="autoScroll" checked>
|
||
</div>
|
||
|
||
<div id="logs" class="log-container"></div>
|
||
|
||
<script>
|
||
let ws = null;
|
||
let allLogs = [];
|
||
|
||
function connect() {
|
||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
const wsUrl = `${protocol}//${window.location.host}/webhook/admin/logs/ws`;
|
||
|
||
ws = new WebSocket(wsUrl);
|
||
|
||
ws.onopen = function() {
|
||
document.getElementById('status').textContent = '已连接';
|
||
document.getElementById('status').className = 'status status-connected';
|
||
};
|
||
|
||
ws.onmessage = function(event) {
|
||
const logData = JSON.parse(event.data);
|
||
allLogs.push(logData);
|
||
|
||
// 限制日志数量,避免内存溢出
|
||
if (allLogs.length > 1000) {
|
||
allLogs = allLogs.slice(-800);
|
||
}
|
||
|
||
filterLogs();
|
||
};
|
||
|
||
ws.onclose = function() {
|
||
document.getElementById('status').textContent = '连接断开';
|
||
document.getElementById('status').className = 'status status-disconnected';
|
||
};
|
||
|
||
ws.onerror = function() {
|
||
document.getElementById('status').textContent = '连接错误';
|
||
document.getElementById('status').className = 'status status-disconnected';
|
||
};
|
||
}
|
||
|
||
function disconnect() {
|
||
if (ws) {
|
||
ws.close();
|
||
}
|
||
}
|
||
|
||
function clearLogs() {
|
||
allLogs = [];
|
||
document.getElementById('logs').innerHTML = '';
|
||
}
|
||
|
||
function filterLogs() {
|
||
const levelFilter = document.getElementById('levelFilter').value;
|
||
const searchText = document.getElementById('searchInput').value.toLowerCase();
|
||
const logsContainer = document.getElementById('logs');
|
||
|
||
let filteredLogs = allLogs;
|
||
|
||
if (levelFilter) {
|
||
filteredLogs = filteredLogs.filter(log => log.level === levelFilter);
|
||
}
|
||
|
||
if (searchText) {
|
||
filteredLogs = filteredLogs.filter(log =>
|
||
log.message.toLowerCase().includes(searchText)
|
||
);
|
||
}
|
||
|
||
logsContainer.innerHTML = filteredLogs.map(log => ` |