接口报错异常处理
This commit is contained in:
1 parent
73e76fc72f
commit
2c56a84bb7
4 files changed
+65
-9
No files matched your search
@@ -77,6 +77,17 @@ export function TimelineStep({ event, isLast, children }: TimelineStepProps) {
|
|||||||
{event.detail}
|
{event.detail}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{/* 接口解析失败时的原始错误响应体,默认折叠,点击展开 */}
|
||||||
|
{event.raw && (
|
||||||
|
<details className="mt-1">
|
||||||
|
<summary className="cursor-pointer select-none text-[11px] font-medium text-muted-foreground hover:text-foreground">
|
||||||
|
错误返回结果(点击展开)
|
||||||
|
</summary>
|
||||||
|
<pre className="mt-1 max-h-60 overflow-auto whitespace-pre-wrap break-all rounded bg-muted/60 p-2 text-[10px] leading-relaxed text-muted-foreground">
|
||||||
|
{event.raw}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="ml-1.5 text-xs text-muted-foreground">{event.message}</span>
|
<span className="ml-1.5 text-xs text-muted-foreground">{event.message}</span>
|
||||||
|
|||||||
@@ -90,6 +90,8 @@ export interface ProgressEvent {
|
|||||||
// notice(非致命告警)/ error:标题 + 真实原因
|
// notice(非致命告警)/ error:标题 + 真实原因
|
||||||
title?: string;
|
title?: string;
|
||||||
detail?: string;
|
detail?: string;
|
||||||
|
// 接口解析失败时的原始错误响应体,前端默认折叠展示
|
||||||
|
raw?: string;
|
||||||
// materials_ready
|
// materials_ready
|
||||||
materials?: AIMaterial[];
|
materials?: AIMaterial[];
|
||||||
keywords?: string[];
|
keywords?: string[];
|
||||||
|
|||||||
+16
-4
@@ -21,9 +21,15 @@ logger = logging.getLogger(__name__)
|
|||||||
_MAX_MATERIALS = 12
|
_MAX_MATERIALS = 12
|
||||||
|
|
||||||
|
|
||||||
def _notice(title: str, detail: str) -> dict:
|
def _notice(title: str, detail: str, raw: str = "") -> dict:
|
||||||
"""构造一个非致命告警事件,前端在时间线显示「标题 + 真实原因」,不打断写作流程。"""
|
"""构造一个非致命告警事件,前端在时间线显示「标题 + 真实原因」,不打断写作流程。
|
||||||
return {"type": "notice", "agent_name": "素材收集专家", "title": title, "detail": detail}
|
|
||||||
|
raw 非空时附带接口的原始错误响应体,前端默认折叠展示。
|
||||||
|
"""
|
||||||
|
event = {"type": "notice", "agent_name": "素材收集专家", "title": title, "detail": detail}
|
||||||
|
if raw:
|
||||||
|
event["raw"] = raw
|
||||||
|
return event
|
||||||
|
|
||||||
|
|
||||||
async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict:
|
async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict:
|
||||||
@@ -173,7 +179,13 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
|
|||||||
notices.append(_notice("模型错误", f"素材排序失败,已按相关度兜底排序。原因:{rank_error}"))
|
notices.append(_notice("模型错误", f"素材排序失败,已按相关度兜底排序。原因:{rank_error}"))
|
||||||
if search_errors:
|
if search_errors:
|
||||||
uniq = list(dict.fromkeys(search_errors))
|
uniq = list(dict.fromkeys(search_errors))
|
||||||
notices.append(_notice("ES 波动导致检索异常", ";".join(uniq[:3])))
|
# 解析失败时收集到的接口原始响应体,前端折叠展示便于定位
|
||||||
|
raw_dumps = list(dict.fromkeys(getattr(provider, "raw_responses", []) or []))
|
||||||
|
notices.append(_notice(
|
||||||
|
"ES 波动导致检索异常",
|
||||||
|
";".join(uniq[:3]),
|
||||||
|
raw="\n\n----------\n\n".join(raw_dumps[:3]),
|
||||||
|
))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"material_package": pkg,
|
"material_package": pkg,
|
||||||
|
|||||||
+36
-5
@@ -11,6 +11,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# 内网知识库默认检索接口与知识库名(可经 config.yaml 的 ai_writing 段覆盖)
|
# 内网知识库默认检索接口与知识库名(可经 config.yaml 的 ai_writing 段覆盖)
|
||||||
_DEFAULT_ENDPOINT = "http://10.106.57.180:7863/knowledge_base/search_docs_es"
|
_DEFAULT_ENDPOINT = "http://10.106.57.180:7863/knowledge_base/search_docs_es"
|
||||||
_DEFAULT_KB = "samples"
|
_DEFAULT_KB = "samples"
|
||||||
|
# 单条原始响应体最大保留长度,避免把超大响应整段灌进前端时间线
|
||||||
|
_RAW_SNIPPET_LIMIT = 2000
|
||||||
|
|
||||||
|
|
||||||
class IntranetSearchProvider(BaseSearchProvider):
|
class IntranetSearchProvider(BaseSearchProvider):
|
||||||
@@ -25,14 +27,26 @@ class IntranetSearchProvider(BaseSearchProvider):
|
|||||||
self,
|
self,
|
||||||
base_url: str = "",
|
base_url: str = "",
|
||||||
knowledge_base_name: str = _DEFAULT_KB,
|
knowledge_base_name: str = _DEFAULT_KB,
|
||||||
timeout: int = 15,
|
timeout: int = 120,
|
||||||
**_ignored,
|
**_ignored,
|
||||||
):
|
):
|
||||||
self.endpoint = base_url or _DEFAULT_ENDPOINT
|
self.endpoint = base_url or _DEFAULT_ENDPOINT
|
||||||
self.knowledge_base_name = knowledge_base_name or _DEFAULT_KB
|
self.knowledge_base_name = knowledge_base_name or _DEFAULT_KB
|
||||||
|
# 内网 ES 接口可能较慢,超时放宽到 2 分钟
|
||||||
self.timeout = timeout
|
self.timeout = timeout
|
||||||
# 收集本次运行的检索异常,供 researcher 节点取出并提示用户(ES 波动等)
|
# 收集本次运行的检索异常,供 researcher 节点取出并提示用户(ES 波动等)
|
||||||
self.errors: list[str] = []
|
self.errors: list[str] = []
|
||||||
|
# 解析失败时收集的原始响应体,供前端「错误返回结果」折叠展示
|
||||||
|
self.raw_responses: list[str] = []
|
||||||
|
|
||||||
|
def _record_raw(self, body: str) -> None:
|
||||||
|
"""收集解析失败 / 结构异常时的原始响应体,供前端折叠查看。"""
|
||||||
|
body = (body or "").strip()
|
||||||
|
if not body:
|
||||||
|
return
|
||||||
|
if len(body) > _RAW_SNIPPET_LIMIT:
|
||||||
|
body = body[:_RAW_SNIPPET_LIMIT] + "…(响应体过长,已截断)"
|
||||||
|
self.raw_responses.append(body)
|
||||||
|
|
||||||
async def search(self, query: str, max_results: int = 5, **kwargs) -> List[SearchResult]:
|
async def search(self, query: str, max_results: int = 5, **kwargs) -> List[SearchResult]:
|
||||||
import httpx
|
import httpx
|
||||||
@@ -42,21 +56,38 @@ class IntranetSearchProvider(BaseSearchProvider):
|
|||||||
"knowledge_base_name": self.knowledge_base_name,
|
"knowledge_base_name": self.knowledge_base_name,
|
||||||
"ts": "yes",
|
"ts": "yes",
|
||||||
}
|
}
|
||||||
|
# 先拿到原始响应文本,JSON 解析失败时把它原样回传前端便于定位接口返回了什么
|
||||||
|
raw_text = ""
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||||
resp = await client.post(self.endpoint, json=payload)
|
resp = await client.post(self.endpoint, json=payload)
|
||||||
resp.raise_for_status()
|
resp.raise_for_status()
|
||||||
|
raw_text = resp.text
|
||||||
data = resp.json()
|
data = resp.json()
|
||||||
except Exception as e:
|
except httpx.HTTPStatusError as e:
|
||||||
|
# 非 2xx:接口有响应但状态码异常,把响应体一并暴露给前端
|
||||||
logger.warning("intranet search failed for query=%r: %s", query, e)
|
logger.warning("intranet search failed for query=%r: %s", query, e)
|
||||||
self.errors.append(f"{type(e).__name__}: {e}")
|
self.errors.append(f"{type(e).__name__}: {e}")
|
||||||
|
self._record_raw(e.response.text if e.response is not None else "")
|
||||||
|
return []
|
||||||
|
except Exception as e:
|
||||||
|
# 连接失败 / 超时 / JSON 解析失败等;JSON 解析失败时 raw_text 已是接口响应体
|
||||||
|
logger.warning("intranet search failed for query=%r: %s", query, e)
|
||||||
|
self.errors.append(f"{type(e).__name__}: {e}")
|
||||||
|
self._record_raw(raw_text)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# 接口直接返回数组;兼容个别情况下包了一层 data/results 的情况
|
# 正常情况下接口直接返回数组;兼容个别情况下包了一层 data/results 的结构
|
||||||
if isinstance(data, dict):
|
if isinstance(data, list):
|
||||||
|
items = data
|
||||||
|
elif isinstance(data, dict):
|
||||||
items = data.get("data") or data.get("results") or []
|
items = data.get("data") or data.get("results") or []
|
||||||
else:
|
else:
|
||||||
items = data or []
|
# 返回结构既不是数组也不是已知 dict 包装:视为异常,把原始响应回传前端
|
||||||
|
logger.warning("intranet search unexpected response type for query=%r: %s", query, type(data).__name__)
|
||||||
|
self.errors.append(f"返回结构异常:期望数组,实际为 {type(data).__name__}")
|
||||||
|
self._record_raw(raw_text)
|
||||||
|
return []
|
||||||
|
|
||||||
results: List[SearchResult] = []
|
results: List[SearchResult] = []
|
||||||
for i, item in enumerate(items[:max_results]):
|
for i, item in enumerate(items[:max_results]):
|
||||||
|
|||||||
Reference in new issue
Block a user