|
|
@@ -3,6 +3,10 @@
|
|
|
"""
|
|
|
|
|
|
import hashlib
|
|
|
+import json
|
|
|
+import re
|
|
|
+
|
|
|
+from typing import Dict, List, Optional
|
|
|
|
|
|
from datetime import datetime, timezone, date, timedelta
|
|
|
from requests import RequestException
|
|
|
@@ -183,3 +187,77 @@ def days_remaining_in_month():
|
|
|
remaining_days = (last_day_of_month - today).days
|
|
|
|
|
|
return remaining_days
|
|
|
+
|
|
|
+
|
|
|
+def safe_json_parse(text: str) -> Optional[Dict | List]:
|
|
|
+ """多层降级解析 JSON:直接解析 → 提取代码块 → 提取 JSON 对象/数组
|
|
|
+
|
|
|
+ 模型有时返回 ```json ... ``` 包裹的文本,或文本中夹杂 markdown 前缀/后缀。
|
|
|
+ 先尝试直接解析(最常见路径),失败后逐层降级提取。
|
|
|
+ """
|
|
|
+ if not text:
|
|
|
+ return None
|
|
|
+
|
|
|
+ # 降级 1:直接解析
|
|
|
+ try:
|
|
|
+ return json.loads(text)
|
|
|
+ except (json.JSONDecodeError, TypeError):
|
|
|
+ pass
|
|
|
+
|
|
|
+ clean = text.strip()
|
|
|
+
|
|
|
+ # 降级 2:提取最外层 json 代码块 ```json ... ```
|
|
|
+ # 优先匹配带语言标注的,再退到任意 code fence
|
|
|
+ m = re.search(r"```json\s*(.*?)\s*```", clean, re.DOTALL)
|
|
|
+ if m:
|
|
|
+ try:
|
|
|
+ return json.loads(m.group(1))
|
|
|
+ except (json.JSONDecodeError, TypeError):
|
|
|
+ pass
|
|
|
+ else:
|
|
|
+ m = re.search(r"```\s*(.*?)\s*```", clean, re.DOTALL)
|
|
|
+ if m:
|
|
|
+ try:
|
|
|
+ return json.loads(m.group(1))
|
|
|
+ except (json.JSONDecodeError, TypeError):
|
|
|
+ pass
|
|
|
+
|
|
|
+ # 降级 3:在文本中查找第一个完整 JSON 对象 { ... } 或数组 [ ... ]
|
|
|
+ # 逐字符扫描,维护字符串状态机,正确处理内嵌括号和转义引号
|
|
|
+ for bracket_pair in [("{}", "{", "}"), ("[]", "[", "]")]:
|
|
|
+ opener, closer = bracket_pair[1], bracket_pair[2]
|
|
|
+ start = clean.find(opener)
|
|
|
+ if start == -1:
|
|
|
+ continue
|
|
|
+ depth = 0
|
|
|
+ in_string = False
|
|
|
+ escape_next = False
|
|
|
+ for i in range(start, len(clean)):
|
|
|
+ ch = clean[i]
|
|
|
+ if escape_next:
|
|
|
+ escape_next = False
|
|
|
+ continue
|
|
|
+ if ch == "\\":
|
|
|
+ escape_next = True
|
|
|
+ continue
|
|
|
+ if ch == '"' and not escape_next:
|
|
|
+ in_string = not in_string
|
|
|
+ continue
|
|
|
+ if in_string:
|
|
|
+ continue
|
|
|
+ if ch == opener:
|
|
|
+ depth += 1
|
|
|
+ elif ch == closer:
|
|
|
+ depth -= 1
|
|
|
+ if depth == 0:
|
|
|
+ try:
|
|
|
+ return json.loads(clean[start : i + 1])
|
|
|
+ except (json.JSONDecodeError, TypeError):
|
|
|
+ return None
|
|
|
+ # 数组或对象未闭合时也尝试下
|
|
|
+ try:
|
|
|
+ return json.loads(clean[start:])
|
|
|
+ except (json.JSONDecodeError, TypeError):
|
|
|
+ pass
|
|
|
+
|
|
|
+ return None
|