From 5608b0fdd0de79ccd1a80608864134edf183380b Mon Sep 17 00:00:00 2001 From: baizhi958216 <1475289190@qq.com> Date: Thu, 11 Jun 2026 16:11:52 +0800 Subject: [PATCH] fix(engine): tolerate duplicated JSON outputs --- lib/engine/jsonParser.ts | 80 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/lib/engine/jsonParser.ts b/lib/engine/jsonParser.ts index fc2489b..048987e 100644 --- a/lib/engine/jsonParser.ts +++ b/lib/engine/jsonParser.ts @@ -3,8 +3,9 @@ import { jsonrepair, JSONRepairError } from "jsonrepair"; // Strict-then-forgiving JSON parser for LLM output. Tries in order: // 1. Direct JSON.parse on the trimmed text. // 2. Extract from ```json``` fenced block. -// 3. Slice between first { and last } and parse. -// 4. Apply targeted regex pre-repairs (see preRepair) and try jsonrepair. +// 3. Parse the first complete JSON value prefix (handles duplicated objects). +// 4. Slice between first { and last } and parse. +// 5. Apply targeted regex pre-repairs (see preRepair) and try jsonrepair. // // On final failure, logs the first 800 chars of the raw model output so we // can diagnose the actual syntax error without flooding logs or leaking @@ -40,6 +41,67 @@ function preRepair(s: string): string { return s.replace(/"([^"\n:]+):(\s+)"/g, '"$1":$2"'); } +function firstJsonStart(s: string): number { + const objectStart = s.indexOf("{"); + const arrayStart = s.indexOf("["); + if (objectStart === -1) return arrayStart; + if (arrayStart === -1) return objectStart; + return Math.min(objectStart, arrayStart); +} + +function firstCompleteJsonValue(s: string): string | undefined { + const start = firstJsonStart(s); + if (start === -1) return undefined; + + const stack: string[] = []; + let inString = false; + let escaped = false; + + for (let i = start; i < s.length; i += 1) { + const ch = s[i]!; + + if (inString) { + if (escaped) { + escaped = false; + } else if (ch === "\\") { + escaped = true; + } else if (ch === "\"") { + inString = false; + } + continue; + } + + if (ch === "\"") { + inString = true; + continue; + } + + if (ch === "{") { + stack.push("}"); + continue; + } + + if (ch === "[") { + stack.push("]"); + continue; + } + + if (ch === "}" || ch === "]") { + if (stack.at(-1) !== ch) return undefined; + stack.pop(); + if (stack.length === 0) return s.slice(start, i + 1); + } + } + + return undefined; +} + +function parseFirstCompleteJsonValue(s: string): T | undefined { + const value = firstCompleteJsonValue(s); + if (!value) return undefined; + return JSON.parse(value) as T; +} + export function parseJsonLoose(raw: string): T { const trimmed = raw.trim(); @@ -54,10 +116,22 @@ export function parseJsonLoose(raw: string): T { try { return JSON.parse(fenced[1]) as T; } catch { - // fall through + try { + const parsed = parseFirstCompleteJsonValue(fenced[1]); + if (parsed !== undefined) return parsed; + } catch { + // fall through + } } } + try { + const parsed = parseFirstCompleteJsonValue(trimmed); + if (parsed !== undefined) return parsed; + } catch { + // fall through + } + const first = trimmed.indexOf("{"); const last = trimmed.lastIndexOf("}"); const slice =