feat: 添加Gitlab项目管理功能,支持创建、更新和通过上下文获取项目

This commit is contained in:
zhaoyingbo 2024-12-21 03:20:22 +00:00
parent f55ca65c30
commit 4dd5d8a36c
14 changed files with 322 additions and 17 deletions

View File

@ -3,6 +3,7 @@
"bunx",
"CEINTL",
"Chakroun",
"CICD",
"commitlint",
"dbaeumer",
"deepseek",

BIN
bun.lockb

Binary file not shown.

View File

@ -14,6 +14,11 @@ const functionMap = {
xAuthor: "zhaoyingbo",
xIcon: "🍪",
},
gitlabAgent: {
xName: "小煎蛋 Gitlab Agent",
xAuthor: "zhaoyingbo",
xIcon: "🐙",
},
}
export default functionMap

View File

@ -0,0 +1,7 @@
import register from "./register"
const gitlabEvent = {
register,
}
export default gitlabEvent

View File

@ -0,0 +1,144 @@
import db from "../../db"
import { GitlabProjectModel } from "../../db/gitlabProject/index."
import { Context } from "../../types"
/**
* Gitlab Event
* @param param0
* @param project
* @returns
*/
const refreshProjectHooks = async (
{ gitlabService }: Context,
project: GitlabProjectModel
) => {
gitlabService.setProjectId(project.projectId)
const eventList = await gitlabService.hook.getList()
const HOOK_URL =
Bun.env.NODE_ENV === "production"
? "https://lark-egg.ai.xiaomi.com/gitlab"
: "https://lark-egg-preview.ai.xiaomi.com/gitlab"
// 找到目标Event
const targetEvent = eventList.find((event) => event.url === HOOK_URL)
// 清除Event
if (!project.openCICDNotify && !project.openMRSummary) {
if (!targetEvent) return
await gitlabService.hook.delete(targetEvent.id)
return
}
// 添加Event
if (targetEvent) return
await gitlabService.hook.add({
url: HOOK_URL,
job_events: true,
merge_requests_events: true,
})
}
const openFunc = async (
ctx: Context,
projectId: number,
func: "openCICDNotify" | "openMRSummary"
) => {
const { logger, larkBody, larkService, larkCard } = ctx
const cardGender = larkCard.child("gitlabAgent")
if (!projectId) {
logger.error(`项目ID格式错误项目ID${projectId}`)
await larkService.message.replyCard(
larkBody.messageId,
cardGender.genErrorCard("项目ID格式错误请检查项目ID是否正确")
)
return
}
const project = await db.gitlabProject.getAndCreate(
projectId,
ctx.gitlabService
)
if (!project) {
logger.error(`项目不存在项目ID${projectId}`)
await larkService.message.replyCard(
larkBody.messageId,
cardGender.genErrorCard("该项目不存在请检查项目ID是否正确")
)
return
}
const newProj = await db.gitlabProject.update(project.id, {
[func]: true,
})
if (!newProj) {
logger.error(`更新项目失败项目ID${projectId}`)
await larkService.message.replyCard(
larkBody.messageId,
cardGender.genErrorCard("操作失败,请稍后再试")
)
return
}
await refreshProjectHooks(ctx, newProj)
await larkService.message.replyCard(
larkBody.messageId,
cardGender.genSuccessCard(
`已开启${func === "openCICDNotify" ? "CI/CD成功提醒" : "MR自动总结"}`
)
)
}
const closeFunc = async (
ctx: Context,
projectId: number,
func: "openCICDNotify" | "openMRSummary"
) => {
const { logger, larkBody, larkService, larkCard } = ctx
const cardGender = larkCard.child("gitlabAgent")
if (!projectId) {
logger.error(`项目ID格式错误项目ID${projectId}`)
await larkService.message.replyCard(
larkBody.messageId,
cardGender.genErrorCard("项目ID格式错误请检查项目ID是否正确")
)
return
}
const project = await db.gitlabProject.getByProjectId(projectId)
if (!project) {
logger.error(`项目不存在项目ID${projectId}`)
await larkService.message.replyCard(
larkBody.messageId,
cardGender.genSuccessCard(
`已关闭${func === "openCICDNotify" ? "CI/CD成功提醒" : "MR自动总结"}`
)
)
return
}
const newProj = await db.gitlabProject.update(project.id, {
[func]: false,
})
if (!newProj) {
logger.error(`更新项目失败项目ID${projectId}`)
await larkService.message.replyCard(
larkBody.messageId,
cardGender.genErrorCard("操作失败,请稍后再试")
)
return
}
await refreshProjectHooks(ctx, newProj)
await larkService.message.replyCard(
larkBody.messageId,
cardGender.genSuccessCard(
`已关闭${func === "openCICDNotify" ? "CI/CD成功提醒" : "MR自动总结"}`
)
)
}
const register = {
openCICDNotify: async (ctx: Context, projectId: number) =>
openFunc(ctx, projectId, "openCICDNotify"),
closeCICDNotify: async (ctx: Context, projectId: number) =>
closeFunc(ctx, projectId, "openCICDNotify"),
openMRSummary: async (ctx: Context, projectId: number) =>
openFunc(ctx, projectId, "openMRSummary"),
closeMRSummary: async (ctx: Context, projectId: number) =>
closeFunc(ctx, projectId, "openMRSummary"),
}
export default register

View File

@ -0,0 +1,97 @@
import { Gitlab, GitlabService } from "@egg/net-tool"
import { RecordModel } from "pocketbase"
import { Context } from "../../types"
import { managePbError } from "../../utils/pbTools"
import pbClient from "../pbClient"
const DB_NAME = "gitlabProject"
// Gitlab项目接口定义
export interface GitlabProject {
projectId: number
name: string
desc: string
pathWithNamespace: string
webUrl: string
openCICDNotify: boolean
openMRSummary: boolean
}
// Gitlab项目模型类型
export type GitlabProjectModel = GitlabProject & RecordModel
/**
* Gitlab项目
* @param {GitlabProject} project - Gitlab项目信息
* @returns {Promise<GitlabProjectModel>} - Gitlab项目模型
*/
const create = async (project: GitlabProject) =>
managePbError<GitlabProjectModel>(() =>
pbClient.collection(DB_NAME).create(project)
)
/**
* ID获取Gitlab项目
* @param {number} projectId - ID
* @returns {Promise<GitlabProjectModel | null>} - Gitlab项目模型或null
*/
const getByProjectId = async (projectId: number) =>
managePbError<GitlabProjectModel>(() =>
pbClient.collection(DB_NAME).getFirstListItem(`projectId = "${projectId}"`)
)
/**
* ID获取Gitlab项目
* @param {number} projectId - ID
* @param {GitlabService} gitlabService - Gitlab服务
* @returns {Promise<GitlabProjectModel | null>} - Gitlab项目模型或null
*/
const getAndCreate = async (
projectId: number,
gitlabService: GitlabService
) => {
const project = await getByProjectId(projectId)
if (project) return project
gitlabService.setProjectId(projectId)
const projectInfo = await gitlabService.project.getDetail()
if (!projectInfo) return null
const { name, description, path_with_namespace, web_url } = projectInfo
const newProject = {
projectId,
name,
desc: description,
pathWithNamespace: path_with_namespace,
webUrl: web_url,
openCICDNotify: false,
openMRSummary: false,
}
return await create(newProject)
}
/**
* Gitlab项目
* @param {Context} context -
* @returns {Promise<GitlabProjectModel | null>} - Gitlab项目模型或null
*/
const getByCtx = async ({ body: rawBody, gitlabService }: Context) => {
const body = rawBody as Gitlab.PipelineEvent | Gitlab.MergeRequestEvent
const projectId = body.project.id
if (!projectId) return null
return await getAndCreate(projectId, gitlabService)
}
const update = async (id: string, project: Partial<GitlabProjectModel>) =>
managePbError<GitlabProjectModel>(() =>
pbClient.collection(DB_NAME).update(id, project)
)
const gitlabProject = {
create,
update,
getByProjectId,
getByCtx,
getAndCreate,
}
export default gitlabProject

View File

@ -3,6 +3,8 @@ import { RecordModel } from "pocketbase"
import { managePbError } from "../../utils/pbTools"
import pbClient from "../pbClient"
const DB_NAME = "groupSummaryLog"
export interface GroupSummaryLog {
subscription: string
content: string
@ -18,7 +20,7 @@ export type GroupSummaryLogModel = GroupSummaryLog & RecordModel
*/
const create = async (log: GroupSummaryLog) =>
managePbError<GroupSummaryLogModel>(() =>
pbClient.collection("groupSummaryLog").create(log)
pbClient.collection(DB_NAME).create(log)
)
const grpSumLog = {

View File

@ -4,6 +4,8 @@ import { AppInfoModel } from "../../constant/config"
import { managePbError } from "../../utils/pbTools"
import pbClient from "../pbClient"
const DB_NAME = "groupSummarySubscription"
export interface GroupSummarySubscription {
app: string
initiator: string
@ -23,7 +25,7 @@ export interface GrpSumSubWithApp extends GroupSummarySubscriptionModel {
const create = async (subscription: GroupSummarySubscription) =>
managePbError<GroupSummarySubscriptionModel>(() =>
pbClient.collection("groupSummarySubscription").create(subscription)
pbClient.collection(DB_NAME).create(subscription)
)
const update = async (
@ -31,12 +33,12 @@ const update = async (
subscription: Partial<GroupSummarySubscription>
) =>
managePbError<GroupSummarySubscriptionModel>(() =>
pbClient.collection("groupSummarySubscription").update(id, subscription)
pbClient.collection(DB_NAME).update(id, subscription)
)
const getAll = async (filter: string = "") =>
managePbError<GrpSumSubWithApp[]>(() =>
pbClient.collection("groupSummarySubscription").getFullList({
pbClient.collection(DB_NAME).getFullList({
filter,
expand: "app",
})
@ -44,9 +46,7 @@ const getAll = async (filter: string = "") =>
const getByFilter = async (filter: string) =>
managePbError<GrpSumSubWithApp>(() =>
pbClient
.collection("groupSummarySubscription")
.getFirstListItem(filter, { expand: "app" })
pbClient.collection(DB_NAME).getFirstListItem(filter, { expand: "app" })
)
const grpSumSub = {

View File

@ -1,4 +1,5 @@
import apiKey from "./apiKey"
import gitlabProject from "./gitlabProject/index."
import grpSumLog from "./grpSumLog"
import grpSumSub from "./grpSumSub"
import log from "./log"
@ -12,6 +13,7 @@ const db = {
user,
grpSumLog,
grpSumSub,
gitlabProject,
}
export default db

View File

@ -71,8 +71,7 @@ const getByCtx = async ({ larkBody, larkService }: Context) => {
password: email,
}
// 创建新用户
const finalUser = await create(newUser)
return finalUser
return await create(newUser)
}
// 用户对象

View File

@ -22,7 +22,7 @@
"@eslint/js": "^9.17.0",
"@types/node-schedule": "^2.1.7",
"@types/uuid": "^10.0.0",
"bun-types": "^1.1.38",
"bun-types": "^1.1.40",
"eslint": "^9.17.0",
"eslint-plugin-simple-import-sort": "^12.1.1",
"eslint-plugin-unused-imports": "^4.1.4",
@ -39,14 +39,14 @@
"@egg/hooks": "^1.2.0",
"@egg/lark-msg-tool": "^1.21.0",
"@egg/logger": "^1.6.0",
"@egg/net-tool": "^1.19.0",
"@egg/net-tool": "^1.21.0",
"@egg/path-tool": "^1.4.1",
"@langchain/core": "^0.3.24",
"@langchain/openai": "^0.3.14",
"@langchain/core": "^0.3.26",
"@langchain/openai": "^0.3.16",
"joi": "^17.13.3",
"langfuse-langchain": "^3.32.0",
"node-schedule": "^2.1.1",
"p-limit": "^6.1.0",
"p-limit": "^6.2.0",
"pocketbase": "^0.23.0",
"uuid": "^10.0.0"
}

View File

@ -1,4 +1,5 @@
import tempMap from "../../constant/template"
import gitlabEvent from "../../controller/gitlabEvent"
import groupAgent from "../../controller/groupAgent"
import createKVTemp from "../../controller/sheet/createKVTemp"
import { Context } from "../../types"
@ -137,6 +138,46 @@ const manageCMDMsg = async (ctx: Context) => {
return
}
// 开启CICD成功提醒
if (msgText.startsWith("/ci")) {
logger.info(`bot command is /ci, chatId: ${chatId}`)
await gitlabEvent.register.openCICDNotify(
ctx,
Number(msgText.replace("/ci ", ""))
)
return
}
// 关闭CICD成功提醒
if (msgText === "/ci off") {
logger.info(`bot command is /notify ci off, chatId: ${chatId}`)
await gitlabEvent.register.closeCICDNotify(
ctx,
Number(msgText.replace("/ci off ", ""))
)
return
}
// 开启MR自动总结
if (msgText === "/mr") {
logger.info(`bot command is /mr, chatId: ${chatId}`)
await gitlabEvent.register.openMRSummary(
ctx,
Number(msgText.replace("/mr ", ""))
)
return
}
// 关闭MR自动总结
if (msgText === "/mr off") {
logger.info(`bot command is /mr off, chatId: ${chatId}`)
await gitlabEvent.register.closeMRSummary(
ctx,
Number(msgText.replace("/mr off ", ""))
)
return
}
// 仅限群组功能
if (isInGroup) {
// 注册群组日报

View File

@ -1,5 +1,5 @@
import { LarkBody, LarkCard } from "@egg/lark-msg-tool"
import { LarkService, NetTool } from "@egg/net-tool"
import { GitlabService, LarkService, NetTool } from "@egg/net-tool"
import { PathCheckTool } from "@egg/path-tool"
import { Logger } from "winston"
@ -20,6 +20,7 @@ export interface Context {
larkBody: LarkBody
larkCard: LarkCard<typeof cardMap, typeof tempMap, typeof functionMap>
attachService: AttachService
gitlabService: GitlabService
path: PathCheckTool
searchParams: URLSearchParams
app: "michat" | "egg" | string

View File

@ -1,11 +1,11 @@
import { LarkBody, LarkCard } from "@egg/lark-msg-tool"
import loggerIns from "@egg/logger"
import { NetTool } from "@egg/net-tool"
import { GitlabService, NetTool } from "@egg/net-tool"
import { PathCheckTool } from "@egg/path-tool"
import { v4 as uuid } from "uuid"
import cardMap from "../constant/card"
import { APP_MAP } from "../constant/config"
import { APP_CONFIG, APP_MAP } from "../constant/config"
import functionMap from "../constant/function"
import tempMap from "../constant/template"
import { AttachService } from "../services"
@ -48,6 +48,11 @@ const genContext = async (req: Request) => {
const logger = loggerIns.child({ requestId })
const genResp = new NetTool({ requestId })
const larkService = genLarkService("egg", requestId)
const gitlabService = new GitlabService({
baseUrl: APP_CONFIG.GITLAB_BASE_URL,
authKey: APP_CONFIG.GITLAB_AUTH_KEY,
requestId,
})
const attachService = new AttachService({ requestId })
const path = new PathCheckTool(req.url)
const larkCard = new LarkCard(
@ -71,6 +76,7 @@ const genContext = async (req: Request) => {
larkBody,
larkCard,
attachService,
gitlabService,
searchParams,
app,
appInfo,