日志丰富功能 #20

Open
huaijin wants to merge 53 commits from qinxinqi/reposync:huaijin into master
8 changed files with 1431 additions and 3 deletions
Showing only changes of commit c19023255b - Show all commits

View File

@ -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

View File

@ -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')
}

View File

@ -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")

113
start_services.py Normal file
View File

@ -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()

View File

@ -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():

302
test_gitee_real_webhook.py Normal file
View File

@ -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()

View File

@ -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()

229
防循环机制详解.md Normal file
View File

@ -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. **日志完整**:详细的防循环日志记录
这套防循环机制确保了在双向同步场景下不会出现无限循环,同时保持了系统的高效性和可维护性。