ForcePilot/backend/package/yuxi/channel/extensions/gitlab/agent_tools.py
Kris 4a4e256955 feat(gitlab): 新增完整 GitLab 渠道插件支持
实现了 GitLab 全功能集成,包含 Webhook 处理、API 客户端、消息收发、会话管理、安全校验、限流器等完整模块,支持 Issues、Merge Requests、流水线等事件监听与交互。
2026-05-21 10:49:01 +08:00

529 lines
22 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
from typing import Any
def build_gitlab_agent_tools(account_id: str, account: dict) -> list[dict[str, Any]]:
return [
{
"name": "gitlab_search_issues",
"description": "搜索 GitLab 项目中的 Issue",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"state": {"type": "string", "enum": ["opened", "closed", "all"], "default": "opened"},
},
"required": ["query"],
},
},
{
"name": "gitlab_create_issue",
"description": "在 GitLab 项目中创建新 Issue",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "Issue 标题"},
"description": {"type": "string", "description": "Issue 描述"},
"labels": {"type": "string", "description": "逗号分隔的标签"},
},
"required": ["title"],
},
},
{
"name": "gitlab_close_issue",
"description": "关闭 GitLab 项目中的 Issue",
"parameters": {
"type": "object",
"properties": {
"issue_iid": {"type": "integer", "description": "Issue 的 iid"},
},
"required": ["issue_iid"],
},
},
{
"name": "gitlab_reopen_issue",
"description": "重新打开 GitLab 项目中的 Issue",
"parameters": {
"type": "object",
"properties": {
"issue_iid": {"type": "integer", "description": "Issue 的 iid"},
},
"required": ["issue_iid"],
},
},
{
"name": "gitlab_search_mr",
"description": "搜索 GitLab 项目中的 Merge Request",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词(标题匹配)"},
"state": {
"type": "string",
"enum": ["opened", "closed", "locked", "merged", "all"],
"default": "opened",
},
},
"required": ["query"],
},
},
{
"name": "gitlab_list_mrs",
"description": "列出 GitLab 项目中的 Merge Request",
"parameters": {
"type": "object",
"properties": {
"state": {
"type": "string",
"enum": ["opened", "closed", "locked", "merged", "all"],
"default": "opened",
},
},
"required": [],
},
},
{
"name": "gitlab_create_mr",
"description": "在 GitLab 项目中创建 Merge Request",
"parameters": {
"type": "object",
"properties": {
"source_branch": {"type": "string", "description": "源分支名"},
"target_branch": {"type": "string", "description": "目标分支名", "default": "main"},
"title": {"type": "string", "description": "MR 标题"},
"description": {"type": "string", "description": "MR 描述"},
"labels": {"type": "string", "description": "逗号分隔的标签"},
},
"required": ["source_branch", "title"],
},
},
{
"name": "gitlab_merge_mr",
"description": "合并 GitLab 项目中的 Merge Request",
"parameters": {
"type": "object",
"properties": {
"mr_iid": {"type": "integer", "description": "MR 的 iid"},
"squash": {"type": "boolean", "description": "是否 squash 合并", "default": False},
"remove_source_branch": {"type": "boolean", "description": "合并后删除源分支", "default": False},
},
"required": ["mr_iid"],
},
},
{
"name": "gitlab_comment",
"description": "在 GitLab Issue 或 MR 中添加评论",
"parameters": {
"type": "object",
"properties": {
"noteable_type": {"type": "string", "enum": ["Issue", "MergeRequest"], "default": "Issue"},
"noteable_iid": {"type": "integer", "description": "Issue/MR 的 iid"},
"body": {"type": "string", "description": "评论内容(支持 GLFM"},
},
"required": ["noteable_iid", "body"],
},
},
{
"name": "gitlab_search_code",
"description": "在 GitLab 项目中搜索代码",
"parameters": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "搜索关键词"},
"scope": {
"type": "string",
"enum": ["blobs", "issues", "merge_requests", "commits", "wiki_blobs", "milestones", "notes"],
"default": "blobs",
},
},
"required": ["query"],
},
},
{
"name": "gitlab_get_pipeline",
"description": "查看 GitLab Pipeline 详情",
"parameters": {
"type": "object",
"properties": {
"pipeline_id": {"type": "integer", "description": "Pipeline ID"},
},
"required": ["pipeline_id"],
},
},
{
"name": "gitlab_list_pipelines",
"description": "列出 GitLab Pipeline 历史",
"parameters": {
"type": "object",
"properties": {
"status": {
"type": "string",
"enum": ["running", "pending", "success", "failed", "canceled", "skipped"],
"description": "按状态过滤",
},
},
"required": [],
},
},
{
"name": "gitlab_get_job_trace",
"description": "查看 GitLab CI Job 的日志(尾部行数)",
"parameters": {
"type": "object",
"properties": {
"job_id": {"type": "integer", "description": "Job ID"},
"tail_lines": {"type": "integer", "description": "获取尾部日志行数", "default": 200},
},
"required": ["job_id"],
},
},
{
"name": "gitlab_get_repository_file",
"description": "读取 GitLab 仓库中的文件内容",
"parameters": {
"type": "object",
"properties": {
"file_path": {"type": "string", "description": "文件路径(如 src/main.py"},
"ref": {"type": "string", "description": "分支/Tag/Commit", "default": "main"},
},
"required": ["file_path"],
},
},
{
"name": "gitlab_list_branches",
"description": "列出 GitLab 项目分支",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
{
"name": "gitlab_list_tags",
"description": "列出 GitLab 项目标签Tag",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
{
"name": "gitlab_add_label",
"description": "为 GitLab 项目创建新标签",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "标签名称"},
"color": {"type": "string", "description": "标签颜色(如 #FF0000"},
"description": {"type": "string", "description": "标签描述"},
},
"required": ["name", "color"],
},
},
{
"name": "gitlab_assign_issue",
"description": "将 GitLab Issue 分配给用户",
"parameters": {
"type": "object",
"properties": {
"issue_iid": {"type": "integer", "description": "Issue 的 iid"},
"assignee_id": {"type": "integer", "description": "用户 ID0 表示取消分配)"},
},
"required": ["issue_iid", "assignee_id"],
},
},
{
"name": "gitlab_set_due_date",
"description": "设置 GitLab Issue 的截止日期",
"parameters": {
"type": "object",
"properties": {
"issue_iid": {"type": "integer", "description": "Issue 的 iid"},
"due_date": {"type": "string", "description": "截止日期YYYY-MM-DD 格式)"},
},
"required": ["issue_iid", "due_date"],
},
},
{
"name": "gitlab_update_issue",
"description": "更新 GitLab Issue 的标题、描述或标签",
"parameters": {
"type": "object",
"properties": {
"issue_iid": {"type": "integer", "description": "Issue 的 iid"},
"title": {"type": "string", "description": "新标题(可选)"},
"description": {"type": "string", "description": "新描述(可选)"},
"labels": {"type": "string", "description": "逗号分隔的标签(可选)"},
},
"required": ["issue_iid"],
},
},
{
"name": "gitlab_trigger_pipeline",
"description": "触发 GitLab CI/CD Pipeline",
"parameters": {
"type": "object",
"properties": {
"ref": {"type": "string", "description": "分支名或 Tag"},
"variables": {"type": "object", "description": "Pipeline 变量"},
},
"required": ["ref"],
},
},
]
async def execute_gitlab_tool(tool_name: str, arguments: dict, account: dict) -> dict:
from yuxi.channel.extensions.gitlab.api import AsyncGitLabClient
client = AsyncGitLabClient(
base_url=account.get("base_url", "https://gitlab.com"),
private_token=account.get("private_token", ""),
ssl_verify=account.get("ssl_verify", True),
)
project_id = account.get("project_id", 0)
try:
if tool_name == "gitlab_search_issues":
result = await client.list_issues(project_id, state=arguments.get("state", "opened"))
issues = []
for issue in result[:10]:
attrs = issue.attributes if hasattr(issue, "attributes") else dict(issue)
issues.append(
{
"iid": attrs.get("iid"),
"title": attrs.get("title"),
"state": attrs.get("state"),
"web_url": attrs.get("web_url"),
}
)
return {"success": True, "issues": issues}
elif tool_name == "gitlab_create_issue":
data = {"title": arguments["title"]}
if arguments.get("description"):
data["description"] = arguments["description"]
if arguments.get("labels"):
data["labels"] = arguments["labels"]
issue = await client.create_issue(project_id, data)
attrs = issue.attributes if hasattr(issue, "attributes") else dict(issue)
return {
"success": True,
"issue": {
"iid": attrs.get("iid"),
"title": attrs.get("title"),
"web_url": attrs.get("web_url"),
},
}
elif tool_name == "gitlab_close_issue":
issue_iid = arguments["issue_iid"]
result = await client.update_issue(project_id, issue_iid, {"state_event": "close"})
attrs = result.attributes if hasattr(result, "attributes") else dict(result)
return {"success": True, "issue": {"iid": attrs.get("iid"), "state": attrs.get("state")}}
elif tool_name == "gitlab_reopen_issue":
issue_iid = arguments["issue_iid"]
result = await client.reopen_issue(project_id, issue_iid)
attrs = result.attributes if hasattr(result, "attributes") else dict(result)
return {"success": True, "issue": {"iid": attrs.get("iid"), "state": attrs.get("state")}}
elif tool_name in ("gitlab_search_mr", "gitlab_list_mrs"):
state = arguments.get("state", "opened")
result = await client.list_mrs(project_id, state=state)
mrs = []
for mr in result[:10]:
attrs = mr.attributes if hasattr(mr, "attributes") else dict(mr)
mrs.append(
{
"iid": attrs.get("iid"),
"title": attrs.get("title"),
"state": attrs.get("state"),
"source_branch": attrs.get("source_branch"),
"target_branch": attrs.get("target_branch"),
"web_url": attrs.get("web_url"),
}
)
return {"success": True, "merge_requests": mrs}
elif tool_name == "gitlab_create_mr":
mr = await client.create_mr(
project_id=project_id,
source_branch=arguments["source_branch"],
target_branch=arguments.get("target_branch", "main"),
title=arguments["title"],
description=arguments.get("description", ""),
labels=arguments.get("labels", ""),
)
attrs = mr.attributes if hasattr(mr, "attributes") else dict(mr)
return {
"success": True,
"mr": {
"iid": attrs.get("iid"),
"title": attrs.get("title"),
"web_url": attrs.get("web_url"),
},
}
elif tool_name == "gitlab_merge_mr":
result = await client.merge_mr(
project_id=project_id,
mr_iid=arguments["mr_iid"],
squash=arguments.get("squash", False),
should_remove_source_branch=arguments.get("remove_source_branch", False),
)
return {"success": True, "result": result}
elif tool_name == "gitlab_comment":
noteable_type = arguments.get("noteable_type", "Issue")
noteable_iid = arguments["noteable_iid"]
body = arguments["body"]
if noteable_type == "MergeRequest":
note = await client.create_mr_note(project_id, noteable_iid, body)
else:
note = await client.create_issue_note(project_id, noteable_iid, body)
attrs = note.attributes if hasattr(note, "attributes") else dict(note)
return {"success": True, "note_id": attrs.get("id")}
elif tool_name == "gitlab_search_code":
scope = arguments.get("scope", "blobs")
query = arguments["query"]
result = await client.search(project_id, scope, query)
items = []
for item in result[:10]:
attrs = item.attributes if hasattr(item, "attributes") else dict(item)
items.append(attrs)
return {"success": True, "results": items}
elif tool_name == "gitlab_get_pipeline":
pipeline = await client.get_pipeline(project_id, arguments["pipeline_id"])
attrs = pipeline.attributes if hasattr(pipeline, "attributes") else dict(pipeline)
return {
"success": True,
"pipeline": {
"id": attrs.get("id"),
"status": attrs.get("status"),
"ref": attrs.get("ref"),
"web_url": attrs.get("web_url"),
},
}
elif tool_name == "gitlab_list_pipelines":
status = arguments.get("status")
kwargs = {}
if status:
kwargs["status"] = status
result = await client.list_pipelines(project_id, **kwargs)
pipelines = []
for p in result[:10]:
attrs = p.attributes if hasattr(p, "attributes") else dict(p)
pipelines.append(
{
"id": attrs.get("id"),
"status": attrs.get("status"),
"ref": attrs.get("ref"),
"web_url": attrs.get("web_url"),
}
)
return {"success": True, "pipelines": pipelines}
elif tool_name == "gitlab_get_job_trace":
trace = await client.get_job_trace(project_id, arguments["job_id"], arguments.get("tail_lines", 200))
return {"success": True, "trace": trace[:10000]}
elif tool_name == "gitlab_get_repository_file":
f = await client.get_file(project_id, arguments["file_path"], arguments.get("ref", "main"))
return {
"success": True,
"file": {
"path": f.get("file_path"),
"content": f.get("content"),
"encoding": f.get("encoding"),
},
}
elif tool_name == "gitlab_list_branches":
result = await client.list_branches(project_id)
branches = []
for b in result[:20]:
attrs = b.attributes if hasattr(b, "attributes") else dict(b)
branches.append({"name": attrs.get("name"), "default": attrs.get("default", False)})
return {"success": True, "branches": branches}
elif tool_name == "gitlab_list_tags":
result = await client.list_tags(project_id)
tags = []
for t in result[:20]:
attrs = t.attributes if hasattr(t, "attributes") else dict(t)
tags.append({"name": attrs.get("name"), "message": attrs.get("message", "")})
return {"success": True, "tags": tags}
elif tool_name == "gitlab_add_label":
label = await client.create_label(
project_id,
arguments["name"],
arguments["color"],
arguments.get("description", ""),
)
attrs = label.attributes if hasattr(label, "attributes") else dict(label)
return {"success": True, "label": {"name": attrs.get("name"), "color": attrs.get("color")}}
elif tool_name == "gitlab_assign_issue":
issue_iid = arguments["issue_iid"]
assignee_id = arguments["assignee_id"]
data = {"assignee_ids": [assignee_id] if assignee_id else []}
result = await client.update_issue(project_id, issue_iid, data)
attrs = result.attributes if hasattr(result, "attributes") else dict(result)
return {
"success": True,
"issue": {
"iid": attrs.get("iid"),
"assignees": attrs.get("assignees", []),
},
}
elif tool_name == "gitlab_set_due_date":
issue_iid = arguments["issue_iid"]
due_date = arguments["due_date"]
result = await client.update_issue(project_id, issue_iid, {"due_date": due_date})
attrs = result.attributes if hasattr(result, "attributes") else dict(result)
return {
"success": True,
"issue": {
"iid": attrs.get("iid"),
"due_date": attrs.get("due_date"),
},
}
elif tool_name == "gitlab_update_issue":
issue_iid = arguments["issue_iid"]
data = {}
if arguments.get("title"):
data["title"] = arguments["title"]
if arguments.get("description"):
data["description"] = arguments["description"]
if arguments.get("labels"):
data["labels"] = arguments["labels"]
result = await client.update_issue(project_id, issue_iid, data)
attrs = result.attributes if hasattr(result, "attributes") else dict(result)
return {
"success": True,
"issue": {
"iid": attrs.get("iid"),
"title": attrs.get("title"),
"web_url": attrs.get("web_url"),
},
}
elif tool_name == "gitlab_trigger_pipeline":
ref = arguments["ref"]
variables = arguments.get("variables")
result = await client.trigger_pipeline(project_id, ref, variables)
attrs = result.attributes if hasattr(result, "attributes") else dict(result)
return {"success": True, "pipeline": {"id": attrs.get("id"), "web_url": attrs.get("web_url")}}
return {"success": False, "error": f"unknown tool: {tool_name}"}
except Exception as e:
return {"success": False, "error": str(e)}