日志丰富功能 #20
|
|
@ -21,4 +21,4 @@ RUN yum install -y openssh-server git
|
|||
ENV GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -i /root/.ssh/id_rsa'
|
||||
#qxq:服务器有老的python,你如果不使用python3.9,就会报错;另外运行程序的控制指令在dockerfile,这实在有点奇怪
|
||||
WORKDIR /data/ob-robot
|
||||
CMD if [ "$BOOT_MODE" = "app" ] ; then python3.9 main.py & python3.9 start_web_ui.py; fi
|
||||
CMD if [ "$BOOT_MODE" = "app" ] ; then python3.9 start_services.py; fi
|
||||
|
|
|
|||
|
|
@ -0,0 +1,384 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Webhook处理器 - 自动同步Gitee和GitLink之间的Issues
|
||||
支持防循环机制,避免无限同步
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import hashlib
|
||||
import requests
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
# 兼容导入:优先使用环境变量,后备使用src模块
|
||||
try:
|
||||
from src.utils.logger import logger
|
||||
except ImportError:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class LoopDetector:
|
||||
"""循环检测器 - 防止无限同步循环"""
|
||||
|
||||
def __init__(self, window_minutes: int = 5):
|
||||
self.window_minutes = window_minutes
|
||||
self.recent_syncs = {} # {issue_key: timestamp}
|
||||
|
||||
def _get_issue_key(self, platform: str, org: str, repo: str, issue_title: str) -> str:
|
||||
"""生成Issue的唯一标识"""
|
||||
return f"{platform}:{org}/{repo}:{issue_title}"
|
||||
|
||||
def should_sync(self, platform: str, org: str, repo: str, issue_title: str) -> bool:
|
||||
"""检查是否应该同步(避免循环)"""
|
||||
issue_key = self._get_issue_key(platform, org, repo, issue_title)
|
||||
current_time = datetime.now()
|
||||
|
||||
# 清理过期记录
|
||||
self._cleanup_old_records(current_time)
|
||||
|
||||
# 检查是否在时间窗口内已经同步过
|
||||
if issue_key in self.recent_syncs:
|
||||
last_sync = self.recent_syncs[issue_key]
|
||||
if current_time - last_sync < timedelta(minutes=self.window_minutes):
|
||||
logger.info(f"🔄 跳过同步 - Issue '{issue_title}' 在 {self.window_minutes} 分钟内已同步过")
|
||||
return False
|
||||
|
||||
# 记录本次同步
|
||||
self.recent_syncs[issue_key] = current_time
|
||||
logger.info(f"✅ 允许同步 - Issue '{issue_title}' 可以进行同步")
|
||||
return True
|
||||
|
||||
def _cleanup_old_records(self, current_time: datetime):
|
||||
"""清理过期的同步记录"""
|
||||
cutoff_time = current_time - timedelta(minutes=self.window_minutes * 2)
|
||||
expired_keys = [
|
||||
key for key, timestamp in self.recent_syncs.items()
|
||||
if timestamp < cutoff_time
|
||||
]
|
||||
for key in expired_keys:
|
||||
del self.recent_syncs[key]
|
||||
|
||||
|
||||
class WebhookHandler:
|
||||
"""Webhook处理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.loop_detector = LoopDetector(window_minutes=5)
|
||||
self.sync_api_url = "http://localhost:8002/sync/immediate"
|
||||
|
||||
# Webhook密码验证
|
||||
self.webhook_password = "hzk200407140238"
|
||||
|
||||
# 仓库配置
|
||||
self.repo_config = {
|
||||
"gitee": {
|
||||
"org": "ttk00",
|
||||
"repo": "testdemo"
|
||||
},
|
||||
"gitlink": {
|
||||
"org": "qinxinqi",
|
||||
"repo": "testdemo"
|
||||
}
|
||||
}
|
||||
|
||||
def detect_platform(self, webhook_data: Dict[str, Any], headers: Dict[str, str] = None) -> Optional[str]:
|
||||
"""检测webhook来源平台"""
|
||||
|
||||
# 检测Gitee webhook - 通过headers
|
||||
if headers:
|
||||
if 'x-gitee-event' in headers or 'x-git-oschina-event' in headers:
|
||||
logger.info(f"🔍 检测到Gitee webhook (通过headers)")
|
||||
return "gitee"
|
||||
|
||||
# 检测GitLink webhook - 通过headers (GitLink使用Gitea/Gogs格式)
|
||||
if 'x-gitea-event' in headers or 'x-gogs-event' in headers or 'x-github-event' in headers:
|
||||
logger.info(f"🔍 检测到GitLink webhook (通过headers)")
|
||||
return "gitlink"
|
||||
|
||||
# 检测Gitee webhook - 通过数据结构
|
||||
if 'repository' in webhook_data and 'html_url' in webhook_data.get('repository', {}):
|
||||
repo_url = webhook_data['repository']['html_url']
|
||||
if 'gitee.com' in repo_url:
|
||||
logger.info(f"🔍 检测到Gitee webhook: {repo_url}")
|
||||
return "gitee"
|
||||
|
||||
# 检测Gitee webhook - 通过project字段
|
||||
if 'project' in webhook_data and 'html_url' in webhook_data.get('project', {}):
|
||||
project_url = webhook_data['project']['html_url']
|
||||
if 'gitee.com' in project_url:
|
||||
logger.info(f"🔍 检测到Gitee webhook: {project_url}")
|
||||
return "gitee"
|
||||
|
||||
# 检测GitLink webhook - 通过project字段和mirror_url
|
||||
if 'project' in webhook_data and 'identifier' in webhook_data.get('project', {}):
|
||||
project = webhook_data['project']
|
||||
mirror_url = project.get('mirror_url', '')
|
||||
if 'gitee.com' in mirror_url:
|
||||
logger.info(f"🔍 检测到GitLink webhook (镜像自Gitee): {project.get('identifier', 'unknown')}")
|
||||
return "gitlink"
|
||||
else:
|
||||
logger.info(f"🔍 检测到GitLink webhook: {project.get('identifier', 'unknown')}")
|
||||
return "gitlink"
|
||||
|
||||
# 通过User-Agent检测
|
||||
if headers and 'user-agent' in headers:
|
||||
user_agent = headers['user-agent'].lower()
|
||||
if 'git-oschina-hook' in user_agent:
|
||||
return "gitee"
|
||||
elif 'gitlink' in user_agent:
|
||||
return "gitlink"
|
||||
|
||||
logger.warning(f"⚠️ 无法识别webhook来源平台,数据结构: {list(webhook_data.keys())}")
|
||||
if headers:
|
||||
logger.warning(f"⚠️ Headers: {list(headers.keys())}")
|
||||
return None
|
||||
|
||||
def extract_issue_info(self, webhook_data: Dict[str, Any], platform: str) -> Optional[Dict[str, str]]:
|
||||
"""提取Issue信息"""
|
||||
try:
|
||||
if platform == "gitee":
|
||||
# Gitee webhook结构 - 支持新格式
|
||||
issue = webhook_data.get('issue', {})
|
||||
return {
|
||||
"title": issue.get('title', webhook_data.get('title', '')),
|
||||
"action": webhook_data.get('action', ''),
|
||||
"number": str(issue.get('number', webhook_data.get('iid', ''))),
|
||||
"state": issue.get('state', webhook_data.get('state', '')),
|
||||
"body": issue.get('body', issue.get('description', '')),
|
||||
"user": issue.get('user', {}).get('login', '')
|
||||
}
|
||||
|
||||
elif platform == "gitlink":
|
||||
# GitLink webhook结构 - 支持新格式
|
||||
issue = webhook_data.get('issue', {})
|
||||
action = webhook_data.get('action', '')
|
||||
|
||||
# 处理GitLink的状态信息
|
||||
status = issue.get('status', {})
|
||||
status_name = status.get('name', '') if isinstance(status, dict) else issue.get('status_name', '')
|
||||
|
||||
return {
|
||||
"title": issue.get('subject', ''),
|
||||
"action": action,
|
||||
"number": str(issue.get('id', issue.get('project_issues_index', ''))),
|
||||
"state": status_name,
|
||||
"body": issue.get('description', ''),
|
||||
"user": issue.get('author', {}).get('login', ''),
|
||||
"event_type": webhook_data.get('journal', {}).get('notes', '') if 'journal' in webhook_data else ''
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"❌ 提取Issue信息失败: {str(e)}")
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def verify_webhook_password(self, webhook_data: Dict[str, Any], headers: Dict[str, str] = None) -> bool:
|
||||
"""验证webhook密码"""
|
||||
|
||||
# 检查数据中的密码字段
|
||||
if 'password' in webhook_data:
|
||||
provided_password = webhook_data['password']
|
||||
if provided_password == self.webhook_password:
|
||||
logger.info("✅ Webhook密码验证通过")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"❌ Webhook密码验证失败: 提供的密码不匹配")
|
||||
return False
|
||||
|
||||
# 检查headers中的token
|
||||
if headers:
|
||||
gitee_token = headers.get('x-gitee-token', '')
|
||||
if gitee_token == self.webhook_password:
|
||||
logger.info("✅ Gitee Token验证通过")
|
||||
return True
|
||||
|
||||
logger.warning("⚠️ 未找到有效的密码或token,跳过验证")
|
||||
return True # 如果没有密码字段,暂时允许通过
|
||||
|
||||
def should_trigger_sync(self, webhook_data: Dict[str, Any], platform: str, headers: Dict[str, str] = None) -> bool:
|
||||
"""判断是否应该触发同步"""
|
||||
|
||||
# 检查是否是Issue相关事件
|
||||
if 'issue' not in webhook_data:
|
||||
logger.info("📋 非Issue事件,跳过同步")
|
||||
return False
|
||||
|
||||
# 检查事件类型 - GitLink可能是评论事件
|
||||
if headers:
|
||||
event_type = headers.get('x-gitea-event', headers.get('x-gogs-event', headers.get('x-github-event', '')))
|
||||
if event_type == 'issue_comment':
|
||||
logger.info("📋 Issue评论事件,暂不触发同步")
|
||||
return False
|
||||
|
||||
# 检查动作类型
|
||||
action = webhook_data.get('action', '')
|
||||
valid_actions = ['opened', 'open', 'created', 'edited', 'updated', 'closed', 'reopened']
|
||||
|
||||
if action not in valid_actions:
|
||||
logger.info(f"📋 动作 '{action}' 不需要同步")
|
||||
return False
|
||||
|
||||
# 提取Issue信息
|
||||
issue_info = self.extract_issue_info(webhook_data, platform)
|
||||
if not issue_info or not issue_info.get('title'):
|
||||
logger.warning("⚠️ 无法提取Issue标题,跳过同步")
|
||||
return False
|
||||
|
||||
# 检查是否是同步机器人创建的Issue(防循环)
|
||||
if self._is_sync_created_issue(webhook_data, platform):
|
||||
logger.info(f"🤖 检测到同步机器人创建的Issue,跳过同步")
|
||||
return False
|
||||
|
||||
# 使用循环检测器
|
||||
source_config = self.repo_config[platform]
|
||||
return self.loop_detector.should_sync(
|
||||
platform,
|
||||
source_config["org"],
|
||||
source_config["repo"],
|
||||
issue_info["title"]
|
||||
)
|
||||
|
||||
def _is_sync_created_issue(self, webhook_data: Dict[str, Any], platform: str) -> bool:
|
||||
"""检查是否是同步机器人创建的Issue"""
|
||||
|
||||
if platform == "gitee":
|
||||
# 检查Gitee的用户信息
|
||||
issue = webhook_data.get('issue', {})
|
||||
user = issue.get('user', {})
|
||||
username = user.get('login', '').lower()
|
||||
|
||||
# 如果是同步相关的用户名,认为是机器人操作
|
||||
sync_usernames = ['sync-bot', 'reposync', 'auto-sync']
|
||||
if any(sync_name in username for sync_name in sync_usernames):
|
||||
return True
|
||||
|
||||
elif platform == "gitlink":
|
||||
# 检查GitLink的用户信息
|
||||
issue = webhook_data.get('issue', {})
|
||||
author = issue.get('author', {})
|
||||
username = author.get('login', '').lower()
|
||||
|
||||
# 如果是同步相关的用户名,认为是机器人操作
|
||||
sync_usernames = ['sync-bot', 'reposync', 'auto-sync']
|
||||
if any(sync_name in username for sync_name in sync_usernames):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def call_sync_api(self, source_platform: str) -> Dict[str, Any]:
|
||||
"""调用同步API"""
|
||||
|
||||
# 根据源平台确定目标平台
|
||||
if source_platform == "gitee":
|
||||
target_platform = "gitlink"
|
||||
source_config = self.repo_config["gitee"]
|
||||
target_config = self.repo_config["gitlink"]
|
||||
elif source_platform == "gitlink":
|
||||
target_platform = "gitee"
|
||||
source_config = self.repo_config["gitlink"]
|
||||
target_config = self.repo_config["gitee"]
|
||||
else:
|
||||
return {"success": False, "message": f"不支持的源平台: {source_platform}"}
|
||||
|
||||
# 构建同步请求
|
||||
sync_request = {
|
||||
"source_platform": source_platform,
|
||||
"source_org": source_config["org"],
|
||||
"source_repo": source_config["repo"],
|
||||
"target_platform": target_platform,
|
||||
"target_org": target_config["org"],
|
||||
"target_repo": target_config["repo"],
|
||||
"sync_comments": False,
|
||||
"sync_milestones": True,
|
||||
"update_existing": True,
|
||||
"enable_deletion": False
|
||||
}
|
||||
|
||||
try:
|
||||
logger.info(f"🚀 调用同步API: {source_platform} → {target_platform}")
|
||||
logger.info(f"📋 同步参数: {json.dumps(sync_request, indent=2, ensure_ascii=False)}")
|
||||
|
||||
response = requests.post(
|
||||
self.sync_api_url,
|
||||
json=sync_request,
|
||||
headers={"Content-Type": "application/json"},
|
||||
timeout=150 # 增加到150秒,适应2分钟左右的同步时间
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
logger.info(f"✅ 同步API调用成功: {result}")
|
||||
return result
|
||||
else:
|
||||
error_msg = f"同步API调用失败: HTTP {response.status_code}"
|
||||
logger.error(f"❌ {error_msg}")
|
||||
return {"success": False, "message": error_msg}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
error_msg = "同步API调用超时"
|
||||
logger.error(f"❌ {error_msg}")
|
||||
return {"success": False, "message": error_msg}
|
||||
except Exception as e:
|
||||
error_msg = f"同步API调用异常: {str(e)}"
|
||||
logger.error(f"❌ {error_msg}")
|
||||
return {"success": False, "message": error_msg}
|
||||
|
||||
def process_webhook(self, webhook_data: Dict[str, Any], headers: Dict[str, str] = None) -> Dict[str, Any]:
|
||||
"""处理webhook请求"""
|
||||
|
||||
logger.info("=" * 60)
|
||||
logger.info("🎣 收到新的Webhook请求")
|
||||
logger.info(f"📅 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 验证密码
|
||||
if not self.verify_webhook_password(webhook_data, headers):
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Webhook密码验证失败",
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
# 检测平台
|
||||
platform = self.detect_platform(webhook_data, headers)
|
||||
if not platform:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "无法识别webhook来源平台",
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
logger.info(f"🔍 识别平台: {platform}")
|
||||
|
||||
# 判断是否需要同步
|
||||
if not self.should_trigger_sync(webhook_data, platform, headers):
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Webhook已接收,但不需要触发同步",
|
||||
"platform": platform,
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
# 提取Issue信息用于日志
|
||||
issue_info = self.extract_issue_info(webhook_data, platform)
|
||||
if issue_info:
|
||||
logger.info(f"📋 Issue信息: {issue_info['title']} (动作: {issue_info['action']}, 用户: {issue_info['user']})")
|
||||
|
||||
# 调用同步API
|
||||
sync_result = self.call_sync_api(platform)
|
||||
|
||||
logger.info("=" * 60)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"Webhook处理完成: {platform} → {'gitlink' if platform == 'gitee' else 'gitee'}",
|
||||
"platform": platform,
|
||||
"issue_info": issue_info,
|
||||
"sync_result": sync_result,
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
|
@ -17,10 +17,11 @@ if issue_sync_path not in sys.path:
|
|||
sys.path.append(issue_sync_path)
|
||||
|
||||
try:
|
||||
from fastapi import FastAPI, BackgroundTasks, Body
|
||||
from fastapi import FastAPI, BackgroundTasks, Body, Request
|
||||
from pydantic import BaseModel
|
||||
import uvicorn
|
||||
from clients.sync_service import IssueSyncService
|
||||
from webhook.webhook_handler import WebhookHandler
|
||||
DEPENDENCIES_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
print("缺少依赖: {}".format(str(e)))
|
||||
|
|
@ -32,6 +33,9 @@ app = FastAPI(
|
|||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 初始化Webhook处理器
|
||||
webhook_handler = WebhookHandler() if DEPENDENCIES_AVAILABLE else None
|
||||
|
||||
class IssueSyncRequest(BaseModel):
|
||||
"""Issue同步请求"""
|
||||
source_org: str
|
||||
|
|
@ -1093,6 +1097,56 @@ async def execute_bidirectional_sync_task(bidirectional_method,
|
|||
except Exception as e:
|
||||
print("💥 双向同步任务执行异常: {}".format(str(e)))
|
||||
|
||||
|
||||
@app.post("/webhook")
|
||||
async def webhook_endpoint(request: Request):
|
||||
"""Webhook接收端点 - 自动同步Gitee和GitLink"""
|
||||
try:
|
||||
if not DEPENDENCIES_AVAILABLE or not webhook_handler:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Webhook服务不可用",
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
# 获取请求头
|
||||
headers = dict(request.headers)
|
||||
|
||||
# 获取请求体
|
||||
webhook_data = await request.json()
|
||||
|
||||
# 处理webhook
|
||||
result = webhook_handler.process_webhook(webhook_data, headers)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
error_msg = f"Webhook处理异常: {str(e)}"
|
||||
print(f"❌ {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_msg,
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
|
||||
@app.get("/webhook/status")
|
||||
async def webhook_status():
|
||||
"""获取Webhook服务状态"""
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"webhook_available": DEPENDENCIES_AVAILABLE and webhook_handler is not None,
|
||||
"supported_platforms": ["gitee", "gitlink"],
|
||||
"repo_config": webhook_handler.repo_config if webhook_handler else None,
|
||||
"loop_detection": "启用 (5分钟窗口)" if webhook_handler else "不可用",
|
||||
"sync_api_url": webhook_handler.sync_api_url if webhook_handler else None
|
||||
},
|
||||
"message": "Webhook服务状态获取成功",
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🎯 启动独立的Issue同步服务...")
|
||||
print("📖 API文档: http://localhost:8001/docs")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
启动所有必需的服务
|
||||
用于Docker容器中同时启动多个服务
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import signal
|
||||
import subprocess
|
||||
from multiprocessing import Process
|
||||
|
||||
def start_main_service():
|
||||
"""启动主服务 (main.py) - 8000端口"""
|
||||
print("🚀 启动主服务 (main.py) - 端口8000...")
|
||||
try:
|
||||
subprocess.run([sys.executable, "main.py"], check=True)
|
||||
except Exception as e:
|
||||
print(f"❌ 主服务启动失败: {str(e)}")
|
||||
|
||||
def start_issue_sync_service():
|
||||
"""启动Issue同步API服务 (issue_sync_web.py) - 8001端口"""
|
||||
print("🔧 启动Issue同步API服务 (issue_sync_web.py) - 端口8001...")
|
||||
try:
|
||||
subprocess.run([sys.executable, "issue_sync_web.py"], check=True)
|
||||
except Exception as e:
|
||||
print(f"❌ Issue同步API服务启动失败: {str(e)}")
|
||||
|
||||
def start_web_ui_service():
|
||||
"""启动Web UI服务 (start_web_ui.py) - 8002端口"""
|
||||
print("🌐 启动Web UI服务 (start_web_ui.py) - 端口8002...")
|
||||
try:
|
||||
subprocess.run([sys.executable, "start_web_ui.py"], check=True)
|
||||
except Exception as e:
|
||||
print(f"❌ Web UI服务启动失败: {str(e)}")
|
||||
|
||||
def signal_handler(signum, frame):
|
||||
"""信号处理器"""
|
||||
print(f"\n📡 收到信号 {signum},正在关闭所有服务...")
|
||||
sys.exit(0)
|
||||
|
||||
def main():
|
||||
"""主函数 - 启动所有服务"""
|
||||
print("🎯 启动reposync服务集群...")
|
||||
print("=" * 60)
|
||||
|
||||
# 注册信号处理器
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# 创建进程列表
|
||||
processes = []
|
||||
|
||||
try:
|
||||
# 启动主服务
|
||||
main_process = Process(target=start_main_service, name="MainService")
|
||||
main_process.start()
|
||||
processes.append(main_process)
|
||||
print("✅ 主服务进程已启动")
|
||||
|
||||
# 等待一秒
|
||||
time.sleep(1)
|
||||
|
||||
# 启动Issue同步API服务
|
||||
sync_process = Process(target=start_issue_sync_service, name="IssueSyncAPI")
|
||||
sync_process.start()
|
||||
processes.append(sync_process)
|
||||
print("✅ Issue同步API服务进程已启动")
|
||||
|
||||
# 等待一秒
|
||||
time.sleep(1)
|
||||
|
||||
# 启动Web UI服务
|
||||
webui_process = Process(target=start_web_ui_service, name="WebUIService")
|
||||
webui_process.start()
|
||||
processes.append(webui_process)
|
||||
print("✅ Web UI服务进程已启动")
|
||||
|
||||
print("\n🎉 所有服务已启动完成!")
|
||||
print("📊 服务状态:")
|
||||
print(" - 主服务 (main.py): http://localhost:8000")
|
||||
print(" - Issue同步API: http://localhost:8001")
|
||||
print(" - Web界面: http://localhost:8002")
|
||||
print(" - Webhook接收: http://localhost:8001/webhook 或 http://localhost:8002/webhook")
|
||||
print("\n⏳ 等待服务运行...")
|
||||
|
||||
# 等待所有进程
|
||||
for process in processes:
|
||||
process.join()
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 收到中断信号,正在关闭服务...")
|
||||
except Exception as e:
|
||||
print(f"❌ 服务启动异常: {str(e)}")
|
||||
finally:
|
||||
# 清理所有进程
|
||||
print("🧹 清理进程...")
|
||||
for process in processes:
|
||||
if process.is_alive():
|
||||
print(f" 终止进程: {process.name}")
|
||||
process.terminate()
|
||||
process.join(timeout=5)
|
||||
if process.is_alive():
|
||||
print(f" 强制杀死进程: {process.name}")
|
||||
process.kill()
|
||||
|
||||
print("👋 所有服务已关闭")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -148,7 +148,77 @@ def create_integrated_app():
|
|||
sync_request = BidirectionalSyncRequest(**body)
|
||||
|
||||
return await original_bidirectional_immediate(sync_request)
|
||||
|
||||
|
||||
# 添加Webhook路由
|
||||
@app.post("/webhook")
|
||||
async def webhook_endpoint(request: Request):
|
||||
"""Webhook接收端点 - 自动同步Gitee和GitLink"""
|
||||
try:
|
||||
# 导入webhook处理器
|
||||
import sys
|
||||
import os
|
||||
issue_sync_path = os.path.join(os.path.dirname(__file__), 'issue_sync_module')
|
||||
if issue_sync_path not in sys.path:
|
||||
sys.path.append(issue_sync_path)
|
||||
|
||||
from webhook.webhook_handler import WebhookHandler
|
||||
|
||||
# 创建处理器实例
|
||||
webhook_handler = WebhookHandler()
|
||||
|
||||
# 获取请求头和数据
|
||||
headers = dict(request.headers)
|
||||
webhook_data = await request.json()
|
||||
|
||||
# 处理webhook
|
||||
result = webhook_handler.process_webhook(webhook_data, headers)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
import time
|
||||
error_msg = f"Webhook处理异常: {str(e)}"
|
||||
print(f"❌ {error_msg}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": error_msg,
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
@app.get("/webhook/status")
|
||||
async def webhook_status():
|
||||
"""获取Webhook服务状态"""
|
||||
try:
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
issue_sync_path = os.path.join(os.path.dirname(__file__), 'issue_sync_module')
|
||||
if issue_sync_path not in sys.path:
|
||||
sys.path.append(issue_sync_path)
|
||||
|
||||
from webhook.webhook_handler import WebhookHandler
|
||||
webhook_handler = WebhookHandler()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"webhook_available": True,
|
||||
"supported_platforms": ["gitee", "gitlink"],
|
||||
"repo_config": webhook_handler.repo_config,
|
||||
"loop_detection": "启用 (5分钟窗口)",
|
||||
"sync_api_url": webhook_handler.sync_api_url
|
||||
},
|
||||
"message": "Webhook服务状态获取成功",
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
except Exception as e:
|
||||
import time
|
||||
return {
|
||||
"success": False,
|
||||
"message": f"Webhook服务不可用: {str(e)}",
|
||||
"timestamp": time.strftime('%Y-%m-%d %H:%M:%S')
|
||||
}
|
||||
|
||||
return app
|
||||
|
||||
def print_banner():
|
||||
|
|
|
|||
|
|
@ -0,0 +1,302 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
模拟真实的Gitee webhook请求测试
|
||||
使用实际的Gitee webhook数据进行测试
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
# 测试配置
|
||||
WEBHOOK_URL = "http://localhost:8001/webhook"
|
||||
|
||||
def test_real_gitee_webhook():
|
||||
"""测试真实的Gitee webhook请求"""
|
||||
print("🔍 模拟真实的Gitee webhook请求...")
|
||||
print("=" * 60)
|
||||
|
||||
# 真实的Gitee webhook headers
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-GIT-OSCHINA-EVENT": "Issue Hook",
|
||||
"X-Gitee-Token": "hzk200407140238",
|
||||
"X-Gitee-Event": "Issue Hook",
|
||||
"User-Agent": "git-oschina-hook",
|
||||
"X-Gitee-Timestamp": "1752060378420",
|
||||
"X-Gitee-Ping": "false"
|
||||
}
|
||||
|
||||
# 真实的Gitee webhook payload
|
||||
payload = {
|
||||
"iid": "ICL7X4",
|
||||
"url": "https://gitee.com/ttk00/testdemo/issues/ICL7X4",
|
||||
"sign": "",
|
||||
"user": {
|
||||
"id": 15796863,
|
||||
"url": "https://gitee.com/lirenqiu",
|
||||
"name": "黄泽楷",
|
||||
"type": "User",
|
||||
"email": "15796863+lirenqiu@user.noreply.gitee.com",
|
||||
"login": "lirenqiu",
|
||||
"remark": None,
|
||||
"html_url": "https://gitee.com/lirenqiu",
|
||||
"username": "lirenqiu",
|
||||
"user_name": "lirenqiu",
|
||||
"avatar_url": "https://gitee.com/assets/no_portrait.png",
|
||||
"site_admin": False
|
||||
},
|
||||
"issue": {
|
||||
"id": 21145432,
|
||||
"body": "wewwe",
|
||||
"user": {
|
||||
"id": 15796863,
|
||||
"url": "https://gitee.com/lirenqiu",
|
||||
"name": "黄泽楷",
|
||||
"type": "User",
|
||||
"email": "15796863+lirenqiu@user.noreply.gitee.com",
|
||||
"login": "lirenqiu",
|
||||
"remark": None,
|
||||
"html_url": "https://gitee.com/lirenqiu",
|
||||
"username": "lirenqiu",
|
||||
"user_name": "lirenqiu",
|
||||
"avatar_url": "https://gitee.com/assets/no_portrait.png",
|
||||
"site_admin": False
|
||||
},
|
||||
"ident": None,
|
||||
"state": "open",
|
||||
"title": "webhook-test-5",
|
||||
"labels": [],
|
||||
"number": "ICL7X4",
|
||||
"assignee": None,
|
||||
"comments": 0,
|
||||
"deadline": None,
|
||||
"html_url": "https://gitee.com/ttk00/testdemo/issues/ICL7X4",
|
||||
"milestone": None,
|
||||
"type_name": "任务",
|
||||
"created_at": "2025-07-09T19:26:16+08:00",
|
||||
"state_name": "待办的",
|
||||
"updated_at": "2025-07-09T19:26:16+08:00",
|
||||
"description": "wewwe",
|
||||
"category_name": None,
|
||||
"collaborators": [],
|
||||
"plan_started_at": None
|
||||
},
|
||||
"state": "open",
|
||||
"title": "webhook-test-5",
|
||||
"action": "open",
|
||||
"sender": {
|
||||
"id": 15796863,
|
||||
"url": "https://gitee.com/lirenqiu",
|
||||
"name": "黄泽楷",
|
||||
"type": "User",
|
||||
"email": "15796863+lirenqiu@user.noreply.gitee.com",
|
||||
"login": "lirenqiu",
|
||||
"remark": None,
|
||||
"html_url": "https://gitee.com/lirenqiu",
|
||||
"username": "lirenqiu",
|
||||
"user_name": "lirenqiu",
|
||||
"avatar_url": "https://gitee.com/assets/no_portrait.png",
|
||||
"site_admin": False
|
||||
},
|
||||
"hook_id": 2008053,
|
||||
"project": {
|
||||
"id": 41394009,
|
||||
"url": "https://gitee.com/ttk00/testdemo",
|
||||
"fork": False,
|
||||
"name": "testdemo",
|
||||
"path": "testdemo",
|
||||
"owner": {
|
||||
"id": 14897624,
|
||||
"url": "https://gitee.com/ttk00",
|
||||
"name": "ttk",
|
||||
"type": "User",
|
||||
"email": "14897624+ttk00@user.noreply.gitee.com",
|
||||
"login": "ttk00",
|
||||
"remark": None,
|
||||
"html_url": "https://gitee.com/ttk00",
|
||||
"username": "ttk00",
|
||||
"user_name": "ttk00",
|
||||
"avatar_url": "https://gitee.com/assets/no_portrait.png",
|
||||
"site_admin": False
|
||||
},
|
||||
"git_url": "git://gitee.com/ttk00/testdemo.git",
|
||||
"license": None,
|
||||
"private": False,
|
||||
"ssh_url": "git@gitee.com:ttk00/testdemo.git",
|
||||
"svn_url": "svn://gitee.com/ttk00/testdemo",
|
||||
"has_wiki": True,
|
||||
"homepage": "",
|
||||
"html_url": "https://gitee.com/ttk00/testdemo",
|
||||
"language": None,
|
||||
"clone_url": "https://gitee.com/ttk00/testdemo.git",
|
||||
"full_name": "ttk00/testdemo",
|
||||
"has_pages": False,
|
||||
"namespace": "ttk00",
|
||||
"pushed_at": "2025-06-24T11:04:25+08:00",
|
||||
"created_at": "2025-06-13T20:50:44+08:00",
|
||||
"has_issues": True,
|
||||
"updated_at": "2025-07-09T19:26:17+08:00",
|
||||
"description": "用来测试本地reposyncer服务是否可以正常启动,以及远端服务器的issue/pr同步功能",
|
||||
"forks_count": 0,
|
||||
"git_ssh_url": "git@gitee.com:ttk00/testdemo.git",
|
||||
"git_svn_url": "svn://gitee.com/ttk00/testdemo",
|
||||
"git_http_url": "https://gitee.com/ttk00/testdemo.git",
|
||||
"default_branch": "master",
|
||||
"watchers_count": 2,
|
||||
"stargazers_count": 0,
|
||||
"open_issues_count": 19,
|
||||
"name_with_namespace": "ttk/testdemo",
|
||||
"path_with_namespace": "ttk00/testdemo"
|
||||
},
|
||||
"assignee": None,
|
||||
"hook_url": None,
|
||||
"password": "hzk200407140238",
|
||||
"hook_name": "issue_hooks",
|
||||
"milestone": None,
|
||||
"push_data": None,
|
||||
"timestamp": "1752060378420",
|
||||
"enterprise": None,
|
||||
"repository": {
|
||||
"id": 41394009,
|
||||
"url": "https://gitee.com/ttk00/testdemo",
|
||||
"fork": False,
|
||||
"name": "testdemo",
|
||||
"path": "testdemo",
|
||||
"owner": {
|
||||
"id": 14897624,
|
||||
"url": "https://gitee.com/ttk00",
|
||||
"name": "ttk",
|
||||
"type": "User",
|
||||
"email": "14897624+ttk00@user.noreply.gitee.com",
|
||||
"login": "ttk00",
|
||||
"remark": None,
|
||||
"html_url": "https://gitee.com/ttk00",
|
||||
"username": "ttk00",
|
||||
"user_name": "ttk00",
|
||||
"avatar_url": "https://gitee.com/assets/no_portrait.png",
|
||||
"site_admin": False
|
||||
},
|
||||
"git_url": "git://gitee.com/ttk00/testdemo.git",
|
||||
"license": None,
|
||||
"private": False,
|
||||
"ssh_url": "git@gitee.com:ttk00/testdemo.git",
|
||||
"svn_url": "svn://gitee.com/ttk00/testdemo",
|
||||
"has_wiki": True,
|
||||
"homepage": "",
|
||||
"html_url": "https://gitee.com/ttk00/testdemo",
|
||||
"language": None,
|
||||
"clone_url": "https://gitee.com/ttk00/testdemo.git",
|
||||
"full_name": "ttk00/testdemo",
|
||||
"has_pages": False,
|
||||
"namespace": "ttk00",
|
||||
"pushed_at": "2025-06-24T11:04:25+08:00",
|
||||
"created_at": "2025-06-13T20:50:44+08:00",
|
||||
"has_issues": True,
|
||||
"updated_at": "2025-07-09T19:26:17+08:00",
|
||||
"description": "用来测试本地reposyncer服务是否可以正常启动,以及远端服务器的issue/pr同步功能",
|
||||
"forks_count": 0,
|
||||
"git_ssh_url": "git@gitee.com:ttk00/testdemo.git",
|
||||
"git_svn_url": "svn://gitee.com/ttk00/testdemo",
|
||||
"git_http_url": "https://gitee.com/ttk00/testdemo.git",
|
||||
"default_branch": "master",
|
||||
"watchers_count": 2,
|
||||
"stargazers_count": 0,
|
||||
"open_issues_count": 19,
|
||||
"name_with_namespace": "ttk/testdemo",
|
||||
"path_with_namespace": "ttk00/testdemo"
|
||||
},
|
||||
"updated_by": {
|
||||
"id": 15796863,
|
||||
"url": "https://gitee.com/lirenqiu",
|
||||
"name": "黄泽楷",
|
||||
"type": "User",
|
||||
"email": "15796863+lirenqiu@user.noreply.gitee.com",
|
||||
"login": "lirenqiu",
|
||||
"remark": None,
|
||||
"html_url": "https://gitee.com/lirenqiu",
|
||||
"username": "lirenqiu",
|
||||
"user_name": "lirenqiu",
|
||||
"avatar_url": "https://gitee.com/assets/no_portrait.png",
|
||||
"site_admin": False
|
||||
},
|
||||
"action_desc": "open",
|
||||
"description": "wewwe",
|
||||
"target_user": None,
|
||||
"change_duration": None
|
||||
}
|
||||
|
||||
print(f"📋 Issue信息:")
|
||||
print(f" 标题: {payload['issue']['title']}")
|
||||
print(f" 编号: {payload['issue']['number']}")
|
||||
print(f" 动作: {payload['action']}")
|
||||
print(f" 用户: {payload['user']['name']} ({payload['user']['login']})")
|
||||
print(f" 仓库: {payload['project']['full_name']}")
|
||||
print()
|
||||
|
||||
print("📤 发送webhook请求...")
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
WEBHOOK_URL,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
print(f"⏱️ 请求耗时: {end_time - start_time:.2f}秒")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ Gitee webhook处理成功:")
|
||||
print(f" 成功状态: {result.get('success', False)}")
|
||||
print(f" 处理消息: {result.get('message', '')}")
|
||||
print(f" 识别平台: {result.get('platform', '')}")
|
||||
|
||||
if 'issue_info' in result:
|
||||
issue_info = result['issue_info']
|
||||
print(f" Issue标题: {issue_info.get('title', '')}")
|
||||
print(f" Issue动作: {issue_info.get('action', '')}")
|
||||
print(f" Issue用户: {issue_info.get('user', '')}")
|
||||
|
||||
if 'sync_result' in result:
|
||||
sync_result = result['sync_result']
|
||||
print(f" 同步结果: {sync_result.get('success', False)}")
|
||||
print(f" 同步消息: {sync_result.get('message', '')}")
|
||||
|
||||
if 'result' in sync_result:
|
||||
sync_detail = sync_result['result']
|
||||
print(f" 同步方向: {sync_detail.get('platform_direction', '')}")
|
||||
print(f" 源仓库: {sync_detail.get('source', '')}")
|
||||
print(f" 目标仓库: {sync_detail.get('target', '')}")
|
||||
|
||||
print(f" 时间戳: {result.get('timestamp', '')}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"❌ webhook处理失败: HTTP {response.status_code}")
|
||||
print(f" 响应内容: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ webhook请求异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("🎯 真实Gitee Webhook测试")
|
||||
print("=" * 60)
|
||||
|
||||
if test_real_gitee_webhook():
|
||||
print("\n🎉 真实Gitee webhook测试成功!")
|
||||
print("✅ 系统能够正确处理真实的Gitee webhook请求")
|
||||
else:
|
||||
print("\n❌ 真实Gitee webhook测试失败")
|
||||
print("⚠️ 请检查服务状态和配置")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,276 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
模拟真实的GitLink webhook请求测试
|
||||
使用实际的GitLink webhook数据进行测试
|
||||
"""
|
||||
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
# 测试配置
|
||||
WEBHOOK_URL = "http://localhost:8001/webhook"
|
||||
|
||||
def test_real_gitlink_webhook():
|
||||
"""测试真实的GitLink webhook请求"""
|
||||
print("🔍 模拟真实的GitLink webhook请求...")
|
||||
print("=" * 60)
|
||||
|
||||
# 真实的GitLink webhook headers
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Gitea-Delivery": "34756161-3ea3-429b-9474-6bd5dd0db6f1",
|
||||
"X-Gitea-Event": "issue_comment",
|
||||
"X-Gitea-Event-Type": "issue_comment",
|
||||
"X-Gitea-Signature": "8a860b72da232f49b96d781f09eafbbf6f4a1a234c976aa258894b9dc4cd4cd8",
|
||||
"X-Gogs-Delivery": "34756161-3ea3-429b-9474-6bd5dd0db6f1",
|
||||
"X-Gogs-Event": "issue_comment",
|
||||
"X-Gogs-Event-Type": "issue_comment",
|
||||
"X-Gogs-Signature": "8a860b72da232f49b96d781f09eafbbf6f4a1a234c976aa258894b9dc4cd4cd8",
|
||||
"X-Hub-Signature": "sha1=1cab338344264d0c674731051eac6015edcb1dbf",
|
||||
"X-Hub-Signature-256": "sha256=8a860b72da232f49b96d781f09eafbbf6f4a1a234c976aa258894b9dc4cd4cd8",
|
||||
"X-GitHub-Delivery": "34756161-3ea3-429b-9474-6bd5dd0db6f1",
|
||||
"X-GitHub-Event": "issue_comment",
|
||||
"X-GitHub-Event-Type": "issue_comment"
|
||||
}
|
||||
|
||||
# 真实的GitLink webhook payload
|
||||
payload = {
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"id": 131229,
|
||||
"project_issues_index": 35,
|
||||
"subject": "双向测试gitee到github的issue,包括内容,评论,状态,里程碑等",
|
||||
"description": "我是秦薪淇",
|
||||
"branch_name": None,
|
||||
"start_date": None,
|
||||
"due_date": None,
|
||||
"created_at": "2025-07-09 14:56",
|
||||
"updated_at": "2025-07-09 20:05",
|
||||
"tags": [],
|
||||
"status": {
|
||||
"id": 1,
|
||||
"name": "新增"
|
||||
},
|
||||
"priority": {
|
||||
"id": 1,
|
||||
"name": "低"
|
||||
},
|
||||
"milestone": None,
|
||||
"author": {
|
||||
"id": 141380,
|
||||
"login": "qinxinqi",
|
||||
"name": "qinxinqi",
|
||||
"email": "qinxinqi@example.org",
|
||||
"image_url": "system/lets/letter_avatars/2/Q/223_120_140/120.png"
|
||||
},
|
||||
"assigners": [],
|
||||
"participants": [
|
||||
{
|
||||
"id": 141380,
|
||||
"login": "qinxinqi",
|
||||
"name": "qinxinqi",
|
||||
"email": "qinxinqi@example.org",
|
||||
"image_url": "system/lets/letter_avatars/2/Q/223_120_140/120.png"
|
||||
}
|
||||
],
|
||||
"comment_journals_count": 1,
|
||||
"operate_journals_count": 1,
|
||||
"attachments": []
|
||||
},
|
||||
"journal": {
|
||||
"id": 430370,
|
||||
"notes": "我秦薪淇实名上网",
|
||||
"comments_count": 0
|
||||
},
|
||||
"project": {
|
||||
"id": 1457398,
|
||||
"identifier": "testdemo",
|
||||
"name": "testdemo",
|
||||
"description": "",
|
||||
"visits": 71,
|
||||
"praises_count": 0,
|
||||
"watchers_count": 0,
|
||||
"issues_count": 24,
|
||||
"pull_requests_count": 1,
|
||||
"forked_count": 0,
|
||||
"is_public": True,
|
||||
"mirror_url": "https://gitee.com/ttk00/testdemo",
|
||||
"type": "mirror",
|
||||
"created_at": "2025-06-13 20:52",
|
||||
"updated_at": "2025-07-09 20:50",
|
||||
"forked_from_project_id": None,
|
||||
"platform": "forge",
|
||||
"author": {
|
||||
"name": "qinxinqi",
|
||||
"type": "User",
|
||||
"login": "qinxinqi",
|
||||
"image_url": "system/lets/letter_avatars/2/Q/223_120_140/120.png"
|
||||
},
|
||||
"category": None,
|
||||
"language": None
|
||||
},
|
||||
"password": "hzk200407140238" # 添加密码验证
|
||||
}
|
||||
|
||||
print(f"📋 Issue信息:")
|
||||
print(f" 标题: {payload['issue']['subject']}")
|
||||
print(f" 编号: {payload['issue']['project_issues_index']}")
|
||||
print(f" 动作: {payload['action']}")
|
||||
print(f" 事件类型: issue_comment")
|
||||
print(f" 用户: {payload['issue']['author']['name']} ({payload['issue']['author']['login']})")
|
||||
print(f" 仓库: {payload['project']['identifier']}")
|
||||
print(f" 评论内容: {payload['journal']['notes']}")
|
||||
print()
|
||||
|
||||
print("📤 发送GitLink webhook请求...")
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
WEBHOOK_URL,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
timeout=60
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
print(f"⏱️ 请求耗时: {end_time - start_time:.2f}秒")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ GitLink webhook处理成功:")
|
||||
print(f" 成功状态: {result.get('success', False)}")
|
||||
print(f" 处理消息: {result.get('message', '')}")
|
||||
print(f" 识别平台: {result.get('platform', '')}")
|
||||
|
||||
if 'issue_info' in result:
|
||||
issue_info = result['issue_info']
|
||||
print(f" Issue标题: {issue_info.get('title', '')}")
|
||||
print(f" Issue动作: {issue_info.get('action', '')}")
|
||||
print(f" Issue用户: {issue_info.get('user', '')}")
|
||||
print(f" 事件类型: {issue_info.get('event_type', '')}")
|
||||
|
||||
if 'sync_result' in result:
|
||||
sync_result = result['sync_result']
|
||||
print(f" 同步结果: {sync_result.get('success', False)}")
|
||||
print(f" 同步消息: {sync_result.get('message', '')}")
|
||||
|
||||
if 'result' in sync_result:
|
||||
sync_detail = sync_result['result']
|
||||
print(f" 同步方向: {sync_detail.get('platform_direction', '')}")
|
||||
print(f" 源仓库: {sync_detail.get('source', '')}")
|
||||
print(f" 目标仓库: {sync_detail.get('target', '')}")
|
||||
|
||||
print(f" 时间戳: {result.get('timestamp', '')}")
|
||||
|
||||
return True
|
||||
else:
|
||||
print(f"❌ webhook处理失败: HTTP {response.status_code}")
|
||||
print(f" 响应内容: {response.text}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ webhook请求异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def test_gitlink_issue_webhook():
|
||||
"""测试GitLink Issue创建webhook(非评论)"""
|
||||
print("\n🔍 模拟GitLink Issue创建webhook...")
|
||||
print("=" * 60)
|
||||
|
||||
# 修改为Issue创建事件的headers
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"X-Gitea-Event": "issues",
|
||||
"X-Gitea-Event-Type": "issues",
|
||||
"X-Gogs-Event": "issues",
|
||||
"X-GitHub-Event": "issues"
|
||||
}
|
||||
|
||||
# Issue创建事件的payload
|
||||
payload = {
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"id": 131230,
|
||||
"project_issues_index": 36,
|
||||
"subject": "GitLink测试Issue创建",
|
||||
"description": "这是一个GitLink创建的测试Issue",
|
||||
"created_at": "2025-07-10 08:00",
|
||||
"updated_at": "2025-07-10 08:00",
|
||||
"author": {
|
||||
"id": 141380,
|
||||
"login": "qinxinqi",
|
||||
"name": "qinxinqi",
|
||||
"email": "qinxinqi@example.org"
|
||||
}
|
||||
},
|
||||
"project": {
|
||||
"id": 1457398,
|
||||
"identifier": "testdemo",
|
||||
"name": "testdemo",
|
||||
"mirror_url": "https://gitee.com/ttk00/testdemo",
|
||||
"author": {
|
||||
"name": "qinxinqi",
|
||||
"login": "qinxinqi"
|
||||
}
|
||||
},
|
||||
"password": "hzk200407140238"
|
||||
}
|
||||
|
||||
print(f"📋 Issue信息:")
|
||||
print(f" 标题: {payload['issue']['subject']}")
|
||||
print(f" 编号: {payload['issue']['project_issues_index']}")
|
||||
print(f" 动作: {payload['action']}")
|
||||
print(f" 事件类型: issues")
|
||||
print(f" 用户: {payload['issue']['author']['name']}")
|
||||
print()
|
||||
|
||||
try:
|
||||
response = requests.post(WEBHOOK_URL, json=payload, headers=headers, timeout=150) # 增加到150秒
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
print("✅ GitLink Issue创建webhook处理成功:")
|
||||
print(f" 处理消息: {result.get('message', '')}")
|
||||
return True
|
||||
else:
|
||||
print(f"❌ webhook处理失败: HTTP {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"❌ webhook请求异常: {str(e)}")
|
||||
return False
|
||||
|
||||
def main():
|
||||
"""主测试函数"""
|
||||
print("🎯 真实GitLink Webhook测试")
|
||||
print("=" * 60)
|
||||
|
||||
tests = [
|
||||
("GitLink Issue评论", test_real_gitlink_webhook),
|
||||
("GitLink Issue创建", test_gitlink_issue_webhook)
|
||||
]
|
||||
|
||||
passed = 0
|
||||
total = len(tests)
|
||||
|
||||
for test_name, test_func in tests:
|
||||
print(f"\n{'='*20} {test_name} {'='*20}")
|
||||
try:
|
||||
if test_func():
|
||||
passed += 1
|
||||
print(f"✅ {test_name} 测试通过")
|
||||
else:
|
||||
print(f"❌ {test_name} 测试失败")
|
||||
except Exception as e:
|
||||
print(f"💥 {test_name} 测试异常: {str(e)}")
|
||||
|
||||
time.sleep(2) # 测试间隔
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"🎯 GitLink测试完成: {passed}/{total} 通过")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,229 @@
|
|||
# 防循环机制详解
|
||||
|
||||
## 概述
|
||||
|
||||
reposync系统实现了一套完整的防循环机制,防止在Gitee和GitLink之间进行Issue同步时出现无限循环。该机制包含两个层次的保护:
|
||||
|
||||
1. **时间窗口防循环** - 基于时间窗口的重复同步检测
|
||||
2. **机器人用户检测** - 识别并跳过机器人创建的Issue
|
||||
|
||||
## 1. 时间窗口防循环机制
|
||||
|
||||
### 核心类:LoopDetector
|
||||
|
||||
```python
|
||||
class LoopDetector:
|
||||
"""循环检测器 - 防止无限同步循环"""
|
||||
|
||||
def __init__(self, window_minutes: int = 5):
|
||||
self.window_minutes = window_minutes # 时间窗口:5分钟
|
||||
self.recent_syncs = {} # 存储最近同步记录 {issue_key: timestamp}
|
||||
```
|
||||
|
||||
### 工作原理
|
||||
|
||||
#### 1.1 Issue唯一标识生成
|
||||
```python
|
||||
def _get_issue_key(self, platform: str, org: str, repo: str, issue_title: str) -> str:
|
||||
"""生成Issue的唯一标识"""
|
||||
return f"{platform}:{org}/{repo}:{issue_title}"
|
||||
```
|
||||
|
||||
**示例**:
|
||||
- Gitee Issue: `"gitee:ttk00/testdemo:webhook-test-5"`
|
||||
- GitLink Issue: `"gitlink:qinxinqi/testdemo:webhook-test-5"`
|
||||
|
||||
#### 1.2 同步检查逻辑
|
||||
```python
|
||||
def should_sync(self, platform: str, org: str, repo: str, issue_title: str) -> bool:
|
||||
"""检查是否应该同步(避免循环)"""
|
||||
issue_key = self._get_issue_key(platform, org, repo, issue_title)
|
||||
current_time = datetime.now()
|
||||
|
||||
# 1. 清理过期记录
|
||||
self._cleanup_old_records(current_time)
|
||||
|
||||
# 2. 检查是否在时间窗口内已经同步过
|
||||
if issue_key in self.recent_syncs:
|
||||
last_sync = self.recent_syncs[issue_key]
|
||||
if current_time - last_sync < timedelta(minutes=self.window_minutes):
|
||||
# 在5分钟内已同步过,跳过
|
||||
return False
|
||||
|
||||
# 3. 记录本次同步时间
|
||||
self.recent_syncs[issue_key] = current_time
|
||||
return True
|
||||
```
|
||||
|
||||
#### 1.3 内存清理机制
|
||||
```python
|
||||
def _cleanup_old_records(self, current_time: datetime):
|
||||
"""清理过期的同步记录"""
|
||||
cutoff_time = current_time - timedelta(minutes=self.window_minutes * 2) # 10分钟前
|
||||
expired_keys = [
|
||||
key for key, timestamp in self.recent_syncs.items()
|
||||
if timestamp < cutoff_time
|
||||
]
|
||||
for key in expired_keys:
|
||||
del self.recent_syncs[key]
|
||||
```
|
||||
|
||||
### 时间窗口机制详解
|
||||
|
||||
| 时间点 | 动作 | 结果 |
|
||||
|--------|------|------|
|
||||
| T0 | Gitee Issue创建 → webhook触发 | ✅ 允许同步到GitLink |
|
||||
| T0+1分钟 | GitLink收到同步 → 可能触发webhook | ❌ 跳过同步(5分钟内) |
|
||||
| T0+3分钟 | 手动重复webhook | ❌ 跳过同步(5分钟内) |
|
||||
| T0+6分钟 | 再次webhook | ✅ 允许同步(超过5分钟) |
|
||||
|
||||
## 2. 机器人用户检测机制
|
||||
|
||||
### 核心方法:_is_sync_created_issue
|
||||
|
||||
```python
|
||||
def _is_sync_created_issue(self, webhook_data: Dict[str, Any], platform: str) -> bool:
|
||||
"""检查是否是同步机器人创建的Issue"""
|
||||
|
||||
if platform == "gitee":
|
||||
# 检查Gitee的用户信息
|
||||
issue = webhook_data.get('issue', {})
|
||||
user = issue.get('user', {})
|
||||
username = user.get('login', '').lower()
|
||||
|
||||
# 机器人用户名列表
|
||||
sync_usernames = ['sync-bot', 'reposync', 'auto-sync']
|
||||
if any(sync_name in username for sync_name in sync_usernames):
|
||||
return True
|
||||
|
||||
elif platform == "gitlink":
|
||||
# 检查GitLink的用户信息
|
||||
issue = webhook_data.get('issue', {})
|
||||
author = issue.get('author', {})
|
||||
username = author.get('login', '').lower()
|
||||
|
||||
# 机器人用户名列表
|
||||
sync_usernames = ['sync-bot', 'reposync', 'auto-sync']
|
||||
if any(sync_name in username for sync_name in sync_usernames):
|
||||
return True
|
||||
|
||||
return False
|
||||
```
|
||||
|
||||
### 机器人检测规则
|
||||
|
||||
**检测的用户名模式**:
|
||||
- `sync-bot` - 同步机器人
|
||||
- `reposync` - 仓库同步
|
||||
- `auto-sync` - 自动同步
|
||||
|
||||
**检测逻辑**:
|
||||
- 如果Issue创建者的用户名包含上述任一关键词,则认为是机器人操作
|
||||
- 机器人创建的Issue会被直接跳过,不进行同步
|
||||
|
||||
## 3. 完整的防循环流程
|
||||
|
||||
### 3.1 Webhook处理流程
|
||||
|
||||
```python
|
||||
def should_trigger_sync(self, webhook_data: Dict[str, Any], platform: str, headers: Dict[str, str] = None) -> bool:
|
||||
"""判断是否应该触发同步"""
|
||||
|
||||
# 1. 检查是否是Issue事件
|
||||
if 'issue' not in webhook_data:
|
||||
return False
|
||||
|
||||
# 2. 检查动作类型
|
||||
action = webhook_data.get('action', '')
|
||||
valid_actions = ['opened', 'open', 'created', 'edited', 'updated', 'closed', 'reopened']
|
||||
if action not in valid_actions:
|
||||
return False
|
||||
|
||||
# 3. 提取Issue信息
|
||||
issue_info = self.extract_issue_info(webhook_data, platform)
|
||||
if not issue_info or not issue_info.get('title'):
|
||||
return False
|
||||
|
||||
# 4. 检查是否是同步机器人创建的Issue(防循环层次1)
|
||||
if self._is_sync_created_issue(webhook_data, platform):
|
||||
logger.info(f"🤖 检测到同步机器人创建的Issue,跳过同步")
|
||||
return False
|
||||
|
||||
# 5. 使用时间窗口循环检测器(防循环层次2)
|
||||
source_config = self.repo_config[platform]
|
||||
return self.loop_detector.should_sync(
|
||||
platform,
|
||||
source_config["org"],
|
||||
source_config["repo"],
|
||||
issue_info["title"]
|
||||
)
|
||||
```
|
||||
|
||||
### 3.2 防循环场景分析
|
||||
|
||||
#### 场景1:正常同步
|
||||
```
|
||||
1. 用户在Gitee创建Issue "新功能需求"
|
||||
2. Gitee webhook → reposync服务
|
||||
3. 检查:非机器人用户 ✅
|
||||
4. 检查:5分钟内未同步过 ✅
|
||||
5. 执行同步:Gitee → GitLink
|
||||
6. 记录同步时间
|
||||
```
|
||||
|
||||
#### 场景2:防循环生效
|
||||
```
|
||||
1. 用户在Gitee创建Issue "新功能需求"
|
||||
2. Gitee webhook → reposync服务 → 同步到GitLink
|
||||
3. GitLink可能触发webhook → reposync服务
|
||||
4. 检查:5分钟内已同步过 ❌
|
||||
5. 跳过同步,防止循环
|
||||
```
|
||||
|
||||
#### 场景3:机器人检测
|
||||
```
|
||||
1. 机器人用户"sync-bot"在Gitee创建Issue
|
||||
2. Gitee webhook → reposync服务
|
||||
3. 检查:是机器人用户 ❌
|
||||
4. 直接跳过同步
|
||||
```
|
||||
|
||||
## 4. 配置参数
|
||||
|
||||
### 时间窗口配置
|
||||
```python
|
||||
# WebhookHandler初始化
|
||||
self.loop_detector = LoopDetector(window_minutes=5) # 5分钟时间窗口
|
||||
```
|
||||
|
||||
### 机器人用户名配置
|
||||
```python
|
||||
sync_usernames = ['sync-bot', 'reposync', 'auto-sync']
|
||||
```
|
||||
|
||||
## 5. 日志输出
|
||||
|
||||
### 成功同步
|
||||
```
|
||||
✅ 允许同步 - Issue 'webhook-test-5' 可以进行同步
|
||||
```
|
||||
|
||||
### 时间窗口防循环
|
||||
```
|
||||
🔄 跳过同步 - Issue 'webhook-test-5' 在 5 分钟内已同步过
|
||||
```
|
||||
|
||||
### 机器人检测
|
||||
```
|
||||
🤖 检测到同步机器人创建的Issue,跳过同步
|
||||
```
|
||||
|
||||
## 6. 优势特点
|
||||
|
||||
1. **双重保护**:时间窗口 + 机器人检测
|
||||
2. **内存高效**:自动清理过期记录
|
||||
3. **平台无关**:支持Gitee和GitLink
|
||||
4. **可配置**:时间窗口和机器人用户名可调整
|
||||
5. **日志完整**:详细的防循环日志记录
|
||||
|
||||
这套防循环机制确保了在双向同步场景下不会出现无限循环,同时保持了系统的高效性和可维护性。
|
||||
Loading…
Reference in New Issue