diff --git a/ai_knowledge_base_v2/apps/backend/app/services/feishu_service.py b/ai_knowledge_base_v2/apps/backend/app/services/feishu_service.py index ffb4a72..eba5be7 100644 --- a/ai_knowledge_base_v2/apps/backend/app/services/feishu_service.py +++ b/ai_knowledge_base_v2/apps/backend/app/services/feishu_service.py @@ -80,9 +80,17 @@ def _feishu_get(path: str, params: dict | None = None, config: FeishuRetrievalCo url = f"{FEISHU_OPEN_API}{path}" headers = {"Authorization": f"Bearer {token}"} - resp = httpx.get(url, headers=headers, params=params, timeout=config.timeout_seconds) - resp.raise_for_status() - data = resp.json() + try: + resp = httpx.get(url, headers=headers, params=params, timeout=config.timeout_seconds) + resp.raise_for_status() + data = resp.json() + except httpx.HTTPStatusError as exc: + raise ExternalServiceError( + f"飞书HTTP请求失败 [{path}]:{exc.response.status_code} {_response_error_detail(exc.response)}", + provider="feishu", + ) from exc + except httpx.HTTPError as exc: + raise ExternalServiceError(f"飞书网络请求失败 [{path}]:{exc}", provider="feishu") from exc if data.get("code") != 0: raise ExternalServiceError( @@ -100,9 +108,17 @@ def _feishu_post(path: str, payload: dict | None = None, config: FeishuRetrieval url = f"{FEISHU_OPEN_API}{path}" headers = {"Authorization": f"Bearer {token}"} - resp = httpx.post(url, headers=headers, json=payload, timeout=config.timeout_seconds) - resp.raise_for_status() - data = resp.json() + try: + resp = httpx.post(url, headers=headers, json=payload, timeout=config.timeout_seconds) + resp.raise_for_status() + data = resp.json() + except httpx.HTTPStatusError as exc: + raise ExternalServiceError( + f"飞书HTTP请求失败 [{path}]:{exc.response.status_code} {_response_error_detail(exc.response)}", + provider="feishu", + ) from exc + except httpx.HTTPError as exc: + raise ExternalServiceError(f"飞书网络请求失败 [{path}]:{exc}", provider="feishu") from exc if data.get("code") != 0: raise ExternalServiceError( @@ -112,6 +128,15 @@ def _feishu_post(path: str, payload: dict | None = None, config: FeishuRetrieval return data +def _response_error_detail(response: httpx.Response) -> str: + try: + body = response.json() + except ValueError: + return response.text[:200] + message = body.get("msg") or body.get("message") or body.get("error") or body + return str(message)[:300] + + def _extract_text(elements: list[dict]) -> str: """从 elements 列表中提取纯文本""" texts = [] @@ -208,8 +233,16 @@ def _get_wiki_node_info(node_token: str, config: FeishuRetrievalConfig) -> dict: } -def _get_wiki_children(node_token: str, config: FeishuRetrievalConfig, page_size: int = 50) -> list[dict]: +def _get_wiki_children( + space_id: str, + node_token: str, + config: FeishuRetrievalConfig, + page_size: int = 50, +) -> list[dict]: """获取 Wiki 节点的子节点列表""" + if not space_id: + raise ExternalServiceError("飞书知识库缺少 SpaceID,无法读取子节点", provider="feishu") + all_children = [] page_token = "" while True: @@ -220,7 +253,7 @@ def _get_wiki_children(node_token: str, config: FeishuRetrievalConfig, page_size if page_token: params["page_token"] = page_token - data = _feishu_get("/open-apis/wiki/v2/spaces/nodes/get_child_nodes", params=params, config=config) + data = _feishu_get(f"/open-apis/wiki/v2/spaces/{space_id}/nodes", params=params, config=config) children = data.get("data", {}).get("items", []) all_children.extend(children) @@ -231,7 +264,7 @@ def _get_wiki_children(node_token: str, config: FeishuRetrievalConfig, page_size return all_children -def _get_space_node_tree(space_id: str) -> list[dict]: +def _get_space_node_tree(space_id: str, config: FeishuRetrievalConfig | None = None) -> list[dict]: """获取知识空间下所有根节点""" all_nodes = [] page_token = "" @@ -243,7 +276,7 @@ def _get_space_node_tree(space_id: str) -> list[dict]: if page_token: params["page_token"] = page_token - data = _feishu_get("/open-apis/wiki/v2/spaces/nodes", params=params) + data = _feishu_get(f"/open-apis/wiki/v2/spaces/{space_id}/nodes", params=params, config=config) items = data.get("data", {}).get("items", []) all_nodes.extend(items) @@ -398,7 +431,7 @@ class FeishuKnowledgeService: return [(title, content, source_url)] # 如果节点是文件夹,获取子节点 - children = cls._get_wiki_children_with_cache(node_id, config) + children = cls._get_wiki_children_with_cache(space_id, node_id, config) documents = [] for child in children: @@ -457,9 +490,14 @@ class FeishuKnowledgeService: return content @classmethod - def _get_wiki_children_with_cache(cls, node_token: str, config: FeishuRetrievalConfig) -> list[dict]: + def _get_wiki_children_with_cache( + cls, + space_id: str, + node_token: str, + config: FeishuRetrievalConfig, + ) -> list[dict]: """带缓存获取子节点""" - cache_key = f"{config.app_id}:children:{node_token}" + cache_key = f"{config.app_id}:children:{space_id}:{node_token}" now = time.time() if cache_key in cls._content_cache: @@ -467,7 +505,7 @@ class FeishuKnowledgeService: if now - ts < cls._CACHE_TTL: return eval(cached_data) # noqa: S307 - children = _get_wiki_children(node_token, config) + children = _get_wiki_children(space_id, node_token, config) cls._content_cache[cache_key] = (str(children), now) return children