import { CommitDiffSchema } from "@gitbeaker/rest" import pLimit from "p-limit" import { Logger } from "winston" import chatTools from "../../utils/chatTools" import tokenTools from "../../utils/tokenTools" import Commenter from "./utils/commenter" import diffTools, { Review } from "./utils/diffTools" import { Inputs } from "./utils/inputs" import { Prompts } from "./utils/prompts" /** * 审查需要审查的文件列表,并生成评论。 * @param {CommitDiffSchema[]} needReviewFiles - 需要审查的文件列表。 * @param {Inputs} rawInput - 原始输入。 * @param {Commenter} commenter - 评论工具实例。 * @param {Logger} logger - 日志记录器。 * @returns {Promise} 返回一个Promise,表示审查过程的完成。 */ const reviewFiles = async ( needReviewFiles: CommitDiffSchema[], rawInput: Inputs, commenter: Commenter, logger: Logger ) => { const prompts = new Prompts() // 创建一个Map来存储文件的差异信息 const diffsFileMap = new Map() // 解析每个文件的差异并存储在diffsFileMap中 needReviewFiles.forEach((file) => { const diffs = diffTools.parseFileDiffs(file) const fileDiff = diffsFileMap.get(file.new_path) || [] diffsFileMap.set(file.new_path, fileDiff.concat(diffs)) }) logger.debug(`diffsFileMap: ${JSON.stringify([...diffsFileMap])}`) // 如果没有需要审查的文件,记录日志并创建评论 if (diffsFileMap.size === 0) { logger.info("No files need review") commenter.createComment("没有需要审查的文件") return } const skippedFiles: string[] = [] const fileCRResultsMap = new Map() /** * 审查单个文件的差异。 * @param {Array} diff - 文件的差异数组。 * @param {string} filename - 文件名。 * @returns {Promise} 返回一个Promise,表示审查过程的完成。 */ const doFileReview = async ( diff: [number, number, string][], filename: string ) => { logger.info(`Reviewing ${filename}`) const inputs = rawInput.clone() inputs.filename = filename let tokens = tokenTools.getTokenCount(prompts.renderReviewFileDiff(inputs)) // 计算并打包文件的差异,确保令牌数不超过限制 const packedFileCount = diff.findIndex((d, idx) => { const hunk = d[2] if (tokens + tokenTools.getTokenCount(hunk) > 100 * 1000) { logger.info( `only packing ${idx} / ${diff.length} diffs, tokens: ${tokens} / ${100 * 1000}` ) return true } inputs.patches += ` ${hunk} ` tokens += tokenTools.getTokenCount(hunk) return false }) // 如果没有打包任何差异,跳过该文件 if (packedFileCount === 0) { logger.info(`skipping ${filename}`) skippedFiles.push(filename) return } // 获取GPT-4模型并生成审查提示 const codeChatBot = await chatTools.getGpt4oModel(0) const reviewPrompt = prompts.renderReviewFileDiff(inputs) logger.debug(`reviewPrompt for ${filename}: ${reviewPrompt}`) try { const { content: review } = await codeChatBot.invoke(reviewPrompt) if (!review) throw new Error("Empty review") logger.info(`review for ${filename}: ${review}`) // 解析审查结果并过滤需要评论的审查 const reviews = diffTools.parseReview(review as string, diff) logger.debug(`reviews for ${filename}: ${JSON.stringify(reviews)}`) const needCommentReviews = reviews.filter( (r) => !r.comment.includes("LGTM") ) fileCRResultsMap.set(filename, needCommentReviews) } catch { logger.error(`Failed to review ${filename}`) } } /** * 创建评论。 * @param {string} filename - 文件名。 * @param {Review} reviews - 审查结果。 * @returns {Promise} 返回一个Promise,表示评论过程的完成。 */ const doComment = async (filename: string, reviews: Review) => { const file = needReviewFiles.find((f) => f.new_path === filename) if (!file) { logger.error(`Failed to find file ${filename}`) return } await commenter.createReviewComment( file.new_path, file.old_path, reviews.startLine, reviews.endLine, reviews.comment ) } // 使用pLimit限制并发审查文件的数量 const limit = pLimit(10) await Promise.allSettled( [...diffsFileMap].map(([filename, diffs]) => limit(() => doFileReview(diffs, filename)) ) ) // 把fileCRResultsMap中的needCommentReviews拍平成一个数组 const needCommentReviews: { filename: string; reviews: Review }[] = [] fileCRResultsMap.forEach((reviews, filename) => { reviews.forEach((r) => needCommentReviews.push({ filename, reviews: r })) }) // 创建评论 await Promise.allSettled( needCommentReviews.map(({ filename, reviews }) => limit(() => doComment(filename, reviews)) ) ) } export default reviewFiles