
























@@ -3,16 +3,119 @@
33/** Parse the last JSON object in a noisy provider output string. */
44function extractLastJsonObject(raw: string): unknown {
55const trimmed = raw.trim();
6-const start = trimmed.lastIndexOf("{");
7-if (start === -1) {
8-return null;
6+const ranges: Array<{ end: number; start: number }> = [];
7+const starts: number[] = [];
8+let inString = false;
9+let escaped = false;
10+let preambleQuote: string | undefined;
11+let preambleEscaped = false;
12+let previousSignificant: string | undefined;
13+let lineHasNonWhitespace = false;
14+let arrayDepth = 0;
15+let candidateHasContent = false;
16+17+for (let index = 0; index < trimmed.length; index += 1) {
18+const character = trimmed[index];
19+if (inString) {
20+if (character === "\n" || character === "\r") {
21+starts.length = 0;
22+inString = false;
23+escaped = false;
24+} else if (escaped) {
25+escaped = false;
26+} else if (character === "\\") {
27+escaped = true;
28+} else if (character === '"') {
29+inString = false;
30+}
31+continue;
32+}
33+34+if (starts.length === 0) {
35+if (preambleQuote !== undefined) {
36+if (character === "\n" || character === "\r") {
37+preambleQuote = undefined;
38+preambleEscaped = false;
39+} else if (preambleEscaped) {
40+preambleEscaped = false;
41+} else if (character === "\\") {
42+preambleEscaped = true;
43+} else if (character === preambleQuote) {
44+preambleQuote = undefined;
45+}
46+continue;
47+}
48+if (character === '"' || character === "'" || character === "`") {
49+const previous = trimmed[index - 1];
50+if (previous === undefined || /[\s:([{]/.test(previous)) {
51+preambleQuote = character;
52+preambleEscaped = false;
53+continue;
54+}
55+}
56+if (character === "{") {
57+arrayDepth = 0;
58+candidateHasContent = false;
59+starts.push(index);
60+}
61+if (!/\s/.test(character)) {
62+previousSignificant = character;
63+lineHasNonWhitespace = true;
64+} else if (character === "\n" || character === "\r") {
65+lineHasNonWhitespace = false;
66+}
67+continue;
68+}
69+70+const hadCandidateContent = candidateHasContent;
71+if (character === '"') {
72+inString = true;
73+} else if (character === "{") {
74+if (
75+previousSignificant === ":" ||
76+previousSignificant === "[" ||
77+previousSignificant === '"' ||
78+(previousSignificant === "," && (lineHasNonWhitespace || arrayDepth > 0))
79+) {
80+starts.push(index);
81+} else if (!lineHasNonWhitespace && !hadCandidateContent) {
82+// Only resync at a clean record boundary; otherwise keep malformed
83+// outer objects from promoting diagnostic payloads as valid results.
84+starts.length = 1;
85+starts[0] = index;
86+arrayDepth = 0;
87+candidateHasContent = false;
88+}
89+} else if (character === "}" && starts.length > 0) {
90+const start = starts.pop();
91+if (start !== undefined && starts.length === 0) {
92+ranges.push({ start, end: index });
93+}
94+} else if (character === "[") {
95+arrayDepth += 1;
96+} else if (character === "]" && arrayDepth > 0) {
97+arrayDepth -= 1;
98+}
99+100+if (!/\s/.test(character)) {
101+candidateHasContent = true;
102+previousSignificant = character;
103+lineHasNonWhitespace = true;
104+} else if (character === "\n" || character === "\r") {
105+lineHasNonWhitespace = false;
106+}
9107}
10-const slice = trimmed.slice(start);
11-try {
12-return JSON.parse(slice);
13-} catch {
14-return null;
108+109+for (let index = ranges.length - 1; index >= 0; index -= 1) {
110+const range = ranges[index];
111+try {
112+return JSON.parse(trimmed.slice(range.start, range.end + 1));
113+} catch {
114+// Ignore malformed objects and try the previous completed range.
115+}
15116}
117+118+return null;
16119}
1712018121/** Extract Gemini CLI-style response text from the last JSON object in output. */
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。