diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..adee0ed --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index ef979a3..272eaea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,11 +13,11 @@ COPY requirements.txt . # 安装依赖 RUN pip install --no-cache-dir -r requirements.txt -RUN mkdir -p log data conf +RUN mkdir -p log data COPY biz ./biz +COPY locales ./locales COPY api.py ./api.py COPY ui.py ./ui.py -COPY conf/prompt_templates.yml ./conf/prompt_templates.yml # 使用 supervisord 作为启动命令 CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"] diff --git a/README.md b/README.md index 1f1d09a..5fc7d8f 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ GITLAB_ACCESS_TOKEN={YOUR_GITLAB_ACCESS_TOKEN} **2. 启动服务** ```bash +git clone https://github.com/sunmh207/ai-codereview-gitlab.git +cd ai-codereview-gitlab docker-compose up -d ``` @@ -93,8 +95,8 @@ docker-compose up -d **1. 获取源码** ```bash -git clone https://github.com/sunmh207/AI-Codereview-Gitlab.git -cd AI-Codereview-Gitlab +git clone https://github.com/sunmh207/ai-codereview-gitlab.git +cd ai-codereview-gitlab ``` **2. 安装依赖** diff --git a/api.py b/api.py index ef40870..d09bb48 100644 --- a/api.py +++ b/api.py @@ -13,16 +13,18 @@ from biz.gitlab.webhook_handler import slugify_url from biz.queue.worker import handle_merge_request_event, handle_push_event, handle_github_pull_request_event, handle_github_push_event from biz.service.review_service import ReviewService +from biz.utils.config_checker import check_config from biz.utils.im import notifier from biz.utils.log import logger from biz.utils.queue import handle_queue from biz.utils.reporter import Reporter -from biz.utils.config_checker import check_config load_dotenv("conf/.env") api_app = Flask(__name__) +from biz.utils.i18n import get_translator +_ = get_translator() PUSH_REVIEW_ENABLED = os.environ.get('PUSH_REVIEW_ENABLED', '0') == '1' @@ -30,9 +32,8 @@ @api_app.route('/') def home(): return """
GitHub project address: - https://github.com/sunmh207/AI-Codereview-Gitlab
-Gitee project address: https://gitee.com/sunminghui/ai-codereview-gitlab
+GitHub project address: + https://github.com/sunmh207/ai-codereview-gitlab
""" @@ -42,6 +43,10 @@ def daily_report(): start_time = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0).timestamp() end_time = datetime.now().replace(hour=23, minute=59, second=59, microsecond=0).timestamp() + # import translator once more due to separate cronjob process + from biz.utils.i18n import get_translator + _ = get_translator() + try: if PUSH_REVIEW_ENABLED: df = ReviewService().get_push_review_logs(updated_at_gte=start_time, updated_at_lte=end_time) @@ -49,8 +54,8 @@ def daily_report(): df = ReviewService().get_mr_review_logs(updated_at_gte=start_time, updated_at_lte=end_time) if df.empty: - logger.info("No data to process.") - return jsonify({'message': 'No data to process.'}), 200 + logger.info(_("No data to process.")) + return jsonify({'message': _('No data to process.')}), 200 # 去重:基于 (author, message) 组合 df_unique = df.drop_duplicates(subset=["author", "commit_messages"]) # 按照 author 排序 @@ -60,12 +65,12 @@ def daily_report(): # 生成日报内容 report_txt = Reporter().generate_report(json.dumps(commits)) # 发送钉钉通知 - notifier.send_notification(content=report_txt, msg_type="markdown", title="代码提交日报") + notifier.send_notification(content=report_txt, msg_type="markdown", title=_("代码提交日报")) # 返回生成的日报内容 return json.dumps(report_txt, ensure_ascii=False, indent=4) except Exception as e: - logger.error(f"Failed to generate daily report: {e}") + logger.error(_("Failed to generate daily report: {}").format(e)) return jsonify({'message': f"Failed to generate daily report: {e}"}), 500 @@ -93,12 +98,12 @@ def setup_scheduler(): # Start the scheduler scheduler.start() - logger.info("Scheduler started successfully.") + logger.info(_("Scheduler started successfully.")) # Shut down the scheduler when exiting the app atexit.register(lambda: scheduler.shutdown()) except Exception as e: - logger.error(f"Error setting up scheduler: {e}") + logger.error(_("Error setting up scheduler: {}").format(e)) logger.error(traceback.format_exc()) @@ -109,46 +114,52 @@ def handle_webhook(): if request.is_json: data = request.get_json() if not data: - return jsonify({"error": "Invalid JSON"}), 400 + return jsonify({"error": _("Invalid JSON")}), 400 # 判断是GitLab还是GitHub的webhook webhook_source = request.headers.get('X-GitHub-Event') - + if webhook_source: # GitHub webhook return handle_github_webhook(webhook_source, data) else: # GitLab webhook return handle_gitlab_webhook(data) else: - return jsonify({'message': 'Invalid data format'}), 400 + return jsonify({'message': _('Invalid data format')}), 400 + def handle_github_webhook(event_type, data): # 获取GitHub配置 github_token = os.getenv('GITHUB_ACCESS_TOKEN') or request.headers.get('X-GitHub-Token') if not github_token: - return jsonify({'message': 'Missing GitHub access token'}), 400 - + return jsonify({'message': _('Missing GitLab URL')}), 400 + github_url = os.getenv('GITHUB_URL') or 'https://github.com' github_url_slug = slugify_url(github_url) - + # 打印整个payload数据 - logger.info(f'Received GitHub event: {event_type}') - logger.info(f'Payload: {json.dumps(data)}') - + logger.info(_('Received event: {}').format(event_type)) + logger.info(_('Payload: {}').format(json.dumps(data))) + if event_type == "pull_request": # 使用handle_queue进行异步处理 handle_queue(handle_github_pull_request_event, data, github_token, github_url, github_url_slug) # 立马返回响应 - return jsonify({'message': f'GitHub request received(event_type={event_type}), will process asynchronously.'}), 200 + return jsonify({'message': _('GitHub request received(event_type={}), will process asynchronously.').format( + event_type)}), 200 elif event_type == "push": # 使用handle_queue进行异步处理 handle_queue(handle_github_push_event, data, github_token, github_url, github_url_slug) # 立马返回响应 - return jsonify({'message': f'GitHub request received(event_type={event_type}), will process asynchronously.'}), 200 + return jsonify({'message': _('GitHub request received(event_type={}), will process asynchronously.').format( + event_type)}), 200 else: - error_message = f'Only pull_request and push events are supported for GitHub webhook, but received: {event_type}.' + error_message = _( + 'Only pull_request and push events are supported for GitHub webhook, but received:: {}.').format( + event_type) logger.error(error_message) return jsonify(error_message), 400 + def handle_gitlab_webhook(data): object_kind = data.get("object_kind") @@ -157,47 +168,50 @@ def handle_gitlab_webhook(data): if not gitlab_url: repository = data.get('repository') if not repository: - return jsonify({'message': 'Missing GitLab URL'}), 400 + return jsonify({'message': _('Missing GitLab URL')}), 400 homepage = repository.get("homepage") if not homepage: - return jsonify({'message': 'Missing GitLab URL'}), 400 + return jsonify({'message': _('Missing GitLab URL')}), 400 try: parsed_url = urlparse(homepage) gitlab_url = f"{parsed_url.scheme}://{parsed_url.netloc}/" except Exception as e: - return jsonify({"error": f"Failed to parse homepage URL: {str(e)}"}), 400 + return jsonify({"error": _("Failed to parse homepage URL: {}").format(str(e))}), 400 # 优先从环境变量获取,如果没有,则从请求头获取 gitlab_token = os.getenv('GITLAB_ACCESS_TOKEN') or request.headers.get('X-Gitlab-Token') # 如果gitlab_token为空,返回错误 if not gitlab_token: - return jsonify({'message': 'Missing GitLab access token'}), 400 + return jsonify({'message': _('Missing GitLab access token')}), 400 gitlab_url_slug = slugify_url(gitlab_url) # 打印整个payload数据,或根据需求进行处理 - logger.info(f'Received event: {object_kind}') - logger.info(f'Payload: {json.dumps(data)}') + logger.info(_('Received event: {}').format(object_kind)) + logger.info(_('Payload: {}').format(json.dumps(data))) # 处理Merge Request Hook if object_kind == "merge_request": # 创建一个新进程进行异步处理 handle_queue(handle_merge_request_event, data, gitlab_token, gitlab_url, gitlab_url_slug) # 立马返回响应 - return jsonify( - {'message': f'Request received(object_kind={object_kind}), will process asynchronously.'}), 200 + return jsonify({'message': _('Request received(object_kind={}), will process asynchronously.').format( + object_kind)}), 200 elif object_kind == "push": # 创建一个新进程进行异步处理 # TODO check if PUSH_REVIEW_ENABLED is needed here handle_queue(handle_push_event, data, gitlab_token, gitlab_url, gitlab_url_slug) # 立马返回响应 - return jsonify( - {'message': f'Request received(object_kind={object_kind}), will process asynchronously.'}), 200 + return jsonify({'message': _('Request received(object_kind={}), will process asynchronously.').format( + object_kind)}), 200 else: - error_message = f'Only merge_request and push events are supported (both Webhook and System Hook), but received: {object_kind}.' + error_message = _( + 'Only merge_request and push events are supported (both Webhook and System Hook), but received: {}.').format( + object_kind) logger.error(error_message) return jsonify(error_message), 400 + if __name__ == '__main__': check_config() # 启动定时任务调度器 diff --git a/biz/cmd/review.py b/biz/cmd/review.py index 0abf8aa..5104ffd 100644 --- a/biz/cmd/review.py +++ b/biz/cmd/review.py @@ -3,28 +3,31 @@ from biz.cmd.func.branch import BranchReviewFunc from biz.cmd.func.complexity import ComplexityReviewFunc from biz.cmd.func.directory import DirectoryReviewFunc +from biz.utils.i18n import get_translator +_ = get_translator() def welcome_message(): - print("\n欢迎使用 Codebase Review 工具!\n") + print(_("\n欢迎使用 Codebase Review 工具!\n")) def get_func_choice(): + _ = get_translator() options = { - "1": ("Review 目录结构规范", DirectoryReviewFunc), - "2": ("Review 代码分支命名规范", BranchReviewFunc), - "3": ("Review 代码复杂度", ComplexityReviewFunc), + "1": (_("Review 目录结构规范"), DirectoryReviewFunc), + "2": (_("Review 代码分支命名规范"), BranchReviewFunc), + "3": (_("Review 代码复杂度"), ComplexityReviewFunc), } - print("📌 请选择功能:") + print(_("📌 请选择功能:")) for key, (desc, _) in options.items(): print(f"{key}. {desc}") while True: - choice = input("请输入数字 (1-3): ").strip() + choice = input(_("请输入数字 (1-3): ")).strip() if choice in options: return options[choice][1] # 返回对应的类 - print("❌ 无效的选择,请输入 1-3") + print(_("❌ 无效的选择,请输入 1-3")) if __name__ == "__main__": diff --git a/biz/event/event_manager.py b/biz/event/event_manager.py index 26e316c..fef1934 100644 --- a/biz/event/event_manager.py +++ b/biz/event/event_manager.py @@ -4,6 +4,9 @@ from biz.service.review_service import ReviewService from biz.utils.im import notifier +from biz.utils.i18n import get_translator +_ = get_translator() + # 定义全局事件管理器(事件信号) event_manager = { "merge_request_reviewed": Signal(), @@ -14,24 +17,33 @@ # 定义事件处理函数 def on_merge_request_reviewed(mr_review_entity: MergeRequestReviewEntity): # 发送IM消息通知 - im_msg = f""" -### 🔀 {mr_review_entity.project_name}: Merge Request + im_msg = _(""" +### 🔀 {project_name}: Merge Request #### 合并请求信息: -- **提交者:** {mr_review_entity.author} +- **提交者:** {author} -- **源分支**: {mr_review_entity.source_branch} -- **目标分支**: {mr_review_entity.target_branch} -- **更新时间**: {mr_review_entity.updated_at} -- **提交信息:** {mr_review_entity.commit_messages} +- **源分支**: {source_branch} +- **目标分支**: {target_branch} +- **更新时间**: {updated_at} +- **提交信息:** {commit_messages} -- [查看合并详情]({mr_review_entity.url}) +- [查看合并详情]({url}) - **AI Review 结果:** -{mr_review_entity.review_result} - """ - notifier.send_notification(content=im_msg, msg_type='markdown', title='Merge Request Review', +{review_result} + """).format( + project_name=mr_review_entity.project_name, + author=mr_review_entity.author, + source_branch=mr_review_entity.source_branch, + target_branch=mr_review_entity.target_branch, + updated_at=mr_review_entity.updated_at, + commit_messages=mr_review_entity.commit_messages, + url=mr_review_entity.url, + review_result=mr_review_entity.review_result + ) + notifier.send_notification(content=im_msg, msg_type='markdown', title=_('Merge Request Review'), project_name=mr_review_entity.project_name, url_slug=mr_review_entity.url_slug) @@ -41,26 +53,32 @@ def on_merge_request_reviewed(mr_review_entity: MergeRequestReviewEntity): def on_push_reviewed(entity: PushReviewEntity): # 发送IM消息通知 - im_msg = f"### 🚀 {entity.project_name}: Push\n\n" - im_msg += "#### 提交记录:\n" + im_msg = _("### 🚀 {project_name}: Push\n\n").format(project_name=entity.project_name) + im_msg += _("#### 提交记录:\n") for commit in entity.commits: message = commit.get('message', '').strip() - author = commit.get('author', 'Unknown Author') + author = commit.get('author', _('Unknown Author')) timestamp = commit.get('timestamp', '') url = commit.get('url', '#') im_msg += ( - f"- **提交信息**: {message}\n" - f"- **提交者**: {author}\n" - f"- **时间**: {timestamp}\n" - f"- [查看提交详情]({url})\n\n" + _("- **提交信息**: {message}\n" + "- **提交者**: {author}\n" + "- **时间**: {timestamp}\n" + "- [查看提交详情]({url})\n\n").format( + message=message, + author=author, + timestamp=timestamp, + url=url + ) ) if entity.review_result: - im_msg += f"#### AI Review 结果: \n {entity.review_result}\n\n" + im_msg += _("#### AI Review 结果: \n {review_result}\n\n").format(review_result=entity.review_result) notifier.send_notification(content=im_msg, msg_type='markdown', - title=f"{entity.project_name} Push Event", project_name=entity.project_name, - url_slug=entity.url_slug) + title=_("{project_name} Push Event").format(project_name=entity.project_name), + project_name=entity.project_name, + url_slug=entity.url_slug) # 记录到数据库 ReviewService().insert_push_review_log(entity) @@ -68,4 +86,4 @@ def on_push_reviewed(entity: PushReviewEntity): # 连接事件处理函数到事件信号 event_manager["merge_request_reviewed"].connect(on_merge_request_reviewed) -event_manager["push_reviewed"].connect(on_push_reviewed) +event_manager["push_reviewed"].connect(on_push_reviewed) \ No newline at end of file diff --git a/biz/github/webhook_handler.py b/biz/github/webhook_handler.py index f2948e7..eea3d1c 100644 --- a/biz/github/webhook_handler.py +++ b/biz/github/webhook_handler.py @@ -1,12 +1,14 @@ -import json import os import re import time import requests +from biz.utils.i18n import get_translator from biz.utils.log import logger +_ = get_translator() + # 从环境变量中获取支持的文件扩展名 SUPPORTED_EXTENSIONS = os.getenv('SUPPORTED_EXTENSIONS', '.java,.py,.php').split(',') @@ -21,9 +23,9 @@ def filter_changes(changes: list): for change in changes: # 优先检查status字段是否为"removed" if change.get('status') == 'removed': - logger.info(f"Detected file deletion via status field: {change.get('new_path')}") + logger.info(_("Detected file deletion via status field: {}").format(change.get('new_path'))) continue - + # 如果没有status字段或status不为"removed",继续检查diff模式 diff = change.get('diff', '') if diff: @@ -34,12 +36,12 @@ def filter_changes(changes: list): if all(line.startswith('-') or not line for line in diff_lines): logger.info(f"Detected file deletion via diff pattern: {change.get('new_path')}") continue - + not_deleted_changes.append(change) - - logger.info(f"SUPPORTED_EXTENSIONS: {SUPPORTED_EXTENSIONS}") - logger.info(f"After filtering deleted files: {not_deleted_changes}") - + + logger.info(_("SUPPORTED_EXTENSIONS: {}").format(SUPPORTED_EXTENSIONS)) + logger.info(_("After filtering deleted files: {}").format(not_deleted_changes)) + # 过滤 `new_path` 以支持的扩展名结尾的元素, 仅保留diff和new_path字段 filtered_changes = [ { @@ -49,7 +51,7 @@ def filter_changes(changes: list): for item in not_deleted_changes if any(item.get('new_path', '').endswith(ext) for ext in SUPPORTED_EXTENSIONS) ] - logger.info(f"After filtering by extension: {filtered_changes}") + logger.info(_("After filtering by extension: {}").format(filtered_changes)) return filtered_changes @@ -78,7 +80,8 @@ def parse_pull_request_event(self): def get_pull_request_changes(self) -> list: # 检查是否为 Pull Request Hook 事件 if self.event_type != 'pull_request': - logger.warn(f"Invalid event type: {self.event_type}. Only 'pull_request' event is supported now.") + logger.warn( + _("Invalid event type: {}. Only 'pull_request' event is supported now.").format(self.event_type)) return [] # GitHub pull request changes API可能存在延迟,多次尝试 @@ -93,7 +96,9 @@ def get_pull_request_changes(self) -> list: } response = requests.get(url, headers=headers) logger.debug( - f"Get changes response from GitHub (attempt {attempt + 1}): {response.status_code}, {response.text}, URL: {url}") + _("Get changes response from GitHub (attempt {attempt}): {response_status_code}, {response_text}, URL: {url}").format( + attempt={attempt + 1}, response_status_code=response.status_code, response_text=response.text, + url=url)) # 检查请求是否成功 if response.status_code == 200: @@ -111,13 +116,14 @@ def get_pull_request_changes(self) -> list: return changes else: logger.info( - f"Changes is empty, retrying in {retry_delay} seconds... (attempt {attempt + 1}/{max_retries}), URL: {url}") + _("Changes is empty, retrying in {retry_delay} seconds... (attempt {attempt}/{max_retries}), URL: {url}").format(retry_delay=retry_delay, + attempt={attempt + 1}, max_retries=max_retries, url=url)) time.sleep(retry_delay) else: - logger.warn(f"Failed to get changes from GitHub (URL: {url}): {response.status_code}, {response.text}") + logger.warn(_("Failed to get changes from GitHub (URL: {url}): {response.status_code}, {response.text}").format(url, response.status_code, response.text)) return [] - logger.warning(f"Max retries ({max_retries}) reached. Changes is still empty.") + logger.warning(_("Max retries ({}) reached. Changes is still empty.").format(max_retries)) return [] # 达到最大重试次数后返回空列表 def get_pull_request_commits(self) -> list: @@ -132,8 +138,8 @@ def get_pull_request_commits(self) -> list: 'Accept': 'application/vnd.github.v3+json' } response = requests.get(url, headers=headers) - logger.debug(f"Get commits response from GitHub: {response.status_code}, {response.text}") - + logger.debug(_("Get commits response from GitHub: {}, {}").format(response.status_code, response.text)) + # 检查请求是否成功 if response.status_code == 200: # 将GitHub的commits转换为GitLab格式的commits @@ -152,7 +158,7 @@ def get_pull_request_commits(self) -> list: gitlab_format_commits.append(gitlab_commit) return gitlab_format_commits else: - logger.warn(f"Failed to get commits: {response.status_code}, {response.text}") + logger.warn(_("Failed to get commits: {}, {}").format(response.status_code, response.text)) return [] def add_pull_request_notes(self, review_result): @@ -165,11 +171,11 @@ def add_pull_request_notes(self, review_result): 'body': review_result } response = requests.post(url, headers=headers, json=data) - logger.debug(f"Add comment to GitHub PR {url}: {response.status_code}, {response.text}") + logger.debug(_("Add comment to GitHub PR {url}: {response_status_code}, {response_text}").format(url=url, response_status_code=response.status_code, response_text=response.text)) if response.status_code == 201: - logger.info("Comment successfully added to pull request.") + logger.info(_("Comment successfully added to pull request.")) else: - logger.error(f"Failed to add comment: {response.status_code}") + logger.error(_("Failed to add comment: {}").format(response.status_code)) logger.error(response.text) @@ -198,7 +204,7 @@ def parse_push_event(self): def get_push_commits(self) -> list: # 检查是否为 Push 事件 if self.event_type != 'push': - logger.warn(f"Invalid event type: {self.event_type}. Only 'push' event is supported now.") + logger.warn(_("Invalid event type: {}. Only 'push' event is supported now.").format(self.event_type)) return [] # 提取提交信息 @@ -212,19 +218,19 @@ def get_push_commits(self) -> list: } commit_details.append(commit_info) - logger.info(f"Collected {len(commit_details)} commits from push event.") + logger.info(_("Collected {} commits from push event.").format(len(commit_details))) return commit_details def add_push_notes(self, message: str): # 添加评论到 GitHub Push 请求的提交中(此处假设是在最后一次提交上添加注释) if not self.commit_list: - logger.warn("No commits found to add notes to.") + logger.warn(_("No commits found to add notes to.")) return # 获取最后一个提交的ID last_commit_id = self.commit_list[-1].get('id') if not last_commit_id: - logger.error("Last commit ID not found.") + logger.error(_("Last commit ID not found.")) return url = f"https://api.github.com/repos/{self.repo_full_name}/commits/{last_commit_id}/comments" @@ -236,11 +242,11 @@ def add_push_notes(self, message: str): 'body': message } response = requests.post(url, headers=headers, json=data) - logger.debug(f"Add comment to commit {last_commit_id}: {response.status_code}, {response.text}") + logger.debug(_("Add comment to commit {last_commit_id}: {response_status_code}, {response_text}").format(last_commit_id, response.status_code, response.text)) if response.status_code == 201: - logger.info("Comment successfully added to push commit.") + logger.info(_("Comment successfully added to push commit.")) else: - logger.error(f"Failed to add comment: {response.status_code}") + logger.error(_("Failed to add comment: {}").format(response.status_code)) logger.error(response.text) def __repository_commits(self, sha: str = "", per_page: int = 100, page: int = 1): @@ -252,13 +258,14 @@ def __repository_commits(self, sha: str = "", per_page: int = 100, page: int = 1 } response = requests.get(url, headers=headers) logger.debug( - f"Get commits response from GitHub for repository_commits: {response.status_code}, {response.text}, URL: {url}") + _("Get commits response from GitHub for repository_commits: {response_status_code}, {response_text}, URL: {url}").format( + response_status_code=response.status_code, response_text=response.text, url=url)) if response.status_code == 200: return response.json() else: logger.warn( - f"Failed to get commits for sha {sha}: {response.status_code}, {response.text}") + _("Failed to get commits for sha {sha}: {response_status_code}, {response_text}").format(sha=sha, response_status_code=response.status_code, response_text=response.text)) return [] def get_parent_commit_id(self, commit_id: str) -> str: @@ -269,7 +276,8 @@ def get_parent_commit_id(self, commit_id: str) -> str: } response = requests.get(url, headers=headers) logger.debug( - f"Get commit response from GitHub: {response.status_code}, {response.text}, URL: {url}") + _("Get commit response from GitHub: {response_status_code}, {response_text}, URL: {url}").format( + response_status_code=response.status_code, response_text=response.text, url=url)) if response.status_code == 200 and response.json().get('parents'): return response.json().get('parents')[0].get('sha', '') @@ -284,7 +292,8 @@ def repository_compare(self, base: str, head: str): } response = requests.get(url, headers=headers) logger.debug( - f"Get changes response from GitHub for repository_compare: {response.status_code}, {response.text}, URL: {url}") + _("Get changes response from GitHub for repository_compare: {response_status_code}, {response_text}, URL: {url}").format( + response_status_code=response.status_code, response_text=response.text, url=url)) if response.status_code == 200: # 转换为GitLab格式的diffs @@ -301,18 +310,18 @@ def repository_compare(self, base: str, head: str): return diffs else: logger.warn( - f"Failed to get changes for repository_compare: {response.status_code}, {response.text}") + _("Failed to get changes for repository_compare: {response.status_code}, {response.text}").format(response_status_code=response.status_code, response_text=response.text)) return [] def get_push_changes(self) -> list: # 检查是否为 Push 事件 if self.event_type != 'push': - logger.warn(f"Invalid event type: {self.event_type}. Only 'push' event is supported now.") + logger.warn(_("Invalid event type: {}. Only 'push' event is supported now.").format(self.event_type)) return [] # 如果没有提交,返回空列表 if not self.commit_list: - logger.info("No commits found in push event.") + logger.info(_("No commits found in push event.")) return [] # 优先尝试compare API获取变更 @@ -330,12 +339,12 @@ def get_push_changes(self) -> list: elif self.webhook_data.get('deleted', False): # 删除分支处理 return [] - + return self.repository_compare(before, after) else: # 如果before和after不存在,尝试通过commits获取 - logger.info("before or after not found in webhook data, trying to get changes from commits.") - + logger.info(_("before or after not found in webhook data, trying to get changes from commits.")) + changes = [] for commit in self.commit_list: commit_id = commit.get('id') @@ -344,5 +353,5 @@ def get_push_changes(self) -> list: if parent_id: commit_changes = self.repository_compare(parent_id, commit_id) changes.extend(commit_changes) - - return changes \ No newline at end of file + + return changes diff --git a/biz/gitlab/webhook_handler.py b/biz/gitlab/webhook_handler.py index ade6e98..b79c639 100644 --- a/biz/gitlab/webhook_handler.py +++ b/biz/gitlab/webhook_handler.py @@ -6,6 +6,9 @@ import requests from biz.utils.log import logger +from biz.utils.i18n import get_translator +_ = get_translator() + # 从环境变量中获取支持的文件扩展名 SUPPORTED_EXTENSIONS = os.getenv('SUPPORTED_EXTENSIONS', '.java,.py,.php').split(',') @@ -76,7 +79,7 @@ def parse_merge_request_event(self): def get_merge_request_changes(self) -> list: # 检查是否为 Merge Request Hook 事件 if self.event_type != 'merge_request': - logger.warn(f"Invalid event type: {self.event_type}. Only 'merge_request' event is supported now.") + logger.warn(_("Invalid event type: {}. Only 'merge_request' event is supported now.").format(self.event_type)) return [] # Gitlab merge request changes API可能存在延迟,多次尝试 @@ -91,8 +94,7 @@ def get_merge_request_changes(self) -> list: } response = requests.get(url, headers=headers, verify=False) logger.debug( - f"Get changes response from GitLab (attempt {attempt + 1}): {response.status_code}, {response.text}, URL: {url}") - + _("Get changes response from GitLab (attempt {}): {}, {}, URL: {}").format(attempt + 1, response.status_code, response.text, url)) # 检查请求是否成功 if response.status_code == 200: changes = response.json().get('changes', []) @@ -100,13 +102,13 @@ def get_merge_request_changes(self) -> list: return changes else: logger.info( - f"Changes is empty, retrying in {retry_delay} seconds... (attempt {attempt + 1}/{max_retries}), URL: {url}") + _("Changes is empty, retrying in {} seconds... (attempt {}/{}), URL: {}").format(retry_delay, attempt + 1, max_retries, url)) time.sleep(retry_delay) else: - logger.warn(f"Failed to get changes from GitLab (URL: {url}): {response.status_code}, {response.text}") + logger.warn(_("Failed to get changes from GitLab (URL: {}): {}, {}").format(url, response.status_code, response.text)) return [] - logger.warning(f"Max retries ({max_retries}) reached. Changes is still empty.") + logger.warning(_("Max retries ({}) reached. Changes is still empty.").format(max_retries)) return [] # 达到最大重试次数后返回空列表 def get_merge_request_commits(self) -> list: @@ -121,12 +123,11 @@ def get_merge_request_commits(self) -> list: 'Private-Token': self.gitlab_token } response = requests.get(url, headers=headers, verify=False) - logger.debug(f"Get commits response from gitlab: {response.status_code}, {response.text}") - # 检查请求是否成功 + logger.debug(_("Get commits response from gitlab: {}, {}").format(response.status_code, response.text)) # 检查请求是否成功 if response.status_code == 200: return response.json() else: - logger.warn(f"Failed to get commits: {response.status_code}, {response.text}") + logger.warn(_("Failed to get commits: {}, {}").format(response.status_code, response.text)) return [] def add_merge_request_notes(self, review_result): @@ -140,11 +141,11 @@ def add_merge_request_notes(self, review_result): 'body': review_result } response = requests.post(url, headers=headers, json=data, verify=False) - logger.debug(f"Add notes to gitlab {url}: {response.status_code}, {response.text}") + logger.debug(_("Add notes to gitlab {}: {}, {}").format(url, response.status_code, response.text)) if response.status_code == 201: logger.info("Note successfully added to merge request.") else: - logger.error(f"Failed to add note: {response.status_code}") + logger.error(_("Failed to add note: {}").format(response.status_code)) logger.error(response.text) @@ -174,7 +175,7 @@ def parse_push_event(self): def get_push_commits(self) -> list: # 检查是否为 Push 事件 if self.event_type != 'push': - logger.warn(f"Invalid event type: {self.event_type}. Only 'push' event is supported now.") + logger.warn(_("Invalid event type: {}. Only 'push' event is supported now.").format(self.event_type)) return [] # 提取提交信息 @@ -188,19 +189,19 @@ def get_push_commits(self) -> list: } commit_details.append(commit_info) - logger.info(f"Collected {len(commit_details)} commits from push event.") + logger.info(_("Collected {} commits from push event.").format(len(commit_details))) return commit_details def add_push_notes(self, message: str): # 添加评论到 GitLab Push 请求的提交中(此处假设是在最后一次提交上添加注释) if not self.commit_list: - logger.warn("No commits found to add notes to.") + logger.warn(_("No commits found to add notes to.")) return # 获取最后一个提交的ID last_commit_id = self.commit_list[-1].get('id') if not last_commit_id: - logger.error("Last commit ID not found.") + logger.error(_("Last commit ID not found.")) return url = urljoin(f"{self.gitlab_url}/", @@ -220,8 +221,7 @@ def add_push_notes(self, message: str): logger.error(f"Failed to add comment: {response.status_code}") logger.error(response.text) - def __repository_commits(self, ref_name: str = "", since: str = "", until: str = "", pre_page: int = 100, - page: int = 1): + def __repository_commits(self, ref_name: str = "", since: str = "", until: str = "", pre_page: int = 100, page: int = 1): # 获取仓库提交信息 url = f"{urljoin(f'{self.gitlab_url}/', f'api/v4/projects/{self.project_id}/repository/commits')}?ref_name={ref_name}&since={since}&until={until}&per_page={pre_page}&page={page}" headers = { @@ -229,7 +229,7 @@ def __repository_commits(self, ref_name: str = "", since: str = "", until: str = } response = requests.get(url, headers=headers, verify=False) logger.debug( - f"Get commits response from GitLab for repository_commits: {response.status_code}, {response.text}, URL: {url}") + _("Get commits response from GitLab for repository_commits: {response.status_code}, {}, URL: {url}").format(response.status_code, response.text, url)) if response.status_code == 200: return response.json() @@ -252,24 +252,24 @@ def repository_compare(self, before: str, after: str): } response = requests.get(url, headers=headers, verify=False) logger.debug( - f"Get changes response from GitLab for repository_compare: {response.status_code}, {response.text}, URL: {url}") + _("Get changes response from GitLab for repository_compare: {}, {response.text}, URL: {}").format(response.status_code, url)) if response.status_code == 200: return response.json().get('diffs', []) else: logger.warn( - f"Failed to get changes for repository_compare: {response.status_code}, {response.text}") + _("Failed to get changes for repository_compare: {}, {}").format(response.status_code, response.text)) return [] def get_push_changes(self) -> list: # 检查是否为 Push 事件 if self.event_type != 'push': - logger.warn(f"Invalid event type: {self.event_type}. Only 'push' event is supported now.") + logger.warn(_("Invalid event type: {}. Only 'push' event is supported now.").format(self.event_type)) return [] # 如果没有提交,返回空列表 if not self.commit_list: - logger.info("No commits found in push event.") + logger.info(_("No commits found in push event.")) return [] headers = { 'Private-Token': self.gitlab_token @@ -290,4 +290,4 @@ def get_push_changes(self) -> list: before = parent_commit_id return self.repository_compare(before, after) else: - return [] + return [] \ No newline at end of file diff --git a/biz/llm/client/deepseek.py b/biz/llm/client/deepseek.py index 9cd63b5..c711db1 100644 --- a/biz/llm/client/deepseek.py +++ b/biz/llm/client/deepseek.py @@ -5,17 +5,20 @@ from biz.llm.client.base import BaseClient from biz.llm.types import NotGiven, NOT_GIVEN +from biz.utils.i18n import get_translator from biz.utils.log import logger +_ = get_translator() + class DeepSeekClient(BaseClient): def __init__(self, api_key: str = None): self.api_key = api_key or os.getenv("DEEPSEEK_API_KEY") self.base_url = os.getenv("DEEPSEEK_API_BASE_URL", "https://api.deepseek.com") if not self.api_key: - raise ValueError("API key is required. Please provide it or set it in the environment variables.") + raise ValueError(_("API key is required. Please provide it or set it in the environment variables.")) - self.client = OpenAI(api_key=self.api_key, base_url=self.base_url) # DeepSeek supports OpenAI API SDK + self.client = OpenAI(api_key=self.api_key, base_url=self.base_url) # DeepSeek supports OpenAI API SDK self.default_model = os.getenv("DEEPSEEK_API_MODEL", "deepseek-chat") def completions(self, @@ -24,25 +27,26 @@ def completions(self, ) -> str: try: model = model or self.default_model - logger.debug(f"Sending request to DeepSeek API. Model: {model}, Messages: {messages}") - + logger.debug(_("Sending request to DeepSeek API. Model: {model}, Messages: {messages}").format(model=model, + messages=messages)) + completion = self.client.chat.completions.create( model=model, messages=messages ) - + if not completion or not completion.choices: - logger.error("Empty response from DeepSeek API") - return "AI服务返回为空,请稍后重试" - + logger.error(_("Empty response from DeepSeek API")) + return _("AI服务返回为空,请稍后重试") + return completion.choices[0].message.content - + except Exception as e: - logger.error(f"DeepSeek API error: {str(e)}") + logger.error(_("DeepSeek API error: {e}").format(e=str(e))) # 检查是否是认证错误 if "401" in str(e): - return "DeepSeek API认证失败,请检查API密钥是否正确" + return _("DeepSeek API认证失败,请检查API密钥是否正确") elif "404" in str(e): - return "DeepSeek API接口未找到,请检查API地址是否正确" + return _("DeepSeek API接口未找到,请检查API地址是否正确") else: - return f"调用DeepSeek API时出错: {str(e)}" + return _("调用DeepSeek API时出错: {e}").format(e=str(e)) diff --git a/biz/llm/client/openai.py b/biz/llm/client/openai.py index 69d35f1..3a8fc60 100644 --- a/biz/llm/client/openai.py +++ b/biz/llm/client/openai.py @@ -5,6 +5,9 @@ from biz.llm.client.base import BaseClient from biz.llm.types import NotGiven, NOT_GIVEN +from biz.utils.i18n import get_translator + +_ = get_translator() class OpenAIClient(BaseClient): @@ -12,7 +15,7 @@ def __init__(self, api_key: str = None): self.api_key = api_key or os.getenv("OPENAI_API_KEY") self.base_url = os.getenv("OPENAI_API_BASE_URL", "https://api.openai.com") if not self.api_key: - raise ValueError("API key is required. Please provide it or set it in the environment variables.") + raise ValueError(_("API key is required. Please provide it or set it in the environment variables.")) self.client = OpenAI(api_key=self.api_key, base_url=self.base_url) self.default_model = os.getenv("OPENAI_API_MODEL", "gpt-4o-mini") diff --git a/biz/llm/client/zhipuai.py b/biz/llm/client/zhipuai.py index 0790cd9..d5075c2 100644 --- a/biz/llm/client/zhipuai.py +++ b/biz/llm/client/zhipuai.py @@ -5,13 +5,16 @@ from biz.llm.client.base import BaseClient from biz.llm.types import NotGiven, NOT_GIVEN +from biz.utils.i18n import get_translator + +_ = get_translator() class ZhipuAIClient(BaseClient): def __init__(self, api_key: str = None): self.api_key = api_key or os.getenv("ZHIPUAI_API_KEY") if not self.api_key: - raise ValueError("API key is required. Please provide it or set it in the environment variables.") + raise ValueError(_("API key is required. Please provide it or set it in the environment variables.")) self.client = ZhipuAI(api_key=api_key) self.default_model = os.getenv("ZHIPUAI_API_MODEL", "GLM-4-Flash") diff --git a/biz/llm/factory.py b/biz/llm/factory.py index c532759..1a4a732 100644 --- a/biz/llm/factory.py +++ b/biz/llm/factory.py @@ -5,8 +5,11 @@ from biz.llm.client.ollama_client import OllamaClient from biz.llm.client.openai import OpenAIClient from biz.llm.client.zhipuai import ZhipuAIClient +from biz.utils.i18n import get_translator from biz.utils.log import logger +_ = get_translator() + class Factory: @staticmethod @@ -16,11 +19,11 @@ def getClient(provider: str = None) -> BaseClient: 'zhipuai': lambda: ZhipuAIClient(), 'openai': lambda: OpenAIClient(), 'deepseek': lambda: DeepSeekClient(), - 'ollama': lambda : OllamaClient() + 'ollama': lambda: OllamaClient() } provider_func = chat_model_providers.get(provider) if provider_func: return provider_func() else: - raise Exception(f'Unknown chat model provider: {provider}') + raise Exception(_('Unknown chat model provider: {provider}'.format(provider=provider))) diff --git a/biz/queue/worker.py b/biz/queue/worker.py index 1fa4774..7fa6f60 100644 --- a/biz/queue/worker.py +++ b/biz/queue/worker.py @@ -2,15 +2,23 @@ import traceback from datetime import datetime +from dotenv import load_dotenv + from biz.entity.review_entity import MergeRequestReviewEntity, PushReviewEntity from biz.event.event_manager import event_manager +from biz.github.webhook_handler import filter_changes as filter_github_changes, \ + PullRequestHandler as GithubPullRequestHandler, PushHandler as GithubPushHandler from biz.gitlab.webhook_handler import filter_changes, MergeRequestHandler, PushHandler -from biz.github.webhook_handler import filter_changes as filter_github_changes, PullRequestHandler as GithubPullRequestHandler, PushHandler as GithubPushHandler from biz.utils.code_reviewer import CodeReviewer from biz.utils.im import notifier from biz.utils.log import logger PUSH_REVIEW_ENABLED = os.environ.get('PUSH_REVIEW_ENABLED', '0') == '1' +load_dotenv() + +from biz.utils.i18n import get_translator + +_ = get_translator() def handle_push_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gitlab_url_slug: str): @@ -19,7 +27,7 @@ def handle_push_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gi logger.info('Push Hook event received') commits = handler.get_push_commits() if not commits: - logger.error('Failed to get commits') + logger.error(_('Failed to get commits')) return review_result = None @@ -30,15 +38,15 @@ def handle_push_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gi logger.info('changes: %s', changes) changes = filter_changes(changes) if not changes: - logger.info('未检测到PUSH代码的修改,修改文件可能不满足SUPPORTED_EXTENSIONS。') - review_result = "关注的文件没有修改" + logger.info(_('未检测到PUSH代码的修改,修改文件可能不满足SUPPORTED_EXTENSIONS。')) + review_result = _("关注的文件没有修改") if len(changes) > 0: commits_text = ';'.join(commit.get('message', '').strip() for commit in commits) review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) score = CodeReviewer.parse_review_score(review_text=review_result) # 将review结果提交到Gitlab的 notes - handler.add_push_notes(f'Auto Review Result: \n{review_result}') + handler.add_push_notes(_('Auto Review Result: \n{}').format(review_result)) # TODO check if not also queueing makes sense here event_manager['push_reviewed'].send(PushReviewEntity( @@ -53,9 +61,9 @@ def handle_push_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gi )) except Exception as e: - error_message = f'服务出现未知错误: {str(e)}\n{traceback.format_exc()}' + error_message = _('服务出现未知错误: {}').format(f'{str(e)}\n{traceback.format_exc()}') notifier.send_notification(content=error_message) - logger.error('出现未知错误: %s', error_message) + logger.error(_('出现未知错误: {}').format(error_message)) def handle_merge_request_event(webhook_data: dict, gitlab_token: str, gitlab_url: str, gitlab_url_slug: str): @@ -70,21 +78,21 @@ def handle_merge_request_event(webhook_data: dict, gitlab_token: str, gitlab_url try: # 解析Webhook数据 handler = MergeRequestHandler(webhook_data, gitlab_token, gitlab_url) - logger.info('Merge Request Hook event received') + logger.info(_('Merge Request Hook event received')) - if (handler.action in ['open', 'update']): # 仅仅在MR创建或更新时进行Code Review + if handler.action in ['open', 'update']: # 仅仅在MR创建或更新时进行Code Review # 获取Merge Request的changes changes = handler.get_merge_request_changes() - logger.info('changes: %s', changes) + logger.info(_('changes: {}').format(changes)) changes = filter_changes(changes) if not changes: - logger.info('未检测到有关代码的修改,修改文件可能不满足SUPPORTED_EXTENSIONS。') + logger.info(_('未检测到有关代码的修改,修改文件可能不满足SUPPORTED_EXTENSIONS。')) return # 获取Merge Request的commits commits = handler.get_merge_request_commits() if not commits: - logger.error('Failed to get commits') + logger.error(_('Failed to get commits')) return # review 代码 @@ -92,7 +100,7 @@ def handle_merge_request_event(webhook_data: dict, gitlab_token: str, gitlab_url review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) # 将review结果提交到Gitlab的 notes - handler.add_merge_request_notes(f'Auto Review Result: \n{review_result}') + handler.add_merge_request_notes(_('Auto Review Result: \n{}').format(review_result)) # dispatch merge_request_reviewed event # TODO check if not also queueing makes sense here @@ -113,20 +121,21 @@ def handle_merge_request_event(webhook_data: dict, gitlab_token: str, gitlab_url ) else: - logger.info(f"Merge Request Hook event, action={handler.action}, ignored.") + logger.info(_("Merge Request Hook event, action={}, ignored.").format(handler.action)) except Exception as e: - error_message = f'AI Code Review 服务出现未知错误: {str(e)}\n{traceback.format_exc()}' + error_message = _('AI Code Review 服务出现未知错误: {}').format(f'{str(e)}\n{traceback.format_exc()}') notifier.send_notification(content=error_message) - logger.error('出现未知错误: %s', error_message) + logger.error(_('出现未知错误: {}').format(error_message)) + def handle_github_push_event(webhook_data: dict, github_token: str, github_url: str, github_url_slug: str): try: handler = GithubPushHandler(webhook_data, github_token, github_url) - logger.info('GitHub Push event received') + logger.info(_('GitHub Push event received')) commits = handler.get_push_commits() if not commits: - logger.error('Failed to get commits') + logger.error(_('Failed to get commits')) return review_result = None @@ -134,11 +143,11 @@ def handle_github_push_event(webhook_data: dict, github_token: str, github_url: if PUSH_REVIEW_ENABLED: # 获取PUSH的changes changes = handler.get_push_changes() - logger.info('changes: %s', changes) + logger.info(_('changes: {}').format(changes)) changes = filter_github_changes(changes) if not changes: - logger.info('未检测到PUSH代码的修改,修改文件可能不满足SUPPORTED_EXTENSIONS。') - review_result = "关注的文件没有修改" + logger.info(_('未检测到PUSH代码的修改,修改文件可能不满足SUPPORTED_EXTENSIONS。')) + review_result = _("关注的文件没有修改") if len(changes) > 0: commits_text = ';'.join(commit.get('message', '').strip() for commit in commits) @@ -159,9 +168,9 @@ def handle_github_push_event(webhook_data: dict, github_token: str, github_url: )) except Exception as e: - error_message = f'服务出现未知错误: {str(e)}\n{traceback.format_exc()}' + error_message = _('服务出现未知错误: {}').format(f'{str(e)}\n{traceback.format_exc()}') notifier.send_notification(content=error_message) - logger.error('出现未知错误: %s', error_message) + logger.error(_('出现未知错误: {}').format(error_message)) def handle_github_pull_request_event(webhook_data: dict, github_token: str, github_url: str, github_url_slug: str): @@ -176,21 +185,21 @@ def handle_github_pull_request_event(webhook_data: dict, github_token: str, gith try: # 解析Webhook数据 handler = GithubPullRequestHandler(webhook_data, github_token, github_url) - logger.info('GitHub Pull Request event received') + logger.info(_('GitHub Pull Request event received')) if (handler.action in ['opened', 'synchronize']): # 仅仅在PR创建或更新时进行Code Review # 获取Pull Request的changes changes = handler.get_pull_request_changes() - logger.info('changes: %s', changes) + logger.info(_('changes: {}').format(changes)) changes = filter_github_changes(changes) if not changes: - logger.info('未检测到有关代码的修改,修改文件可能不满足SUPPORTED_EXTENSIONS。') + logger.info(_('未检测到有关代码的修改,修改文件可能不满足SUPPORTED_EXTENSIONS。')) return # 获取Pull Request的commits commits = handler.get_pull_request_commits() if not commits: - logger.error('Failed to get commits') + logger.error(_('Failed to get commits')) return # review 代码 @@ -198,7 +207,7 @@ def handle_github_pull_request_event(webhook_data: dict, github_token: str, gith review_result = CodeReviewer().review_and_strip_code(str(changes), commits_text) # 将review结果提交到GitHub的 notes - handler.add_pull_request_notes(f'Auto Review Result: \n{review_result}') + handler.add_pull_request_notes(_('Auto Review Result: \n{}').format(review_result)) # dispatch pull_request_reviewed event event_manager['merge_request_reviewed'].send( @@ -216,6 +225,6 @@ def handle_github_pull_request_event(webhook_data: dict, github_token: str, gith )) except Exception as e: - error_message = f'服务出现未知错误: {str(e)}\n{traceback.format_exc()}' + error_message = _('服务出现未知错误: {}').format(f'{str(e)}\n{traceback.format_exc()}') notifier.send_notification(content=error_message) - logger.error('出现未知错误: %s', error_message) + logger.error(_('出现未知错误: {}').format(error_message)) diff --git a/biz/utils/code_reviewer.py b/biz/utils/code_reviewer.py index e31942f..a361bbe 100644 --- a/biz/utils/code_reviewer.py +++ b/biz/utils/code_reviewer.py @@ -7,9 +7,12 @@ from jinja2 import Template from biz.llm.factory import Factory +from biz.utils.i18n import get_translator from biz.utils.log import logger from biz.utils.token_util import count_tokens, truncate_text_by_tokens +_ = get_translator() + class BaseReviewer(abc.ABC): """代码审查基类""" @@ -20,7 +23,8 @@ def __init__(self, prompt_key: str): def _load_prompts(self, prompt_key: str, style="professional") -> Dict[str, Any]: """加载提示词配置""" - prompt_templates_file = "conf/prompt_templates.yml" + lang = os.environ.get('LANGUAGE', 'zh_CN') + prompt_templates_file = os.path.join("locales", lang, "prompt_templates.yml") try: # 在打开 YAML 文件时显式指定编码为 UTF-8,避免使用系统默认的 GBK 编码。 with open(prompt_templates_file, "r", encoding="utf-8") as file: @@ -38,14 +42,14 @@ def render_template(template_str: str) -> str: "user_message": {"role": "user", "content": user_prompt}, } except (FileNotFoundError, KeyError, yaml.YAMLError) as e: - logger.error(f"加载提示词配置失败: {e}") - raise Exception(f"提示词配置加载失败: {e}") + logger.error(_("加载提示词配置失败: {e}").format(e=e)) + raise Exception(_("提示词配置加载失败: {e}").format(e=e)) def call_llm(self, messages: List[Dict[str, Any]]) -> str: """调用 LLM 进行代码审核""" - logger.info(f"向 AI 发送代码 Review 请求, messages: {messages}") + logger.info(_("向 AI 发送代码 Review 请求, messages: {messages}").format(messages=messages)) review_result = self.client.completions(messages=messages) - logger.info(f"收到 AI 返回结果: {review_result}") + logger.info(_("收到 AI 返回结果: {review_result}").format(review_result=review_result)) return review_result @abc.abstractmethod @@ -60,7 +64,7 @@ class CodeReviewer(BaseReviewer): def __init__(self): super().__init__("code_review_prompt") - def review_and_strip_code(self, changes_text: str, commits_text: str = "") -> str: + def review_and_strip_code(self, changes_text: str, commits_text: str = '') -> str: """ Review判断changes_text超出取前REVIEW_MAX_TOKENS个token,超出则截断changes_text, 调用review_code方法,返回review_result,如果review_result是markdown格式,则去掉头尾的``` @@ -69,11 +73,11 @@ def review_and_strip_code(self, changes_text: str, commits_text: str = "") -> st :return: """ # 如果超长,取前REVIEW_MAX_TOKENS个token - review_max_tokens = int(os.getenv("REVIEW_MAX_TOKENS", 10000)) + review_max_tokens = int(os.getenv('REVIEW_MAX_TOKENS', 10000)) # 如果changes为空,打印日志 if not changes_text: - logger.info("代码为空, diffs_text = %", str(changes_text)) - return "代码为空" + logger.info(_('代码为空, diffs_text = {}').format(str(changes_text))) + return _('代码为空') # 计算tokens数量,如果超过REVIEW_MAX_TOKENS,截断changes_text tokens_count = count_tokens(changes_text) @@ -81,6 +85,7 @@ def review_and_strip_code(self, changes_text: str, commits_text: str = "") -> st changes_text = truncate_text_by_tokens(changes_text, review_max_tokens) review_result = self.review_code(changes_text, commits_text).strip() + if review_result.startswith("```markdown") and review_result.endswith("```"): return review_result[11:-3].strip() return review_result @@ -103,6 +108,6 @@ def parse_review_score(review_text: str) -> int: """解析 AI 返回的 Review 结果,返回评分""" if not review_text: return 0 - match = re.search(r"总分[::]\s*(\d+)分?", review_text) + match = re.search(_("总分[::]\\s*\\**(\\d+)分?"), review_text) return int(match.group(1)) if match else 0 diff --git a/biz/utils/config_checker.py b/biz/utils/config_checker.py index a09c662..53e8794 100644 --- a/biz/utils/config_checker.py +++ b/biz/utils/config_checker.py @@ -9,6 +9,9 @@ ENV_FILE_PATH = "conf/.env" load_dotenv(ENV_FILE_PATH) +from i18n import get_translator + +_ = get_translator() REQUIRED_ENV_VARS = [ "LLM_PROVIDER", @@ -30,9 +33,9 @@ def check_env_vars(): """检查环境变量""" missing_vars = [var for var in REQUIRED_ENV_VARS if var not in os.environ] if missing_vars: - logger.warning(f"缺少环境变量: {', '.join(missing_vars)}") + logger.warning(_("缺少环境变量: {}").format(", ".join(missing_vars))) else: - logger.info("所有必要的环境变量均已设置。") + logger.info(_("所有必要的环境变量均已设置。")) def check_llm_provider(): @@ -40,33 +43,35 @@ def check_llm_provider(): llm_provider = os.getenv("LLM_PROVIDER") if not llm_provider: - logger.error("LLM_PROVIDER 未设置!") + logger.error(_("LLM_PROVIDER 未设置!")) return if llm_provider not in LLM_PROVIDERS: - logger.error(f"LLM_PROVIDER 值错误,应为 {LLM_PROVIDERS} 之一。") + logger.error(_("LLM_PROVIDER 值错误,应为 {} 之一。").format(LLM_PROVIDERS)) return required_keys = LLM_REQUIRED_KEYS.get(llm_provider, []) missing_keys = [key for key in required_keys if not os.getenv(key)] if missing_keys: - logger.error(f"当前 LLM 供应商为 {llm_provider},但缺少必要的环境变量: {', '.join(missing_keys)}") + logger.error(_("当前 LLM 供应商为 {},但缺少必要的环境变量: {}").format(llm_provider, ', '.join(missing_keys))) else: - logger.info(f"LLM 供应商 {llm_provider} 的配置项已设置。") + logger.info(_("LLM 供应商 {} 的配置项已设置。").format(llm_provider)) + def check_llm_connectivity(): client = Factory().getClient() - logger.info(f"正在检查 LLM 供应商的连接...") + logger.info(_("正在检查 LLM 供应商的连接...")) if client.ping(): - logger.info("LLM 可以连接成功。") + logger.info(_("LLM 可以连接成功。")) else: - logger.error("LLM连接可能有问题,请检查配置项。") + logger.error(_("LLM连接可能有问题,请检查配置项。")) + def check_config(): """主检查入口""" - logger.info("开始检查配置项...") + logger.info(_("开始检查配置项...")) check_env_vars() check_llm_provider() check_llm_connectivity() - logger.info("配置项检查完成。") \ No newline at end of file + logger.info(_("配置项检查完成。")) diff --git a/biz/utils/i18n.py b/biz/utils/i18n.py new file mode 100644 index 0000000..555247b --- /dev/null +++ b/biz/utils/i18n.py @@ -0,0 +1,25 @@ +import gettext +import os +from typing import Callable + +from dotenv import load_dotenv + +load_dotenv("conf/.env") + + +def init_language(lang_code=None) -> Callable[[str], str]: + if lang_code is None: + lang_code = os.environ.get('LANGUAGE', 'zh_CN') + print(f"Using language: {lang_code}") + lang = gettext.translation("messages", localedir="locales", languages=[lang_code], fallback=True) + lang.install() + global _ + _ = lang.gettext + return _ + + +def get_translator() -> Callable[[str], str]: + return _ + + +init_language() diff --git a/biz/utils/im/dingtalk.py b/biz/utils/im/dingtalk.py index 17c99e2..4c5b3fa 100644 --- a/biz/utils/im/dingtalk.py +++ b/biz/utils/im/dingtalk.py @@ -1,15 +1,13 @@ -import base64 -import hashlib -import hmac import json import os -import time -import urllib.parse import requests +from biz.utils.i18n import get_translator from biz.utils.log import logger +_ = get_translator() + class DingTalkNotifier: def __init__(self, webhook_url=None): @@ -29,7 +27,7 @@ def _get_webhook_url(self, project_name=None, url_slug=None): if self.default_webhook_url: return self.default_webhook_url else: - raise ValueError("未提供项目名称,且未设置默认的钉钉 Webhook URL。") + raise ValueError(_("未提供项目名称,且未设置默认的钉钉 Webhook URL。")) # 构造目标键 target_key_project = f"DINGTALK_WEBHOOK_URL_{project_name.upper()}" @@ -48,11 +46,11 @@ def _get_webhook_url(self, project_name=None, url_slug=None): return self.default_webhook_url # 如果既未找到匹配项,也没有默认值,抛出异常 - raise ValueError(f"未找到项目 '{project_name}' 对应的钉钉Webhook URL,且未设置默认的 Webhook URL。") + raise ValueError(_("未找到项目 '{}' 对应的钉钉Webhook URL,且未设置默认的 Webhook URL。").format(project_name)) def send_message(self, content: str, msg_type='text', title='通知', is_at_all=False, project_name=None, url_slug = None): if not self.enabled: - logger.info("钉钉推送未启用") + logger.info(_("钉钉推送未启用")) return try: @@ -85,8 +83,8 @@ def send_message(self, content: str, msg_type='text', title='通知', is_at_all= response = requests.post(url=post_url, data=json.dumps(message), headers=headers) response_data = response.json() if response_data.get('errmsg') == 'ok': - logger.info(f"钉钉消息发送成功! webhook_url:{post_url}") + logger.info(_("钉钉消息发送成功! webhook_url: {}").format(post_url)) else: - logger.error(f"钉钉消息发送失败! webhook_url:{post_url},errmsg:{response_data.get('errmsg')}") + logger.error(_("钉钉消息发送失败! webhook_url: {}, errmsg: {}").format(post_url, response_data.get('errmsg'))) except Exception as e: - logger.error(f"钉钉消息发送失败! ", e) + logger.error(_("钉钉消息发送失败!"), e) diff --git a/biz/utils/im/feishu.py b/biz/utils/im/feishu.py index 071225d..f828530 100644 --- a/biz/utils/im/feishu.py +++ b/biz/utils/im/feishu.py @@ -1,9 +1,8 @@ -import json import requests import os -import re from biz.utils.log import logger - +from biz.utils.i18n import get_translator +_ = get_translator() class FeishuNotifier: def __init__(self, webhook_url=None): @@ -26,7 +25,7 @@ def _get_webhook_url(self, project_name=None, url_slug=None): if self.default_webhook_url: return self.default_webhook_url else: - raise ValueError("未提供项目名称,且未设置默认的 飞书 Webhook URL。") + raise ValueError(_("未提供项目名称,且未设置默认的 飞书 Webhook URL。")) # 构造目标键 target_key_project = f"FEISHU_WEBHOOK_URL_{project_name.upper()}" @@ -45,7 +44,7 @@ def _get_webhook_url(self, project_name=None, url_slug=None): return self.default_webhook_url # 如果既未找到匹配项,也没有默认值,抛出异常 - raise ValueError(f"未找到项目 '{project_name}' 对应的 Feishu Webhook URL,且未设置默认的 Webhook URL。") + raise ValueError(_("未找到项目 '{project_name}' 对应的 Feishu Webhook URL,且未设置默认的 Webhook URL。")) def send_message(self, content, msg_type='text', title=None, is_at_all=False, project_name=None, url_slug=None): """ @@ -57,7 +56,7 @@ def send_message(self, content, msg_type='text', title=None, is_at_all=False, pr :param project_name: 项目名称 """ if not self.enabled: - logger.info("飞书推送未启用") + logger.info(_("飞书推送未启用")) return try: @@ -117,14 +116,14 @@ def send_message(self, content, msg_type='text', title=None, is_at_all=False, pr ) if response.status_code != 200: - logger.error(f"飞书消息发送失败! webhook_url:{post_url}, error_msg:{response.text}") + logger.error(_("飞书消息发送失败! webhook_url: {post_url}, error_msg: {post_url}").format(post_url=post_url, error_msg=response.text)) return result = response.json() if result.get('msg') != "success": - logger.error(f"发送飞书消息失败! webhook_url:{post_url},errmsg:{result}") + logger.error(_("发送飞书消息失败! webhook_url: {post_url}, errmsg: {result}").format(post_url=post_url, result=result)) else: - logger.info(f"飞书消息发送成功! webhook_url:{post_url}") + logger.info(_("飞书消息发送成功! webhook_url: {post_url}").format(post_url=post_url)) except Exception as e: - logger.error(f"飞书消息发送失败! ", e) + logger.error(_("飞书消息发送失败!"), e) diff --git a/biz/utils/im/notifier.py b/biz/utils/im/notifier.py index 3d6b84f..4b39114 100644 --- a/biz/utils/im/notifier.py +++ b/biz/utils/im/notifier.py @@ -1,9 +1,12 @@ +from biz.utils.i18n import get_translator from biz.utils.im.dingtalk import DingTalkNotifier from biz.utils.im.feishu import FeishuNotifier from biz.utils.im.wecom import WeComNotifier +_ = get_translator() -def send_notification(content, msg_type='text', title="通知", is_at_all=False, project_name=None, url_slug=None): + +def send_notification(content, msg_type='text', title=_("通知"), is_at_all=False, project_name=None, url_slug=None): """ 发送通知消息到配置的平台(钉钉和企业微信) :param content: 消息内容 @@ -15,7 +18,7 @@ def send_notification(content, msg_type='text', title="通知", is_at_all=False, # 钉钉推送 dingtalk_notifier = DingTalkNotifier() dingtalk_notifier.send_message(content=content, msg_type=msg_type, title=title, is_at_all=is_at_all, - project_name=project_name, url_slug=url_slug) + project_name=project_name, url_slug=url_slug) # 企业微信推送 wecom_notifier = WeComNotifier() diff --git a/biz/utils/im/wecom.py b/biz/utils/im/wecom.py index e4327f1..6dee73b 100644 --- a/biz/utils/im/wecom.py +++ b/biz/utils/im/wecom.py @@ -3,7 +3,8 @@ import os import re from biz.utils.log import logger - +from biz.utils.i18n import get_translator +_ = get_translator() class WeComNotifier: def __init__(self, webhook_url=None): @@ -26,7 +27,7 @@ def _get_webhook_url(self, project_name=None, url_slug=None): if self.default_webhook_url: return self.default_webhook_url else: - raise ValueError("未提供项目名称,且未设置默认的企业微信 Webhook URL。") + raise ValueError(_("未提供项目名称,且未设置默认的企业微信 Webhook URL。")) # 构造目标键 target_key_project = f"WECOM_WEBHOOK_URL_{project_name.upper()}" @@ -45,7 +46,7 @@ def _get_webhook_url(self, project_name=None, url_slug=None): return self.default_webhook_url # 如果既未找到匹配项,也没有默认值,抛出异常 - raise ValueError(f"未找到项目 '{project_name}' 对应的企业微信 Webhook URL,且未设置默认的 Webhook URL。") + raise ValueError(_("未找到项目 '{}' 对应的企业微信 Webhook URL,且未设置默认的 Webhook URL。").format(project_name)) def format_markdown_content(self, content, title=None): """ @@ -78,7 +79,7 @@ def send_message(self, content, msg_type='text', title=None, is_at_all=False, pr :param url_slug: GitLab URL Slug """ if not self.enabled: - logger.info("企业微信推送未启用") + logger.info(_("企业微信推送未启用")) return try: @@ -86,20 +87,20 @@ def send_message(self, content, msg_type='text', title=None, is_at_all=False, pr data = self._build_markdown_message(content, title) if msg_type == 'markdown' else self._build_text_message( content, is_at_all) - logger.debug(f"发送企业微信消息: url={post_url}, data={data}") + logger.debug(_("发送企业微信消息: url={post_url}, data={data}").format(post_url=post_url, data=data)) response = self._send_request(post_url, data) if response and response.get('errcode') != 0: - logger.error(f"企业微信消息发送失败! webhook_url:{post_url}, errmsg:{response}") + logger.error(_("企业微信消息发送失败! webhook_url:{}, error_msg:{}").format(post_url, response.text)) if response.get("errmsg") and "markdown.content exceed max length" in response["errmsg"]: - logger.warning("Markdown 消息过长,尝试发送纯文本") + logger.warning(_("Markdown 消息过长,尝试发送纯文本")) data = self._build_text_message(content, is_at_all) self._send_request(post_url, data) else: - logger.info(f"企业微信消息发送成功! webhook_url:{post_url}") + logger.info(_("企业微信消息发送成功! webhook_url: {}").format(post_url)) except Exception as e: - logger.error(f"企业微信消息发送失败! {e}") + logger.error(_("企业微信消息发送失败!"), e) def _send_request(self, url, data): """ 发送请求并返回 JSON 响应 """ @@ -108,9 +109,9 @@ def _send_request(self, url, data): response.raise_for_status() # 触发 HTTP 错误 return response.json() except requests.RequestException as e: - logger.error(f"企业微信消息发送请求失败! url:{url}, error: {e}") + logger.error(_("企业微信消息发送请求失败! url:{url}, error: {e}").format(url=url, e=e)) except json.JSONDecodeError as e: - logger.error(f"企业微信返回的 JSON 解析失败! url:{url}, error: {e}") + logger.error(_("企业微信返回的 JSON 解析失败! url:{url}, error: {e}").format(url=url, e=e)) return None def _build_text_message(self, content, is_at_all): diff --git a/biz/utils/reporter.py b/biz/utils/reporter.py index ab9a56e..ba4e851 100644 --- a/biz/utils/reporter.py +++ b/biz/utils/reporter.py @@ -1,5 +1,6 @@ from biz.llm.factory import Factory - +from biz.utils.i18n import get_translator +_ = get_translator() class Reporter: def __init__(self): @@ -9,6 +10,6 @@ def generate_report(self, data: str) -> str: # 根据data生成报告 return self.client.completions( messages=[ - {"role": "user", "content": f"下面是以json格式记录员工代码提交信息。请总结这些信息,生成每个员工的工作日报摘要。员工姓名直接用json内容中的author属性值,不要进行转换。特别要求:以Markdown格式返回。\n{data}"}, + {"role": "user", "content": _("下面是以json格式记录员工代码提交信息。请总结这些信息,生成每个员工的工作日报摘要。员工姓名直接用json内容中的author属性值,不要进行转换。特别要求:以Markdown格式返回。\n{}").format(data)}, ], ) diff --git a/conf/.env.dist b/conf/.env.dist index 5857290..cb64990 100644 --- a/conf/.env.dist +++ b/conf/.env.dist @@ -53,8 +53,8 @@ LOG_LEVEL=DEBUG REPORT_CRONTAB_EXPRESSION=0 18 * * 1-5 #Gitlab配置 -#GITLAB_URL={YOUR_GITLAB_URL} #部分老版本Gitlab webhook不传递URL,需要开启此配置,示例:https://gitlab.example.com -#GITLAB_ACCESS_TOKEN={YOUR_GITLAB_ACCESS_TOKEN} #系统会优先使用此GITLAB_ACCESS_TOKEN,如果未配置,则使用Webhook 传递的Secret Token +GITLAB_URL={YOUR_GITLAB_URL} #部分老版本Gitlab webhook不传递URL,需要开启此配置,示例:https://gitlab.example.com +GITLAB_ACCESS_TOKEN={YOUR_GITLAB_ACCESS_TOKEN} #系统会优先使用此GITLAB_ACCESS_TOKEN,如果未配置,则使用Webhook 传递的Secret Token #Github配置(如果使用 Github 作为代码托管平台,需要配置此项) #GITHUB_ACCESS_TOKEN={YOUR_GITHUB_ACCESS_TOKEN} @@ -62,6 +62,9 @@ REPORT_CRONTAB_EXPRESSION=0 18 * * 1-5 # 开启Push Review功能(如果不需要push事件触发Code Review,设置为0) PUSH_REVIEW_ENABLED=1 +# language (supported: zh_CN/en_US) +LANGUAGE=zh_CN + # Dashboard登录用户名和密码 DASHBOARD_USER=admin DASHBOARD_PASSWORD=admin diff --git a/doc/faq.md b/doc/faq.md index 8826b18..b4fa75b 100644 --- a/doc/faq.md +++ b/doc/faq.md @@ -75,6 +75,7 @@ DINGTALK_WEBHOOK_example_gitlab_com=https://oapi.dingtalk.com/robot/send?access_ **可能原因** 配置127.0.0.1:11434连接Ollama。由于docker容器的网络模式为bridge,容器内的127.0.0.1并不是宿主机的127.0.0.1,所以连接失败。 +对于Docker Desktop,请使用此地址连接Ollama: http://host.docker.internal:11434。 **解决方案** @@ -109,6 +110,7 @@ docker compose -f docker-compose.prod.yml up -d WORKER_QUEUE=gitlab_test_cn ``` +<<<<<<< HEAD ### 如何配置企业微信和飞书消息推送? **1.配置企业微信推送** @@ -160,3 +162,13 @@ WORKER_QUEUE=gitlab_test_cn GITHUB_ACCESS_TOKEN=your-access-token #替换为你的Access Token ``` +### 翻译 + +更改翻译后无法正确应用,我该怎么办? + +该应用程序使用 python 原生 gettext 实现来加载各种语言的翻译。如果您更改了翻译文件,但是应用程序没有正确应用新的翻译,您可以尝试以下步骤: + +1. 更新翻译: `bash translations_update.sh` +2. 通过向具有空 msgstr 的新行添加值来手动调整 `locales/