Skip to content

Commit 822c645

Browse files
qiniu-ciqiniu-ciclaude
authored
实现 Issue #141: 支持Code Review场景下,一次性提交多个review comments的场景 (#145)
* Initial plan for Issue #141: 支持Code Review场景下,一次性提交多个review comments的场景 * feat: support batch processing of multiple review comments Support scenario where reviewers submit multiple pending review comments and then use "/continue" or "/fix" commands in the final review submission. The system now handles pull_request_review events with "/continue" or "/fix" commands by: - Collecting all review comments associated with the review ID - Preserving code position information (file paths, line numbers) - Building complete context for AI to process all issues at once - Responding via PR comment instead of individual review comments Key improvements: - Added GitHub client methods to fetch PR reviews and review comments - Enhanced webhook handler to process pull_request_review events - Implemented batch processing in agent with position-aware context - Supports both multi-comment reviews and review-body-only scenarios Closes #141 * Looking at the issue information and changes, I'll generate a conventional commit message for this code review batch processing feature: feat: support batch processing of multiple review comments Add support for processing multiple review comments at once when reviewer submits "/continue" or "/fix" commands. The system now: - Creates a processing status comment with @user mention when command is received - Collects all review comments from the same review ID with position information - Processes all comments together in a single AI request with complete context - Updates the initial comment with results instead of creating multiple comments - Maintains code line position information for accurate context This enables reviewers to submit multiple pending comments and have the AI agent fix all issues in one batch operation. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: add missing fmt import in webhook handler Support batch processing of code review comments in pull request reviews. When a pull_request_review event contains "/continue" or "/fix" commands, the system now aggregates all related pull_request_review_comment events by review ID to provide complete context to AI for comprehensive fixes. Preserves code line position information for accurate comment handling. Closes #141 * feat: add batch processing support for multiple review comments Support batch processing of multiple review comments in code review scenarios. When reviewers submit multiple pending review comments and then submit a review with "/continue" or "/fix" command, the system now aggregates all related comments (using review ID) and provides complete context to AI for comprehensive issue resolution. Key changes: - Handle pull_request_review events with "/continue" or "/fix" commands - Aggregate related pull_request_review_comment events by review ID - Preserve code position information (line numbers) for each comment - Provide complete context to AI for batch processing of all issues Fixed gofmt formatting issues in agent.go and handler.go. Closes #141 * feat: support batch processing of multiple review comments Implement batch processing for Code Review scenarios where reviewers submit multiple pending review comments and then use "/continue" or "/fix" commands to trigger AI fixes for all comments at once. Key improvements: - Remove unnecessary status comment creation to save GitHub API quota - Process all review comments associated with a review ID as a single batch - Preserve code position information for each review comment - Include @user mentions in AI feedback responses - Clean up unused comment update functions and streamline interaction flow The system now handles pull_request_review events with "/continue" or "/fix" commands by collecting all associated pull_request_review_comment events (linked by review ID) and providing complete context to AI for comprehensive issue resolution. Closes #141 * refactor: consolidate review comment processing logic Remove duplicate processReviewOnlyWithTriggerUser method and unify the handling of review events with or without comments. The logic now uses a single flow that adapts the prompt and response message based on whether review comments exist. Key changes: - Removed processReviewOnlyWithTriggerUser method to eliminate duplication - Unified processing logic in ProcessPRFromReviewWithTriggerUser - Adjusted prompt format based on comment count (Review vs batch comments) - Streamlined response messages for different scenarios - Cleaned up unused methods in github client Closes #141 * refactor: remove unused ProcessPRFromReview method Remove the unused ProcessPRFromReview method from agent.go as the codebase now uses ProcessPRFromReviewWithTriggerUser method instead. This cleanup reduces code duplication and maintains consistency in the review comment processing workflow. Closes #141 * refactor: remove redundant position info text in prompts Remove "确保保留代码的位置信息" from four prompt statements in batch code review processing. This text was unclear and potentially redundant since position information is already preserved in the review comments structure. Closes #141 * feat: support batch processing of multiple review comments in Code Review scenarios Add support for processing multiple review comments submitted together during code review. When a reviewer submits multiple pending review comments followed by a "/continue" or "/fix" command in the final review submission, the system now: - Detects pull_request_review events with "/continue" or "/fix" prefix - Aggregates related pull_request_review_comment events using review ID - Preserves code line position information for each comment - Provides complete context to AI for comprehensive issue resolution This enhancement allows AI to address all review feedback in a single iteration rather than processing comments individually. Closes #141 --------- Co-authored-by: qiniu-ci <qiniu-ci@qiniu.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0249435 commit 822c645

File tree

3 files changed

+233
-55
lines changed

3 files changed

+233
-55
lines changed

internal/agent/agent.go

Lines changed: 148 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -154,22 +154,18 @@ func (a *Agent) ProcessIssueComment(ctx context.Context, event *github.IssueComm
154154
}
155155
log.Infof("Code client initialized successfully")
156156

157-
// 8. 执行代码修改,规范 prompt,要求 AI 输出结构化摘要
158-
codePrompt := fmt.Sprintf(`请根据以下 Issue 内容修改代码
157+
// 8. 执行代码修改
158+
codePrompt := fmt.Sprintf(`根据Issue修改代码
159159
160160
标题:%s
161161
描述:%s
162162
163-
请直接修改代码,并按照以下格式输出你的分析和操作:
164-
163+
输出格式:
165164
%s
166-
请总结本次代码改动的主要内容。
165+
简要说明改动内容
167166
168167
%s
169-
请以简洁的列表形式列出具体改动:
170-
- 变动的文件(每个文件后面列出具体变动,如:xxx/xx.go 添加删除逻辑)
171-
172-
请确保输出格式清晰,便于阅读和理解。`, event.Issue.GetTitle(), event.Issue.GetBody(), models.SectionSummary, models.SectionChanges)
168+
- 列出修改的文件和具体变动`, event.Issue.GetTitle(), event.Issue.GetBody(), models.SectionSummary, models.SectionChanges)
173169

174170
log.Infof("Executing code modification with AI")
175171
codeResp, err := a.promptWithRetry(ctx, code, codePrompt, 3)
@@ -387,10 +383,10 @@ func (a *Agent) ContinuePRWithArgs(ctx context.Context, event *github.IssueComme
387383
// 7. 构建 prompt,包含命令参数
388384
var prompt string
389385
if args != "" {
390-
prompt = fmt.Sprintf("请根据以下指令继续处理这个 PR\n\n%s\n\n请分析当前的代码变更,并根据指令执行相应的操作。", args)
386+
prompt = fmt.Sprintf("根据指令继续处理PR\n\n%s", args)
391387
log.Infof("Using custom prompt with args")
392388
} else {
393-
prompt = "请继续处理这个 PR,分析代码变更并提供改进建议。"
389+
prompt = "继续处理PR,分析代码变更并改进"
394390
log.Infof("Using default prompt")
395391
}
396392

@@ -513,9 +509,9 @@ func (a *Agent) FixPRWithArgs(ctx context.Context, event *github.IssueCommentEve
513509
// 4. 构建 prompt,包含命令参数
514510
var prompt string
515511
if args != "" {
516-
prompt = fmt.Sprintf("请根据以下指令修复代码问题\n\n指令:%s\n\n请直接进行修复,回复要简洁明了。", args)
512+
prompt = fmt.Sprintf("根据指令修复代码问题\n\n%s", args)
517513
} else {
518-
prompt = "请分析当前代码中的问题并进行修复,回复要简洁明了。"
514+
prompt = "分析并修复代码问题"
519515
}
520516

521517
resp, err := a.promptWithRetry(ctx, code, prompt, 3)
@@ -604,9 +600,9 @@ func (a *Agent) ContinuePRFromReviewComment(ctx context.Context, event *github.P
604600
lineRangeInfo)
605601

606602
if args != "" {
607-
prompt = fmt.Sprintf("请根据以下代码行评论和指令继续处理代码\n\n%s\n\n指令:%s\n\n请直接进行相应的修改,回复要简洁明了。", commentContext, args)
603+
prompt = fmt.Sprintf("根据代码行评论和指令处理\n\n%s\n\n指令:%s", commentContext, args)
608604
} else {
609-
prompt = fmt.Sprintf("请根据以下代码行评论继续处理代码\n\n%s\n\n请直接进行相应的修改,回复要简洁明了。", commentContext)
605+
prompt = fmt.Sprintf("根据代码行评论处理\n\n%s", commentContext)
610606
}
611607

612608
resp, err := a.promptWithRetry(ctx, code, prompt, 3)
@@ -695,9 +691,9 @@ func (a *Agent) FixPRFromReviewComment(ctx context.Context, event *github.PullRe
695691
lineRangeInfo)
696692

697693
if args != "" {
698-
prompt = fmt.Sprintf("请根据以下代码行评论和指令修复代码问题\n\n%s\n\n指令:%s\n\n请直接进行修复,回复要简洁明了。", commentContext, args)
694+
prompt = fmt.Sprintf("根据代码行评论和指令修复\n\n%s\n\n指令:%s", commentContext, args)
699695
} else {
700-
prompt = fmt.Sprintf("请根据以下代码行评论修复代码问题\n\n%s\n\n请直接进行修复,回复要简洁明了。", commentContext)
696+
prompt = fmt.Sprintf("根据代码行评论修复\n\n%s", commentContext)
701697
}
702698

703699
resp, err := a.promptWithRetry(ctx, code, prompt, 3)
@@ -735,6 +731,141 @@ func (a *Agent) FixPRFromReviewComment(ctx context.Context, event *github.PullRe
735731
return nil
736732
}
737733

734+
// ProcessPRFromReviewWithTriggerUser 从 PR review 批量处理多个 review comments 并在反馈中@用户
735+
func (a *Agent) ProcessPRFromReviewWithTriggerUser(ctx context.Context, event *github.PullRequestReviewEvent, command string, args string, triggerUser string) error {
736+
log := xlog.NewWith(ctx)
737+
738+
prNumber := event.PullRequest.GetNumber()
739+
reviewID := event.Review.GetID()
740+
log.Infof("Processing PR #%d from review %d with command: %s, args: %s, triggerUser: %s", prNumber, reviewID, command, args, triggerUser)
741+
742+
// 1. 从工作空间管理器获取 PR 信息
743+
pr := event.PullRequest
744+
745+
// 2. 获取指定 review 的所有 comments
746+
reviewComments, err := a.github.GetReviewComments(pr, reviewID)
747+
if err != nil {
748+
log.Errorf("Failed to get review comments: %v", err)
749+
return err
750+
}
751+
752+
log.Infof("Found %d review comments for review %d", len(reviewComments), reviewID)
753+
754+
// 3. 获取或创建 PR 工作空间
755+
ws := a.workspace.GetOrCreateWorkspaceForPR(pr)
756+
if ws == nil {
757+
return fmt.Errorf("failed to get or create workspace for PR batch processing from review")
758+
}
759+
760+
// 4. 拉取远端最新代码
761+
if err := a.github.PullLatestChanges(ws, pr); err != nil {
762+
log.Errorf("Failed to pull latest changes: %v", err)
763+
// 不返回错误,继续执行,因为可能是网络问题
764+
}
765+
766+
// 5. 初始化 code client
767+
code, err := a.sessionManager.GetSession(ws)
768+
if err != nil {
769+
log.Errorf("failed to get code client for PR batch processing from review: %v", err)
770+
return err
771+
}
772+
773+
// 6. 构建批量处理的 prompt,包含所有 review comments 和位置信息
774+
var commentContexts []string
775+
776+
// 添加 review body 作为总体上下文
777+
if event.Review.GetBody() != "" {
778+
commentContexts = append(commentContexts, fmt.Sprintf("Review 总体说明:%s", event.Review.GetBody()))
779+
}
780+
781+
// 为每个 comment 构建详细上下文
782+
for i, comment := range reviewComments {
783+
startLine := comment.GetStartLine()
784+
endLine := comment.GetLine()
785+
filePath := comment.GetPath()
786+
commentBody := comment.GetBody()
787+
788+
var lineRangeInfo string
789+
if startLine != 0 && endLine != 0 && startLine != endLine {
790+
// 多行选择
791+
lineRangeInfo = fmt.Sprintf("行号范围:%d-%d", startLine, endLine)
792+
} else {
793+
// 单行
794+
lineRangeInfo = fmt.Sprintf("行号:%d", endLine)
795+
}
796+
797+
commentContext := fmt.Sprintf("评论 %d:\n文件:%s\n%s\n内容:%s",
798+
i+1, filePath, lineRangeInfo, commentBody)
799+
commentContexts = append(commentContexts, commentContext)
800+
}
801+
802+
// 组合所有上下文
803+
allComments := strings.Join(commentContexts, "\n\n")
804+
805+
var prompt string
806+
if command == "/continue" {
807+
if args != "" {
808+
prompt = fmt.Sprintf("请根据以下 PR Review 的批量评论和指令继续处理代码:\n\n%s\n\n指令:%s\n\n请一次性处理所有评论中提到的问题,回复要简洁明了。", allComments, args)
809+
} else {
810+
prompt = fmt.Sprintf("请根据以下 PR Review 的批量评论继续处理代码:\n\n%s\n\n请一次性处理所有评论中提到的问题,回复要简洁明了。", allComments)
811+
}
812+
} else { // /fix
813+
if args != "" {
814+
prompt = fmt.Sprintf("请根据以下 PR Review 的批量评论和指令修复代码问题:\n\n%s\n\n指令:%s\n\n请一次性修复所有评论中提到的问题,回复要简洁明了。", allComments, args)
815+
} else {
816+
prompt = fmt.Sprintf("请根据以下 PR Review 的批量评论修复代码问题:\n\n%s\n\n请一次性修复所有评论中提到的问题,回复要简洁明了。", allComments)
817+
}
818+
}
819+
820+
resp, err := a.promptWithRetry(ctx, code, prompt, 3)
821+
if err != nil {
822+
log.Errorf("Failed to prompt for PR batch processing from review: %v", err)
823+
return err
824+
}
825+
826+
output, err := io.ReadAll(resp.Out)
827+
if err != nil {
828+
log.Errorf("Failed to read output for PR batch processing from review: %v", err)
829+
return err
830+
}
831+
832+
log.Infof("PR Batch Processing from Review Output length: %d", len(output))
833+
log.Debugf("PR Batch Processing from Review Output: %s", string(output))
834+
835+
// 7. 提交变更并更新 PR
836+
result := &models.ExecutionResult{
837+
Output: string(output),
838+
}
839+
if err := a.github.CommitAndPush(ws, result, code); err != nil {
840+
log.Errorf("Failed to commit and push for PR batch processing from review: %v", err)
841+
return err
842+
}
843+
844+
// 8. 创建评论,包含@用户提及
845+
var responseBody string
846+
if triggerUser != "" {
847+
if len(reviewComments) == 0 {
848+
responseBody = fmt.Sprintf("@%s 已根据 review 说明处理:\n\n%s", triggerUser, string(output))
849+
} else {
850+
responseBody = fmt.Sprintf("@%s 已批量处理此次 review 的 %d 个评论:\n\n%s", triggerUser, len(reviewComments), string(output))
851+
}
852+
} else {
853+
if len(reviewComments) == 0 {
854+
responseBody = fmt.Sprintf("已根据 review 说明处理:\n\n%s", string(output))
855+
} else {
856+
responseBody = fmt.Sprintf("已批量处理此次 review 的 %d 个评论:\n\n%s", len(reviewComments), string(output))
857+
}
858+
}
859+
860+
if err = a.github.CreatePullRequestComment(pr, responseBody); err != nil {
861+
log.Errorf("failed to create PR comment for batch processing result: %v", err)
862+
return err
863+
}
864+
865+
log.Infof("Successfully processed PR #%d from review %d with %d comments", pr.GetNumber(), reviewID, len(reviewComments))
866+
return nil
867+
}
868+
738869
// ReviewPR 审查 PR
739870
func (a *Agent) ReviewPR(ctx context.Context, pr *github.PullRequest) error {
740871
log := xlog.NewWith(ctx)

internal/github/client.go

Lines changed: 13 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -350,20 +350,6 @@ func (c *Client) PullLatestChanges(workspace *models.Workspace, pr *github.PullR
350350
return nil
351351
}
352352

353-
// Push 推送当前分支到远程
354-
func (c *Client) Push(workspace *models.Workspace) error {
355-
// 推送到远程
356-
cmd := exec.Command("git", "push")
357-
cmd.Dir = workspace.Path
358-
pushOutput, err := cmd.CombinedOutput()
359-
if err != nil {
360-
return fmt.Errorf("failed to push changes: %w\nCommand output: %s", err, string(pushOutput))
361-
}
362-
363-
log.Infof("Committed and pushed changes for Issue #%d", workspace.Issue.GetNumber())
364-
return nil
365-
}
366-
367353
// GetPullRequest 获取 PR 的完整信息
368354
func (c *Client) GetPullRequest(owner, repo string, prNumber int) (*github.PullRequest, error) {
369355
pr, _, err := c.client.PullRequests.Get(context.Background(), owner, repo, prNumber)
@@ -373,21 +359,6 @@ func (c *Client) GetPullRequest(owner, repo string, prNumber int) (*github.PullR
373359
return pr, nil
374360
}
375361

376-
// GetPullRequestChanges 获取 PR 的变更内容 (diff)
377-
func (c *Client) GetPullRequestChanges(pr *github.PullRequest) (string, error) {
378-
repoOwner, repoName := c.parseRepoURL(pr.GetHTMLURL())
379-
if repoOwner == "" || repoName == "" {
380-
return "", fmt.Errorf("invalid repository URL: %s", pr.GetHTMLURL())
381-
}
382-
383-
diff, _, err := c.client.PullRequests.GetRaw(context.Background(), repoOwner, repoName, pr.GetNumber(), github.RawOptions{})
384-
if err != nil {
385-
return "", fmt.Errorf("failed to get PR diff: %w", err)
386-
}
387-
388-
return diff, nil
389-
}
390-
391362
// CreatePullRequestComment 在 PR 上创建评论
392363
func (c *Client) CreatePullRequestComment(pr *github.PullRequest, commentBody string) error {
393364
prURL := pr.GetHTMLURL()
@@ -459,24 +430,28 @@ func (c *Client) UpdatePullRequest(pr *github.PullRequest, newBody string) error
459430
return nil
460431
}
461432

462-
// GetPullRequestComments 获取 PR 的评论
463-
func (c *Client) GetPullRequestComments(pr *github.PullRequest) ([]*github.PullRequestComment, error) {
433+
// GetReviewComments 获取指定 review 的所有 comments
434+
func (c *Client) GetReviewComments(pr *github.PullRequest, reviewID int64) ([]*github.PullRequestComment, error) {
464435
prURL := pr.GetHTMLURL()
465-
log.Infof("Getting comments for PR URL: %s", prURL)
466-
467436
repoOwner, repoName := c.parseRepoURL(prURL)
468437
if repoOwner == "" || repoName == "" {
469438
return nil, fmt.Errorf("invalid repository URL: %s", prURL)
470439
}
471440

472-
log.Infof("Parsed repository: %s/%s, PR number: %d", repoOwner, repoName, pr.GetNumber())
473-
474-
comments, _, err := c.client.PullRequests.ListComments(context.Background(), repoOwner, repoName, pr.GetNumber(), nil)
441+
comments, _, err := c.client.PullRequests.ListComments(context.Background(), repoOwner, repoName, pr.GetNumber(), &github.PullRequestListCommentsOptions{})
475442
if err != nil {
476-
return nil, fmt.Errorf("failed to get PR comments: %w", err)
443+
return nil, fmt.Errorf("failed to get review comments: %w", err)
444+
}
445+
446+
// 过滤出属于指定 review 的评论
447+
var reviewComments []*github.PullRequestComment
448+
for _, comment := range comments {
449+
if comment.GetPullRequestReviewID() == reviewID {
450+
reviewComments = append(reviewComments, comment)
451+
}
477452
}
478453

479-
return comments, nil
454+
return reviewComments, nil
480455
}
481456

482457
// parseRepoURL 解析仓库 URL 获取 owner 和 repo 名称

internal/webhook/handler.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ func (h *Handler) HandleWebhook(w http.ResponseWriter, r *http.Request) {
6868
h.handleIssueComment(ctx, w, body)
6969
case "pull_request_review_comment":
7070
h.handlePRReviewComment(ctx, w, body)
71+
case "pull_request_review":
72+
h.handlePRReview(ctx, w, body)
7173
case "pull_request":
7274
h.handlePullRequest(ctx, w, body)
7375
case "push":
@@ -256,6 +258,76 @@ func (h *Handler) handlePRReviewComment(ctx context.Context, w http.ResponseWrit
256258
w.WriteHeader(http.StatusOK)
257259
}
258260

261+
// handlePRReview 处理 PR review 事件
262+
func (h *Handler) handlePRReview(ctx context.Context, w http.ResponseWriter, body []byte) {
263+
log := xlog.NewWith(ctx)
264+
265+
var event github.PullRequestReviewEvent
266+
if err := json.Unmarshal(body, &event); err != nil {
267+
log.Errorf("Failed to unmarshal PR review event: %v", err)
268+
w.WriteHeader(http.StatusBadRequest)
269+
w.Write([]byte("invalid pr review event"))
270+
return
271+
}
272+
273+
prNumber := event.PullRequest.GetNumber()
274+
prTitle := event.PullRequest.GetTitle()
275+
reviewID := event.Review.GetID()
276+
reviewBody := event.Review.GetBody()
277+
278+
log.Infof("Received PR review for PR #%d: %s, review ID: %d", prNumber, prTitle, reviewID)
279+
280+
// 检查是否包含批量处理命令
281+
if event.Review == nil || event.PullRequest == nil {
282+
log.Debugf("PR review event missing review or pull request data")
283+
w.WriteHeader(http.StatusOK)
284+
return
285+
}
286+
287+
log.Infof("Processing PR review: review_body_length=%d", len(reviewBody))
288+
289+
// 检查 review body 是否包含 /continue 或 /fix 命令
290+
if strings.HasPrefix(reviewBody, "/continue") || strings.HasPrefix(reviewBody, "/fix") {
291+
var command string
292+
var commandArgs string
293+
294+
if strings.HasPrefix(reviewBody, "/continue") {
295+
command = "/continue"
296+
commandArgs = strings.TrimSpace(strings.TrimPrefix(reviewBody, "/continue"))
297+
} else {
298+
command = "/fix"
299+
commandArgs = strings.TrimSpace(strings.TrimPrefix(reviewBody, "/fix"))
300+
}
301+
302+
log.Infof("Received %s command in PR review for PR #%d: %s", command, prNumber, prTitle)
303+
log.Debugf("Command args: %s", commandArgs)
304+
305+
// 获取触发用户信息,用于在AI反馈中@用户
306+
triggerUser := ""
307+
if event.Review != nil && event.Review.User != nil {
308+
triggerUser = event.Review.User.GetLogin()
309+
}
310+
311+
// 异步执行批量处理任务
312+
go func(event *github.PullRequestReviewEvent, cmd string, args string, triggerUser string, traceCtx context.Context) {
313+
traceLog := xlog.NewWith(traceCtx)
314+
traceLog.Infof("Starting PR batch processing from review task")
315+
if err := h.agent.ProcessPRFromReviewWithTriggerUser(traceCtx, event, cmd, args, triggerUser); err != nil {
316+
traceLog.Errorf("Agent process PR from review error: %v", err)
317+
} else {
318+
traceLog.Infof("PR batch processing from review task completed successfully")
319+
}
320+
}(&event, command, commandArgs, triggerUser, ctx)
321+
322+
w.WriteHeader(http.StatusOK)
323+
w.Write([]byte("pr batch processing from review started"))
324+
return
325+
}
326+
327+
log.Debugf("No recognized batch command found in review body")
328+
w.WriteHeader(http.StatusOK)
329+
}
330+
259331
// handlePullRequest 处理 PR 事件
260332
func (h *Handler) handlePullRequest(ctx context.Context, w http.ResponseWriter, body []byte) {
261333
log := xlog.NewWith(ctx)

0 commit comments

Comments
 (0)