接口报错异常处理

This commit is contained in:
董宏凯 committed 2026-05-18 23:06:59 +08:00
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}
</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>
) : (
<span className="ml-1.5 text-xs text-muted-foreground">{event.message}</span>
@@ -90,6 +90,8 @@ export interface ProgressEvent {
// notice(非致命告警)/ error:标题 + 真实原因
title?: string;
detail?: string;
// 接口解析失败时的原始错误响应体,前端默认折叠展示
raw?: string;
// materials_ready
materials?: AIMaterial[];
keywords?: string[];
@@ -21,9 +21,15 @@ logger = logging.getLogger(__name__)
_MAX_MATERIALS = 12
def _notice(title: str, detail: str) -> dict:
"""构造一个非致命告警事件,前端在时间线显示「标题 + 真实原因」,不打断写作流程。"""
return {"type": "notice", "agent_name": "素材收集专家", "title": title, "detail": detail}
def _notice(title: str, detail: str, raw: str = "") -> dict:
"""构造一个非致命告警事件,前端在时间线显示「标题 + 真实原因」,不打断写作流程。
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:
@@ -173,7 +179,13 @@ async def researcher_node(state: AIWritingState, config: RunnableConfig) -> dict
notices.append(_notice("模型错误", f"素材排序失败,已按相关度兜底排序。原因:{rank_error}"))
if 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 {
"material_package": pkg,
@@ -11,6 +11,8 @@ logger = logging.getLogger(__name__)
# 内网知识库默认检索接口与知识库名(可经 config.yaml 的 ai_writing 段覆盖)
_DEFAULT_ENDPOINT = "http://10.106.57.180:7863/knowledge_base/search_docs_es"
_DEFAULT_KB = "samples"
# 单条原始响应体最大保留长度,避免把超大响应整段灌进前端时间线
_RAW_SNIPPET_LIMIT = 2000
class IntranetSearchProvider(BaseSearchProvider):
@@ -25,14 +27,26 @@ class IntranetSearchProvider(BaseSearchProvider):
self,
base_url: str = "",
knowledge_base_name: str = _DEFAULT_KB,
timeout: int = 15,
timeout: int = 120,
**_ignored,
):
self.endpoint = base_url or _DEFAULT_ENDPOINT
self.knowledge_base_name = knowledge_base_name or _DEFAULT_KB
# 内网 ES 接口可能较慢,超时放宽到 2 分钟
self.timeout = timeout
# 收集本次运行的检索异常,供 researcher 节点取出并提示用户(ES 波动等)
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]:
import httpx
@@ -42,21 +56,38 @@ class IntranetSearchProvider(BaseSearchProvider):
"knowledge_base_name": self.knowledge_base_name,
"ts": "yes",
}
# 先拿到原始响应文本,JSON 解析失败时把它原样回传前端便于定位接口返回了什么
raw_text = ""
try:
async with httpx.AsyncClient(timeout=self.timeout) as client:
resp = await client.post(self.endpoint, json=payload)
resp.raise_for_status()
raw_text = resp.text
data = resp.json()
except Exception as e:
except httpx.HTTPStatusError as e:
# 非 2xx:接口有响应但状态码异常,把响应体一并暴露给前端
logger.warning("intranet search failed for query=%r: %s", query, 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 []
# 接口直接返回数组;兼容个别情况下包了一层 data/results 的情况
if isinstance(data, dict):
# 正常情况下接口直接返回数组;兼容个别情况下包了一层 data/results 的结构
if isinstance(data, list):
items = data
elif isinstance(data, dict):
items = data.get("data") or data.get("results") or []
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] = []
for i, item in enumerate(items[:max_results]):