mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-11 18:29:06 +00:00
feat: --format json 模式下静默进度输出,保持 stdout 纯净
This commit is contained in:
parent
39d02d1a04
commit
5aea3ac1ed
5 changed files with 76 additions and 24 deletions
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/open-code-review/open-code-review/internal/config/template"
|
||||
"github.com/open-code-review/open-code-review/internal/config/toolsconfig"
|
||||
"github.com/open-code-review/open-code-review/internal/llm"
|
||||
"github.com/open-code-review/open-code-review/internal/stdout"
|
||||
"github.com/open-code-review/open-code-review/internal/telemetry"
|
||||
"github.com/open-code-review/open-code-review/internal/tool"
|
||||
)
|
||||
|
|
@ -107,6 +108,11 @@ func runReview(args []string) error {
|
|||
Model: model,
|
||||
})
|
||||
|
||||
// Silence progress output in JSON mode so stdout stays clean.
|
||||
if opts.outputFormat == "json" {
|
||||
defer stdout.Quiet()()
|
||||
}
|
||||
|
||||
ctx, span := telemetry.StartSpan(context.Background(), "review.run")
|
||||
defer span.End()
|
||||
startTime := time.Now()
|
||||
|
|
@ -126,6 +132,7 @@ func runReview(args []string) error {
|
|||
if len(comments) > 0 {
|
||||
telemetry.RecordCommentsGenerated(ctx, int64(len(comments)))
|
||||
}
|
||||
|
||||
telemetry.PrintTraceSummary(ag.FilesReviewed(), int64(len(comments)), ag.TotalInputTokens(), ag.TotalOutputTokens(), ag.TotalTokensUsed(), duration)
|
||||
|
||||
if opts.outputFormat == "json" {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/open-code-review/open-code-review/internal/llm"
|
||||
"github.com/open-code-review/open-code-review/internal/model"
|
||||
"github.com/open-code-review/open-code-review/internal/session"
|
||||
"github.com/open-code-review/open-code-review/internal/stdout"
|
||||
"github.com/open-code-review/open-code-review/internal/telemetry"
|
||||
"github.com/open-code-review/open-code-review/internal/tool"
|
||||
)
|
||||
|
|
@ -137,7 +138,7 @@ func (p *CommentWorkerPool) Submit(f func() ([]model.LlmComment, error)) {
|
|||
|
||||
comments, err := f()
|
||||
if err != nil {
|
||||
fmt.Printf("[ocr] CommentWorkerPool error: %v\n", err)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] CommentWorkerPool error: %v\n", err)
|
||||
}
|
||||
p.resultsMu.Lock()
|
||||
p.results = append(p.results, comments...)
|
||||
|
|
@ -185,7 +186,7 @@ func (a *Agent) Run(ctx context.Context) ([]model.LlmComment, error) {
|
|||
a.diffs = a.filterUnsupportedExts(a.diffs)
|
||||
|
||||
if len(a.diffs) == 0 {
|
||||
fmt.Println("[ocr] No supported files changed. Skipping review.")
|
||||
fmt.Fprintln(stdout.Writer(), "[ocr] No supported files changed. Skipping review.")
|
||||
telemetry.Event(ctx, "no.files.changed")
|
||||
a.session.Finalize()
|
||||
return nil, nil
|
||||
|
|
@ -193,7 +194,7 @@ func (a *Agent) Run(ctx context.Context) ([]model.LlmComment, error) {
|
|||
|
||||
a.currentDate = time.Now().Format("2006-01-02 15:04")
|
||||
|
||||
fmt.Printf("[ocr] Reviewing %d file(s) in %s\n", len(a.diffs), a.args.RepoDir)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Reviewing %d file(s) in %s\n", len(a.diffs), a.args.RepoDir)
|
||||
telemetry.Event(ctx, "review.started",
|
||||
telemetry.AnyToAttr("file.count", len(a.diffs)),
|
||||
telemetry.AnyToAttr("repo.dir", a.args.RepoDir))
|
||||
|
|
@ -323,7 +324,7 @@ func (a *Agent) dispatchSubtasks(ctx context.Context) ([]model.LlmComment, error
|
|||
}
|
||||
|
||||
if err := a.executeSubtask(fileCtx, d); err != nil {
|
||||
fmt.Printf("[ocr] Subtask error for %s: %v\n", d.NewPath, err)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Subtask error for %s: %v\n", d.NewPath, err)
|
||||
telemetry.ErrorEvent(fileCtx, "subtask.error", err,
|
||||
telemetry.AnyToAttr("file.path", d.NewPath))
|
||||
}
|
||||
|
|
@ -365,7 +366,7 @@ func (a *Agent) executeSubtask(ctx context.Context, d model.Diff) error {
|
|||
// Phase 1: Plan (skip when changes are below threshold)
|
||||
var planResult string
|
||||
if a.args.Template.PlanTask != nil && len(a.args.Template.PlanTask.Messages) > 0 && threshold > 0 && changeLines < int64(threshold) {
|
||||
fmt.Printf("[ocr] Skipping plan phase for %s (%d lines < threshold %d)\n", newPath, changeLines, threshold)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Skipping plan phase for %s (%d lines < threshold %d)\n", newPath, changeLines, threshold)
|
||||
telemetry.Event(ctx, "plan.skipped",
|
||||
telemetry.AnyToAttr("file.path", newPath),
|
||||
telemetry.AnyToAttr("lines.changed", changeLines),
|
||||
|
|
@ -374,7 +375,7 @@ func (a *Agent) executeSubtask(ctx context.Context, d model.Diff) error {
|
|||
var err error
|
||||
planResult, err = a.executePlanPhase(ctx, newPath, d.Diff, changeFilesExcludingCurrent, rule)
|
||||
if err != nil {
|
||||
fmt.Printf("[ocr] Plan phase failed for %s: %v (continuing without plan)\n", newPath, err)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Plan phase failed for %s: %v (continuing without plan)\n", newPath, err)
|
||||
telemetry.Eventf(ctx, "plan.failed", err.Error(),
|
||||
telemetry.AnyToAttr("file.path", newPath))
|
||||
planResult = ""
|
||||
|
|
@ -405,7 +406,7 @@ func (a *Agent) executeSubtask(ctx context.Context, d model.Diff) error {
|
|||
|
||||
tokenCount := countMessagesTokens(messages)
|
||||
if tokenCount > a.args.Template.TokenWarningThreshold {
|
||||
fmt.Printf("[ocr] WARNING: prompt tokens (%d) exceed threshold (%d) for %s\n",
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] WARNING: prompt tokens (%d) exceed threshold (%d) for %s\n",
|
||||
tokenCount, a.args.Template.TokenWarningThreshold, newPath)
|
||||
telemetry.Event(ctx, "token.threshold.exceeded",
|
||||
telemetry.AnyToAttr("file.path", newPath),
|
||||
|
|
@ -468,7 +469,7 @@ func (a *Agent) filterLargeDiffs(diffs []model.Diff) []model.Diff {
|
|||
for _, d := range diffs {
|
||||
tokens := llm.CountTokens(d.Diff)
|
||||
if float64(tokens) > limit {
|
||||
fmt.Printf("[ocr] Skipping %s (~%d tokens exceeds 80%% of threshold %d)\n",
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Skipping %s (~%d tokens exceeds 80%% of threshold %d)\n",
|
||||
d.NewPath, tokens, threshold)
|
||||
skipped++
|
||||
continue
|
||||
|
|
@ -477,7 +478,7 @@ func (a *Agent) filterLargeDiffs(diffs []model.Diff) []model.Diff {
|
|||
}
|
||||
|
||||
if skipped > 0 {
|
||||
fmt.Printf("[ocr] Pre-filtered %d file(s) exceeding 80%% token threshold\n", skipped)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Pre-filtered %d file(s) exceeding 80%% token threshold\n", skipped)
|
||||
}
|
||||
return kept
|
||||
}
|
||||
|
|
@ -494,7 +495,7 @@ func (a *Agent) filterUnsupportedExts(diffs []model.Diff) []model.Diff {
|
|||
}
|
||||
ext := a.extFromPath(path)
|
||||
if ext != "" && !allowedext.IsAllowedExt(ext) {
|
||||
fmt.Printf("[ocr] Skipping %s — extension %q not in supported file types\n", d.NewPath, ext)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Skipping %s — extension %q not in supported file types\n", d.NewPath, ext)
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
|
@ -502,7 +503,7 @@ func (a *Agent) filterUnsupportedExts(diffs []model.Diff) []model.Diff {
|
|||
}
|
||||
|
||||
if skipped > 0 {
|
||||
fmt.Printf("[ocr] Skipped %d file(s) with unsupported extensions\n", skipped)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Skipped %d file(s) with unsupported extensions\n", skipped)
|
||||
}
|
||||
return kept
|
||||
}
|
||||
|
|
@ -551,7 +552,7 @@ func (a *Agent) executePlanPhase(ctx context.Context, newPath, rawDiff, changeFi
|
|||
atomic.AddInt64(&a.totalInputTokens, int64(resp.Usage.PromptTokens+resp.Usage.CacheReadTokens))
|
||||
atomic.AddInt64(&a.totalOutputTokens, int64(resp.Usage.CompletionTokens+resp.Usage.CacheWriteTokens))
|
||||
}
|
||||
fmt.Printf("[ocr] Plan completed for %s\n", newPath)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Plan completed for %s\n", newPath)
|
||||
return resp.Content(), nil
|
||||
}
|
||||
|
||||
|
|
@ -639,7 +640,7 @@ func (a *Agent) performLlmCodeReview(ctx context.Context, messages []llm.Message
|
|||
|
||||
if len(calls) == 0 {
|
||||
// No tool calls - remind the model
|
||||
fmt.Printf("[ocr] No tool calls parsed for %s, retrying...\n", newPath)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] No tool calls parsed for %s, retrying...\n", newPath)
|
||||
messages = append(messages, llm.NewTextMessage("user", "You did not successfully call any tools. Please try again or use task_done if finished."))
|
||||
if content != "" {
|
||||
messages = append(messages[:len(messages)-1], llm.NewTextMessage("assistant", content), messages[len(messages)-1])
|
||||
|
|
@ -680,19 +681,19 @@ func (a *Agent) performLlmCodeReview(ctx context.Context, messages []llm.Message
|
|||
break
|
||||
}
|
||||
if !hasValidResult {
|
||||
fmt.Printf("[ocr] No valid tool results for %s, stopping.\n", newPath)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] No valid tool results for %s, stopping.\n", newPath)
|
||||
break
|
||||
}
|
||||
|
||||
succeed := a.addNextMessage(content, calls, results, &messages, newPath)
|
||||
if !succeed {
|
||||
fmt.Printf("[ocr] Context compression exceeded threshold for %s, stopping.\n", newPath)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Context compression exceeded threshold for %s, stopping.\n", newPath)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if toolReqCount <= 0 {
|
||||
fmt.Printf("[ocr] Max tool requests reached for %s.\n", newPath)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Max tool requests reached for %s.\n", newPath)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
@ -818,7 +819,7 @@ func BuildToolDefs(entries []toolsconfig.ToolConfigEntry, planOnly bool) []llm.T
|
|||
}
|
||||
var fn llm.FunctionDef
|
||||
if err := json.Unmarshal(defRaw, &fn); err != nil {
|
||||
fmt.Printf("[ocr] WARNING: failed to parse tool definition %q: %v\n", e.Name, err)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] WARNING: failed to parse tool definition %q: %v\n", e.Name, err)
|
||||
continue
|
||||
}
|
||||
defs = append(defs, llm.ToolDef{
|
||||
|
|
@ -850,7 +851,7 @@ func (a *Agent) compressAndRecord(msgs []llm.Message, filePath string) []llm.Mes
|
|||
rec := fs.AppendTaskRecord(session.MemoryCompressionTask, compressionMsgs)
|
||||
if err != nil {
|
||||
rec.SetError(err, duration)
|
||||
fmt.Printf("[ocr] Memory compression failed: %v\n", err)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Memory compression failed: %v\n", err)
|
||||
return msgs[:2]
|
||||
}
|
||||
rec.SetResponse(resp, duration)
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import (
|
|||
"time"
|
||||
|
||||
tiktoken "github.com/pkoukk/tiktoken-go"
|
||||
|
||||
"github.com/open-code-review/open-code-review/internal/stdout"
|
||||
)
|
||||
|
||||
const maxRetries = 10 // Maximum number of retry attempts with exponential backoff.
|
||||
|
|
@ -459,7 +461,7 @@ func sleepWithBackoff(attempt int) {
|
|||
jitter := time.Duration(rand.Int63n(int64(delay))) - delay/2
|
||||
delay += jitter
|
||||
|
||||
fmt.Printf("[llm] Retrying in %v (attempt info)... \n", delay)
|
||||
fmt.Fprintf(stdout.Writer(), "[llm] Retrying in %v (attempt info)... \n", delay)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
|
|
|
|||
40
internal/stdout/stdout.go
Normal file
40
internal/stdout/stdout.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package stdout
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
w io.Writer = os.Stdout
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
// Writer returns the current stdout writer (real stdout or discard).
|
||||
func Writer() io.Writer {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
return w
|
||||
}
|
||||
|
||||
// Quiet replaces stdout with io.Discard and returns a cleanup function.
|
||||
// Usage:
|
||||
//
|
||||
// defer stdout.Quiet()()
|
||||
//
|
||||
// WARNING: Quiet must ONLY be called from the main goroutine, before spawning
|
||||
// any concurrent work that writes to stdout, and its returned cleanup must be
|
||||
// deferred in the same goroutine. Never call Quiet from multiple goroutines
|
||||
// concurrently — it is not designed for nested or parallel silencing.
|
||||
func Quiet() func() {
|
||||
mu.Lock()
|
||||
old := w
|
||||
w = io.Discard
|
||||
mu.Unlock()
|
||||
return func() {
|
||||
mu.Lock()
|
||||
w = old
|
||||
mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
|
@ -12,6 +12,8 @@ import (
|
|||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
|
||||
"github.com/open-code-review/open-code-review/internal/stdout"
|
||||
)
|
||||
|
||||
// Event emits a structured event as a span with immediate end.
|
||||
|
|
@ -62,10 +64,10 @@ func FormatDuration(dur time.Duration) string {
|
|||
// PrintTraceSummary prints a one-line summary of the review to stdout.
|
||||
func PrintTraceSummary(filesReviewed, commentsGenerated int64, inputTokens, outputTokens, totalTokens int64, duration time.Duration) {
|
||||
if inputTokens > 0 || outputTokens > 0 {
|
||||
fmt.Printf("[ocr] Summary: %d file(s) reviewed, %d comment(s), ~%d token(s) used (input: ~%d, output: ~%d), %s elapsed\n",
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Summary: %d file(s) reviewed, %d comment(s), ~%d token(s) used (input: ~%d, output: ~%d), %s elapsed\n",
|
||||
filesReviewed, commentsGenerated, totalTokens, inputTokens, outputTokens, FormatDuration(duration))
|
||||
} else {
|
||||
fmt.Printf("[ocr] Summary: %d file(s) reviewed, %d comment(s), ~%d token(s) used, %s elapsed\n",
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] Summary: %d file(s) reviewed, %d comment(s), ~%d token(s) used, %s elapsed\n",
|
||||
filesReviewed, commentsGenerated, totalTokens, FormatDuration(duration))
|
||||
}
|
||||
}
|
||||
|
|
@ -76,16 +78,16 @@ func PrintTraceSummary(filesReviewed, commentsGenerated int64, inputTokens, outp
|
|||
func PrintToolCallStarted(toolName string, args map[string]any) {
|
||||
summary := summarizeArgs(args)
|
||||
if summary != "" {
|
||||
fmt.Printf("[ocr] ▶ %s %s\n", toolName, summary)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] ▶ %s %s\n", toolName, summary)
|
||||
} else {
|
||||
fmt.Printf("[ocr] ▶ %s\n", toolName)
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] ▶ %s\n", toolName)
|
||||
}
|
||||
}
|
||||
|
||||
// PrintToolCallFinished prints a line when a tool finishes successfully.
|
||||
// Example: [ocr] ✔ file_read "internal/config/rules/loader.go" (12ms)
|
||||
func PrintToolCallFinished(toolName string, dur time.Duration) {
|
||||
fmt.Printf("[ocr] ✔ %s (%s)\n", toolName, FormatDuration(dur))
|
||||
fmt.Fprintf(stdout.Writer(), "[ocr] ✔ %s (%s)\n", toolName, FormatDuration(dur))
|
||||
}
|
||||
|
||||
// PrintToolCallError prints a line when a tool fails.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue