111 lines
2.4 KiB
TypeScript
111 lines
2.4 KiB
TypeScript
function stripMarkdownFences(value: string): string {
|
|
return value
|
|
.replace(/^```json\s*/i, "")
|
|
.replace(/^```\s*/i, "")
|
|
.replace(/\s*```$/, "")
|
|
.trim();
|
|
}
|
|
|
|
function extractBalancedJson(text: string): string | null {
|
|
let start = -1;
|
|
let inString = false;
|
|
let escaped = false;
|
|
const stack: string[] = [];
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
const char = text[i];
|
|
|
|
if (start === -1) {
|
|
if (char === "{" || char === "[") {
|
|
start = i;
|
|
stack.push(char);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (inString) {
|
|
if (!escaped && char === "\\") {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if (!escaped && char === '"') {
|
|
inString = false;
|
|
}
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
|
|
if (char === '"') {
|
|
inString = true;
|
|
continue;
|
|
}
|
|
|
|
if (char === "{" || char === "[") {
|
|
stack.push(char);
|
|
continue;
|
|
}
|
|
|
|
if (char === "}" || char === "]") {
|
|
const last = stack[stack.length - 1];
|
|
const isMatch =
|
|
(last === "{" && char === "}") || (last === "[" && char === "]");
|
|
|
|
if (!isMatch) {
|
|
return null;
|
|
}
|
|
|
|
stack.pop();
|
|
|
|
if (stack.length === 0 && start !== -1) {
|
|
return text.slice(start, i + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function sanitizeLooseJson(value: string): string {
|
|
return value
|
|
.replace(/[\u201C\u201D]/g, '"')
|
|
.replace(/[\u2018\u2019]/g, "'")
|
|
.replace(/,\s*([}\]])/g, "$1")
|
|
.trim();
|
|
}
|
|
|
|
export function parseJsonResponse(text: string): unknown {
|
|
if (!text || typeof text !== "string" || text.trim().length === 0) {
|
|
throw new Error("AI response is empty.");
|
|
}
|
|
|
|
const cleaned = stripMarkdownFences(text);
|
|
|
|
try {
|
|
return JSON.parse(cleaned);
|
|
} catch {
|
|
// continue to robust fallback
|
|
}
|
|
|
|
const extracted = extractBalancedJson(cleaned);
|
|
if (!extracted) {
|
|
throw new Error(
|
|
`No complete JSON object found in AI response. Preview: ${cleaned.slice(0, 220)}`,
|
|
);
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(extracted);
|
|
} catch {
|
|
const sanitized = sanitizeLooseJson(extracted);
|
|
try {
|
|
return JSON.parse(sanitized);
|
|
} catch (error) {
|
|
const message =
|
|
error instanceof Error ? error.message : "unknown parse error";
|
|
throw new Error(
|
|
`JSON parse failed after recovery attempts: ${message}. Preview: ${sanitized.slice(0, 220)}`,
|
|
);
|
|
}
|
|
}
|
|
}
|