UNPKG

t-comm

Version:

专业、稳定、纯粹的工具库

163 lines (156 loc) 6.15 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _typeof = require('@babel/runtime/helpers/typeof'); function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; } var _typeof__default = /*#__PURE__*/_interopDefaultLegacy(_typeof); /** * 通用 LLM 输出 JSON 容错解析 * * 用于解析大语言模型(AI Agent)输出的 JSON:通常 LLM 输出的内容有以下几种 "脏" 情况: * 1. 直接就是合法 JSON * 2. JSON 被自然语言包围("好的,下面是 JSON: { ... } 谢谢") * 3. JSON 被 markdown 代码块包裹(```json { ... } ```) * 4. 因 token 上限被截断(缺少闭合的 ] 和 }) * * 本函数依次使用 5 层策略尽力解析: * 策略 1: 直接 JSON.parse * 策略 2: 提取第一个 `{` 到最后一个 `}` 之间的内容 * 策略 3: 贪婪匹配最外层 markdown 代码块 * 策略 4: 按括号平衡从第一个 `{` 开始匹配(应对尾部自然语言夹带) * 策略 5: token 截断时尝试补齐闭合符号("、]、}) * * @param content LLM 返回的原始字符串 * @param logTag 策略 5 触发时的日志前缀(默认 [AI]),便于上层区分调用方 * * @example * ```ts * const result = parseLooseJson<{ name: string }>(aiResponseText); * if (result) console.log(result.name); * ``` */ function parseLooseJson(content) { var logTag = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : '[AI]'; var trimmed = content.trim(); var tryParse = function tryParse(str) { try { var _result = JSON.parse(str); if (_result && _typeof__default["default"](_result) === 'object') return _result; } catch (/* ignore */_a) {/* ignore */} return null; }; // 策略 1: 直接解析 var result = tryParse(trimmed); // 策略 2: 提取第一个 `{` 到最后一个 `}` 之间的内容 if (!result) { var firstBrace = trimmed.indexOf('{'); var lastBrace = trimmed.lastIndexOf('}'); if (firstBrace !== -1 && lastBrace > firstBrace) { result = tryParse(trimmed.slice(firstBrace, lastBrace + 1)); } } // 策略 3: 贪婪匹配最外层 markdown 代码块(从第一个 ``` 到最后一个 ```) if (!result) { var codeBlockMatch = trimmed.match(/```(?:json)?\s*([\s\S]*)```\s*$/); if (codeBlockMatch) { var inner = codeBlockMatch[1].trim(); result = tryParse(inner); if (!result) { var _firstBrace = inner.indexOf('{'); var _lastBrace = inner.lastIndexOf('}'); if (_firstBrace !== -1 && _lastBrace > _firstBrace) { result = tryParse(inner.slice(_firstBrace, _lastBrace + 1)); } } } } // 策略 4: 按括号平衡从第一个 `{` 开始匹配 // 策略 2 是简单地取 "第一个{到最后一个}",可能因为尾部有自然语言而匹配到错误范围。 // 这里从第一个 `{` 开始,逐字符追踪括号层级和字符串上下文,找到顶层闭合位置。 if (!result) { var startIdx = trimmed.indexOf('{'); if (startIdx !== -1) { var depth = 0; var inString = false; var _escape = false; var endIdx = -1; for (var i = startIdx; i < trimmed.length; i += 1) { var ch = trimmed[i]; if (_escape) { _escape = false; continue; } if (ch === '\\' && inString) { _escape = true; continue; } if (ch === '"') { inString = !inString; continue; } if (inString) continue; if (ch === '{') depth += 1;else if (ch === '}') { depth -= 1; if (depth === 0) { endIdx = i; break; } } } if (endIdx > startIdx) { result = tryParse(trimmed.slice(startIdx, endIdx + 1)); } } } // 策略 5: JSON 被 token 截断时尝试修复 // AI 输出可能因为 token 限制在中途截断,导致 JSON 不完整(如缺少闭合的 "]}")。 // 尝试从第一个 `{` 取到字符串末尾,逐步补上缺失的闭合符号。 if (!result) { var _startIdx = trimmed.indexOf('{'); if (_startIdx !== -1) { var jsonCandidate = trimmed.slice(_startIdx); // 移除尾部明显不属于 JSON 的内容(如 AI 在 JSON 后面追加的自然语言) var lastJsonChar = Math.max(jsonCandidate.lastIndexOf('}'), jsonCandidate.lastIndexOf(']'), jsonCandidate.lastIndexOf('"')); if (lastJsonChar > 0) { jsonCandidate = jsonCandidate.slice(0, lastJsonChar + 1); } // 计算缺失的闭合符号 var depth2 = 0; var arrDepth = 0; var inStr = false; var esc = false; for (var _i = 0; _i < jsonCandidate.length; _i += 1) { var _ch = jsonCandidate[_i]; if (esc) { esc = false; continue; } if (_ch === '\\' && inStr) { esc = true; continue; } if (_ch === '"') { inStr = !inStr; continue; } if (inStr) continue; if (_ch === '{') depth2 += 1;else if (_ch === '}') depth2 -= 1;else if (_ch === '[') arrDepth += 1;else if (_ch === ']') arrDepth -= 1; } // 如果在字符串内,先关闭字符串 if (inStr) jsonCandidate += '"'; // 补上缺失的 ] 和 } for (var a = 0; a < arrDepth; a += 1) jsonCandidate += ']'; for (var b = 0; b < depth2; b += 1) jsonCandidate += '}'; if (depth2 > 0 || arrDepth > 0 || inStr) { var closeParts = []; if (inStr) closeParts.push('1个"'); if (arrDepth > 0) closeParts.push("".concat(arrDepth, "\u4E2A]")); if (depth2 > 0) closeParts.push("".concat(depth2, "\u4E2A}")); // eslint-disable-next-line no-console console.log("".concat(logTag, " parseLooseJson \u7B56\u75655: \u5C1D\u8BD5\u4FEE\u590D\u622A\u65AD\u7684 JSON (\u8865\u5145 ").concat(closeParts.join(''), ")")); result = tryParse(jsonCandidate); } } } return result; } exports.parseLooseJson = parseLooseJson;