接口报错异常处理
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}
|
||||
</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[];
|
||||
|
||||
+16
-4
@@ -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,
|
||||
|
||||
+36
-5
@@ -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]):
|
||||
|
||||
Reference in new issue
Block a user