From 3562a67a5807b9131cc33109e0f2e2c4a99e1995 Mon Sep 17 00:00:00 2001 From: OG T Date: Tue, 31 Mar 2026 15:04:39 +0800 Subject: [PATCH] fix(openclaw): robust JSON repair for small LLM responses --- apps/api/src/services/openclaw.py | 40 +++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/apps/api/src/services/openclaw.py b/apps/api/src/services/openclaw.py index 07445da4d..14fc49213 100644 --- a/apps/api/src/services/openclaw.py +++ b/apps/api/src/services/openclaw.py @@ -942,31 +942,61 @@ class OpenClawService: # ========================================================================= def _extract_json_from_response(self, text: str) -> str | None: - """從 LLM 回應中提取 JSON""" - # 嘗試直接解析 + """從 LLM 回應中提取 JSON (含啟發式修補)""" + # 0. 清理開頭結尾空白 + text = text.strip() + if not text: + return None + + # 1. 嘗試直接解析 try: json.loads(text) return text except json.JSONDecodeError: pass - # 嘗試從 markdown code block 提取 + # 2. 嘗試從 markdown code block 提取 patterns = [ r"```json\s*([\s\S]*?)\s*```", r"```\s*([\s\S]*?)\s*```", - r"\{[\s\S]*\}", + r"(\{[\s\S]*\})", # 貪婪匹配最大括號對 ] for pattern in patterns: match = re.search(pattern, text) if match: - candidate = match.group(1) if "```" in pattern else match.group(0) + candidate = match.group(1) if "(" in pattern else match.group(0) + candidate = candidate.strip() try: json.loads(candidate) return candidate except json.JSONDecodeError: + # 3. 啟發式修補: 如果結尾缺少括號,嘗試補齊 + if candidate.startswith("{") and not candidate.endswith("}"): + for i in range(1, 5): # 嘗試補 1-5 個括號/引號 + try: + repaired = candidate + '"' * (i-1) + "}" * i + json.loads(repaired) + logger.info("json_repaired_heuristically", level=i) + return repaired + except: + continue continue + # 4. 極端情況: 找出最後一個有效 key + if "{" in text: + start_idx = text.find("{") + candidate = text[start_idx:] + # 暴力去除非法尾綴 (如 \t\t...) + candidate = re.sub(r"[ \t\r\n]+$", "", candidate) + if not candidate.endswith("}"): + candidate += '"}' # 嘗試最簡單的閉合 + try: + json.loads(candidate) + return candidate + except: + pass + return None def _parse_analysis_result(self, raw_response: str) -> OpenClawDecision | None: