From 5b4bee19395697aad1cd50959731440cbc61972c Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 8 Apr 2026 19:12:59 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E7=94=9F=E6=88=90?= =?UTF-8?q?=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/services/ai_rewriter.py | 57 ++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/app/services/ai_rewriter.py b/app/services/ai_rewriter.py index d90e949..c8c9d8f 100644 --- a/app/services/ai_rewriter.py +++ b/app/services/ai_rewriter.py @@ -484,6 +484,10 @@ class AIRewriter: return json.loads(raw) except json.JSONDecodeError: pass + try: + return json.loads(self._escape_control_chars_in_json_string(raw)) + except json.JSONDecodeError: + pass fenced = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw, flags=re.IGNORECASE).strip() if fenced != raw: @@ -491,14 +495,65 @@ class AIRewriter: return json.loads(fenced) except json.JSONDecodeError: pass + try: + return json.loads(self._escape_control_chars_in_json_string(fenced)) + except json.JSONDecodeError: + pass start = raw.find("{") end = raw.rfind("}") if start != -1 and end != -1 and end > start: - return json.loads(raw[start : end + 1]) + sliced = raw[start : end + 1] + try: + return json.loads(sliced) + except json.JSONDecodeError: + return json.loads(self._escape_control_chars_in_json_string(sliced)) raise ValueError("model output is not valid JSON") + def _escape_control_chars_in_json_string(self, s: str) -> str: + """ + 修复“近似 JSON”中字符串里的裸控制字符(尤其是换行), + 避免 `Invalid control character` 导致误判为无效 JSON。 + """ + out: list[str] = [] + in_string = False + escaped = False + for ch in s: + if in_string: + if escaped: + out.append(ch) + escaped = False + continue + if ch == "\\": + out.append(ch) + escaped = True + continue + if ch == '"': + out.append(ch) + in_string = False + continue + if ch == "\n": + out.append("\\n") + continue + if ch == "\r": + out.append("\\r") + continue + if ch == "\t": + out.append("\\t") + continue + if ord(ch) < 0x20: + out.append(f"\\u{ord(ch):04x}") + continue + out.append(ch) + continue + else: + out.append(ch) + if ch == '"': + in_string = True + escaped = False + return "".join(out) + def _chat_completions_json(self, user_prompt: str, timeout_sec: float, request_id: str) -> dict | None: """chat.completions:通义兼容层在 json_object 下易产出极短 JSON,故 DashScope 不传 response_format,并支持短文自动重试。""" max_attempts = 2 if self._prefer_chat_first else 1