change stdout

This commit is contained in:
Pinghua
2026-06-10 15:56:43 +08:00
parent 971ee4dfff
commit 8bb6f02253
+95 -30
View File
@@ -317,9 +317,11 @@ def build_prompt(meta, files, diff_path, agent_md, impact_report_path=None):
def run_claude(claude_bin, project_root, prompt, extra_dir=None): def run_claude(claude_bin, project_root, prompt, extra_dir=None):
"""Run claude -p, return stdout.""" """Run claude with stream-json output, return all text lines."""
cmd = [ cmd = [
claude_bin, "-p", claude_bin, "-p",
"--output-format", "stream-json",
"--include-partial-messages",
"--permission-mode", "acceptEdits", "--permission-mode", "acceptEdits",
"--verbose", "--verbose",
] ]
@@ -327,7 +329,7 @@ def run_claude(claude_bin, project_root, prompt, extra_dir=None):
cmd += ["--add-dir", extra_dir] cmd += ["--add-dir", extra_dir]
cmd.append(prompt) cmd.append(prompt)
print(f"[review_pr] Running: {' '.join(cmd[:4])} ...", file=sys.stderr) print(f"[review_pr] Running claude -p (stream-json) ...", file=sys.stderr)
proc = subprocess.Popen( proc = subprocess.Popen(
cmd, cmd,
cwd=project_root, cwd=project_root,
@@ -340,19 +342,45 @@ def run_claude(claude_bin, project_root, prompt, extra_dir=None):
def _stream(pipe, label, dest_lines): def _stream(pipe, label, dest_lines):
for line in pipe: for line in pipe:
line = line.rstrip("\n") line = line.rstrip("\n")
if line: if not line:
print(f"[{label}] {line}", file=sys.stderr) continue
dest_lines.append(line) dest_lines.append(line)
# Pretty-print stream-json events for progress visibility
stderr_lines = [] try:
t = threading.Thread(target=_stream, args=(proc.stderr, "claude:stderr", stderr_lines)) msg = json.loads(line)
t.start() t = msg.get("type", "")
if t == "assistant":
text = ""
for block in msg.get("message", {}).get("content", []):
if block.get("type") == "text":
text = block.get("text", "")
if text:
print(f"[claude] {text}", file=sys.stderr)
elif t == "tool_use":
name = msg.get("message", {}).get("name", "?")
inp = msg.get("message", {}).get("input", {})
print(f"[claude] tool: {name}({json.dumps(inp, ensure_ascii=False)})", file=sys.stderr)
elif t == "tool_result":
print(f"[claude] tool: result", file=sys.stderr)
elif t == "system":
subtype = msg.get("subtype", "")
if subtype == "init":
print(f"[claude] system: init", file=sys.stderr)
# skip heartbeat/user/other events to avoid noise
except (json.JSONDecodeError, KeyError, TypeError):
print(f"[claude] {line}", file=sys.stderr)
stdout_lines = [] stdout_lines = []
_stream(proc.stdout, "claude", stdout_lines) stderr_lines = []
t.join(timeout=10) t_out = threading.Thread(target=_stream, args=(proc.stdout, "claude", stdout_lines))
proc.wait(timeout=600) t_err = threading.Thread(target=_stream, args=(proc.stderr, "claude:stderr", stderr_lines))
t_out.start()
t_err.start()
t_out.join(timeout=600)
t_err.join(timeout=10)
proc.wait(timeout=10)
if proc.returncode != 0: if proc.returncode != 0:
print(f"Claude exited with code {proc.returncode}", file=sys.stderr) print(f"Claude exited with code {proc.returncode}", file=sys.stderr)
@@ -363,30 +391,67 @@ def run_claude(claude_bin, project_root, prompt, extra_dir=None):
def extract_json_from_output(stdout): def extract_json_from_output(stdout):
"""Extract the review JSON from Claude's output.""" """Extract the review JSON from Claude's output.
match = re.search(r"```json\s*([\s\S]*)\s*```", stdout)
if not match:
print("ERROR: No ```json block found in Claude output.", file=sys.stderr)
print(stdout[:3000], file=sys.stderr)
sys.exit(1)
Handles both stream-json lines and plain-text ```json blocks.
"""
# ── Try stream-json: collect assistant text ──
assistant_text = _extract_stream_json_text(stdout)
if assistant_text:
obj = _find_review_json(assistant_text)
if obj:
return obj
print("[review_pr] stream-json text gathered but no review JSON found; trying raw ...",
file=sys.stderr)
# ── Fall back to plain-text ```json block ──
obj = _find_review_json(stdout)
if obj:
return obj
print("ERROR: No valid review JSON found in Claude output.", file=sys.stderr)
print(stdout[-3000:], file=sys.stderr)
sys.exit(1)
def _extract_stream_json_text(stdout):
"""Parse stdout as stream-json lines and return concatenated assistant text."""
texts = []
for line in stdout.splitlines():
line = line.strip()
if not line or not line.startswith("{"):
continue
try:
msg = json.loads(line)
except json.JSONDecodeError:
continue
if msg.get("type") != "assistant":
continue
for block in msg.get("message", {}).get("content", []):
if block.get("type") == "text":
texts.append(block.get("text", ""))
return "\n".join(texts) if texts else ""
def _find_review_json(text):
"""Find and parse a review JSON object in *text*.
Returns the parsed dict, or None.
"""
match = re.search(r"```json\s*([\s\S]*)\s*```", text)
if not match:
return None
try: try:
obj = json.loads(match.group(1)) obj = json.loads(match.group(1))
except json.JSONDecodeError as e: except json.JSONDecodeError:
print(f"ERROR: Invalid JSON in Claude output: {e}", file=sys.stderr) return None
print(match.group(1)[:2000], file=sys.stderr) if not isinstance(obj, dict):
sys.exit(1) return None
# Unwrap {review: {...}} envelope if present # Unwrap {review: {...}} envelope if present
if isinstance(obj, dict) and "review" in obj and isinstance(obj["review"], dict): if "review" in obj and isinstance(obj["review"], dict):
obj = obj["review"] obj = obj["review"]
if "body" not in obj:
if not isinstance(obj, dict) or "body" not in obj: return None
print(f"ERROR: JSON missing required 'body' field. "
f"Keys: {list(obj.keys()) if isinstance(obj, dict) else type(obj).__name__}",
file=sys.stderr)
sys.exit(1)
return obj return obj