From 6af0532185c73798aadfd608ae91202ba34e77f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B0=8F=E4=BA=91?= Date: Tue, 27 Jan 2026 14:17:44 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0webhook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .devops/config.yaml | 6 + .devops/monitor.py | 68 +++++++++- .devops/scripts/dingtalk.py | 243 ++++++++++++++++++++++++++++++++++++ 3 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 .devops/scripts/dingtalk.py diff --git a/.devops/config.yaml b/.devops/config.yaml index 25e5db6..2e686bd 100644 --- a/.devops/config.yaml +++ b/.devops/config.yaml @@ -22,6 +22,12 @@ logging: file: .devops/logs/devops.log max_size: 10485760 # 10MB +# 钉钉通知配置 +dingtalk: + enabled: true # 是否启用钉钉通知 + access_token: ed3533e05cf13e090c098436ee6cd52b2adfa2d85b5b2b9da1ae2bccdaecb8f3 + secret: SEC66372694e16e7e931f53aefb4b847b7fb6c42350a10f0f27fbf4151785353261 + # 基础设施服务配置(只部署一次) infrastructure: - name: ruoyi-mysql diff --git a/.devops/monitor.py b/.devops/monitor.py index 2f64653..1c3dffb 100644 --- a/.devops/monitor.py +++ b/.devops/monitor.py @@ -20,6 +20,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from scripts.log import Logger from scripts import docker, maven, npm from scripts.init import mysql, redis, nacos +from scripts.dingtalk import DingTalkNotifier class GitMonitor: @@ -33,11 +34,13 @@ class GitMonitor: self.global_branch = 'main' self.project_root = None self.runtime_path = None + self.dingtalk_notifier = None # 初始化 self._print_startup_banner() self._load_config() self._init_paths() + self._init_dingtalk() def _print_startup_banner(self): """打印启动横幅""" @@ -89,6 +92,24 @@ class GitMonitor: Logger.error(f"路径初始化失败: {e}") sys.exit(1) + def _init_dingtalk(self): + """初始化钉钉通知器""" + try: + dingtalk_config = self.config.get('dingtalk', {}) + if dingtalk_config.get('enabled', False): + access_token = dingtalk_config.get('access_token') + secret = dingtalk_config.get('secret') + + if access_token: + self.dingtalk_notifier = DingTalkNotifier(access_token, secret) + Logger.info("✓ 钉钉通知已启用") + else: + Logger.warning("钉钉通知已启用但未配置 access_token") + else: + Logger.info("钉钉通知未启用") + except Exception as e: + Logger.warning(f"钉钉通知初始化失败: {e}") + def get_remote_commit(self, repo_url, branch): """获取远程仓库的最新提交 hash""" try: @@ -263,11 +284,22 @@ class GitMonitor: def deploy(self, repo_config): """执行部署流程""" repo_path = self.runtime_path / 'a-cloud-all' + repo_name = repo_config['name'] + commit_hash = self.last_commits.get(repo_name, 'unknown') + start_time = time.time() Logger.separator() - Logger.info(f"开始部署: {repo_config['name']}") + Logger.info(f"开始部署: {repo_name}") Logger.separator() + # 发送构建开始通知 + if self.dingtalk_notifier: + self.dingtalk_notifier.send_build_start( + repo_name=repo_name, + branch=self.global_branch, + commit_hash=commit_hash + ) + try: # 1. 更新主仓库和子模块 if not self.update_main_repo(): @@ -303,12 +335,46 @@ class GitMonitor: service_name = repo_config['docker_service'] if not docker.run_docker_compose(compose_dir, service_name): + # 发送构建失败通知 + if self.dingtalk_notifier: + duration = time.time() - start_time + self.dingtalk_notifier.send_build_failure( + repo_name=repo_name, + branch=self.global_branch, + commit_hash=commit_hash, + error_msg="Docker 部署失败" + ) return False + # 计算构建耗时 + duration = time.time() - start_time + + # 发送构建成功通知 + if self.dingtalk_notifier: + self.dingtalk_notifier.send_build_success( + repo_name=repo_name, + branch=self.global_branch, + commit_hash=commit_hash, + duration=duration + ) + Logger.info(f"部署完成: {repo_config['name']}") return True except Exception as e: + # 计算构建耗时 + duration = time.time() - start_time + + # 发送构建失败通知 + if self.dingtalk_notifier: + self.dingtalk_notifier.send_build_failure( + repo_name=repo_name, + branch=self.global_branch, + commit_hash=commit_hash, + error_msg=str(e), + at_all=True + ) + Logger.error(f"部署异常: {e}") return False diff --git a/.devops/scripts/dingtalk.py b/.devops/scripts/dingtalk.py new file mode 100644 index 0000000..641288d --- /dev/null +++ b/.devops/scripts/dingtalk.py @@ -0,0 +1,243 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +钉钉 Webhook 通知模块 +用于发送构建和部署通知到钉钉群 +""" + +import time +import hmac +import hashlib +import base64 +import urllib.parse +import urllib.request +import json +from datetime import datetime + + +class DingTalkNotifier: + """钉钉通知器""" + + def __init__(self, access_token, secret=None): + """ + 初始化钉钉通知器 + + Args: + access_token: 钉钉机器人的 access_token + secret: 钉钉机器人的加签密钥(可选) + """ + self.access_token = access_token + self.secret = secret + self.base_url = "https://oapi.dingtalk.com/robot/send" + + def _generate_sign(self, timestamp): + """ + 生成钉钉加签 + + Args: + timestamp: 当前时间戳(毫秒) + + Returns: + 签名字符串 + """ + if not self.secret: + return None + + string_to_sign = f'{timestamp}\n{self.secret}' + string_to_sign_enc = string_to_sign.encode('utf-8') + secret_enc = self.secret.encode('utf-8') + + hmac_code = hmac.new( + secret_enc, + string_to_sign_enc, + digestmod=hashlib.sha256 + ).digest() + + sign = urllib.parse.quote_plus(base64.b64encode(hmac_code)) + return sign + + def _build_url(self): + """ + 构建完整的 webhook URL + + Returns: + 完整的 URL 字符串 + """ + url = f"{self.base_url}?access_token={self.access_token}" + + if self.secret: + timestamp = str(round(time.time() * 1000)) + sign = self._generate_sign(timestamp) + url += f"×tamp={timestamp}&sign={sign}" + + return url + + def send_text(self, content, at_mobiles=None, at_all=False): + """ + 发送文本消息 + + Args: + content: 消息内容 + at_mobiles: @的手机号列表 + at_all: 是否@所有人 + + Returns: + 发送结果 (True/False) + """ + data = { + "msgtype": "text", + "text": { + "content": content + }, + "at": { + "atMobiles": at_mobiles or [], + "isAtAll": at_all + } + } + + return self._send_request(data) + + def send_build_success(self, repo_name, branch, commit_hash, duration): + """ + 发送构建成功通知 + + Args: + repo_name: 仓库名称 + branch: 分支名称 + commit_hash: 提交哈希 + duration: 构建耗时(秒) + + Returns: + 发送结果 (True/False) + """ + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + title = f"✅ 构建成功 - {repo_name}" + text = f"""### ✅ 构建成功 + +**仓库**: {repo_name} +**分支**: {branch} +**提交**: {commit_hash[:8]} +**耗时**: {duration:.1f} 秒 +**时间**: {now} + +--- +构建和部署已完成! +""" + return self.send_markdown(title, text) + + def send_build_failure(self, repo_name, branch, commit_hash, error_msg, at_all=False): + """ + 发送构建失败通知 + + Args: + repo_name: 仓库名称 + branch: 分支名称 + commit_hash: 提交哈希 + error_msg: 错误信息 + at_all: 是否@所有人 + + Returns: + 发送结果 (True/False) + """ + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + title = f"❌ 构建失败 - {repo_name}" + # 限制错误信息长度 + error_display = error_msg[:500] if len(error_msg) > 500 else error_msg + text = f"""### ❌ 构建失败 + +**仓库**: {repo_name} +**分支**: {branch} +**提交**: {commit_hash[:8]} +**时间**: {now} + +**错误信息**: +``` +{error_display} +``` + +--- +请检查日志并修复问题! +""" + return self.send_markdown(title, text, at_all=at_all) + + def send_build_start(self, repo_name, branch, commit_hash): + """ + 发送构建开始通知 + + Args: + repo_name: 仓库名称 + branch: 分支名称 + commit_hash: 提交哈希 + + Returns: + 发送结果 (True/False) + """ + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + title = f"🚀 构建开始 - {repo_name}" + text = f"""### 🚀 构建开始 + +**仓库**: {repo_name} +**分支**: {branch} +**提交**: {commit_hash[:8]} +**时间**: {now} + +--- +构建任务已启动,请稍候... +""" + return self.send_markdown(title, text) + + def _send_request(self, data): + """ + 发送 HTTP 请求到钉钉 webhook + + Args: + data: 请求数据字典 + + Returns: + 发送结果 (True/False) + """ + try: + url = self._build_url() + headers = {'Content-Type': 'application/json'} + json_data = json.dumps(data).encode('utf-8') + + req = urllib.request.Request(url, data=json_data, headers=headers) + response = urllib.request.urlopen(req, timeout=10) + result = json.loads(response.read().decode('utf-8')) + + if result.get('errcode') == 0: + return True + else: + print(f"钉钉通知发送失败: {result.get('errmsg')}") + return False + + except Exception as e: + print(f"钉钉通知发送异常: {e}") + return False + + def send_markdown(self, title, text, at_mobiles=None, at_all=False): + """ + 发送 Markdown 消息 + + Args: + title: 消息标题 + text: Markdown 格式的消息内容 + at_mobiles: @的手机号列表 + at_all: 是否@所有人 + + Returns: + 发送结果 (True/False) + """ + data = { + "msgtype": "markdown", + "markdown": { + "title": title, + "text": text + }, + "at": { + "atMobiles": at_mobiles or [], + "isAtAll": at_all + } + } + + return self._send_request(data) \ No newline at end of file