Skip to content

Add localization and full support for english language #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
bd03f2f
feat: add support for multiple locales, add full english translation
mashb1t Mar 18, 2025
8b61897
feat: optimize docker image build speed
mashb1t Mar 18, 2025
e905546
Merge branch 'main' of github.com:mashb1t/AI-Codereview-Gitlab
mashb1t Mar 18, 2025
c7c5c4c
feat: update translations after merge from main
mashb1t Mar 18, 2025
1890345
Merge branch 'main_upstream'
mashb1t Mar 19, 2025
74933fa
Merge branch 'main_upstream'
mashb1t Mar 19, 2025
7dd5f41
Merge branch 'feature/queue-driver-rq'
mashb1t Mar 20, 2025
3e5b442
Merge branch 'main' into feature/github-actions-docker-build-v2
mashb1t Mar 20, 2025
390422d
feat: add multi-stage build for prod and worker
mashb1t Mar 20, 2025
9477839
docs: update repository references
mashb1t Mar 20, 2025
956961f
Merge branch 'feature/github-actions-docker-build-v2'
mashb1t Mar 20, 2025
4b97b46
feat: add multiarch build for amd64 and arm64/v8
mashb1t Mar 20, 2025
dc69164
Merge branch 'main_upstream'
mashb1t Mar 20, 2025
4718bd9
docs: add LICENSE
mashb1t Mar 20, 2025
436b41c
docs: add github files for improved collaboration
mashb1t Mar 20, 2025
5373bfb
Merge branch 'main_upstream' into feature/i18n
mashb1t Mar 20, 2025
4e2af2f
Merge branch 'main_upstream' into feature/i18n
mashb1t Mar 20, 2025
1eb1ba4
Merge branch 'feature/i18n'
mashb1t Mar 20, 2025
316444b
Merge branch 'feature/queue-driver-rq'
mashb1t Mar 20, 2025
b994113
Merge branch 'main_upstream' into feature/i18n
mashb1t Mar 20, 2025
5f02c1b
Merge branch 'feature/i18n'
mashb1t Mar 21, 2025
1c2a334
Merge branch 'feature/queue-driver-rq'
mashb1t Mar 21, 2025
d1478dc
Merge branch 'main_upstream'
mashb1t Mar 21, 2025
4a41cd6
Merge branch 'main_upstream'
mashb1t Mar 22, 2025
c81344d
Merge branch 'main_upstream'
mashb1t Mar 23, 2025
a35f519
Merge branch 'main' into feature/i18n
mashb1t Mar 23, 2025
bafc92a
fix: correctly log in cronjob
mashb1t Mar 28, 2025
be05b3b
feat: adjust regex in prompt template
mashb1t Mar 28, 2025
960a970
Merge branch 'main' into feature/i18n
mashb1t Mar 28, 2025
8c26cc1
Merge branch 'main_upstream'
mashb1t Mar 28, 2025
bdea977
Merge branch 'main' into feature/i18n
mashb1t Mar 28, 2025
baed4b0
fix: add git clone to docker setup instructions again
mashb1t Mar 28, 2025
aa95657
Merge branch 'main' into feature/i18n
mashb1t Mar 28, 2025
fbe52c5
i18n: add translation for latest updates
mashb1t Mar 28, 2025
3dea850
Merge branch 'main' into feature/i18n
mashb1t Mar 28, 2025
857183a
Merge branch 'main_upstream'
mashb1t Mar 29, 2025
f4ae78c
Merge branch 'main' into feature/i18n
mashb1t Mar 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

Expand All @@ -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. 安装依赖**
Expand Down
80 changes: 47 additions & 33 deletions api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,27 @@
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'


@api_app.route('/')
def home():
return """<h2>The code review api server is running.</h2>
<p>GitHub project address: <a href="https://github.com/sunmh207/AI-Codereview-Gitlab" target="_blank">
https://github.com/sunmh207/AI-Codereview-Gitlab</a></p>
<p>Gitee project address: <a href="https://gitee.com/sunminghui/ai-codereview-gitlab" target="_blank">https://gitee.com/sunminghui/ai-codereview-gitlab</a></p>
<p>GitHub project address: <a href="https://github.com/sunmh207/ai-codereview-gitlab" target="_blank">
https://github.com/sunmh207/ai-codereview-gitlab</a></p>
"""


Expand All @@ -42,15 +43,19 @@ 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)
else:
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 排序
Expand All @@ -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


Expand Down Expand Up @@ -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())


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

Expand All @@ -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()
# 启动定时任务调度器
Expand Down
17 changes: 10 additions & 7 deletions biz/cmd/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
62 changes: 40 additions & 22 deletions biz/event/event_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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)

Expand All @@ -41,31 +53,37 @@ 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)


# 连接事件处理函数到事件信号
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)
Loading