From 925ef0238d33f24799f2f0b5524add618f2732eb Mon Sep 17 00:00:00 2001 From: Nelson <1475262689@qq.com> Date: Wed, 8 Jul 2026 12:29:21 +0800 Subject: [PATCH] Support Feishu file node retrieval --- .../backend/app/services/feishu_service.py | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) 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 eba5be7..9ae0bc6 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 @@ -128,6 +128,24 @@ def _feishu_post(path: str, payload: dict | None = None, config: FeishuRetrieval return data +def _feishu_download(path: str, config: FeishuRetrievalConfig) -> bytes: + token = _get_tenant_token(config) + url = f"{FEISHU_OPEN_API}{path}" + headers = {"Authorization": f"Bearer {token}"} + + try: + resp = httpx.get(url, headers=headers, timeout=config.timeout_seconds) + resp.raise_for_status() + return resp.content + except httpx.HTTPStatusError as exc: + raise ExternalServiceError( + f"飞书文件下载失败 [{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 + + def _response_error_detail(response: httpx.Response) -> str: try: body = response.json() @@ -220,6 +238,17 @@ def _get_docx_content(doc_token: str, config: FeishuRetrievalConfig) -> str: return _parse_docx_content(items) +def _get_file_content(file_token: str, config: FeishuRetrievalConfig) -> str: + """下载普通文件节点内容,主要用于 Markdown/TXT 知识库文件。""" + content = _feishu_download(f"/open-apis/drive/v1/files/{file_token}/download", config) + for encoding in ("utf-8-sig", "utf-8", "gb18030"): + try: + return content.decode(encoding).strip() + except UnicodeDecodeError: + continue + raise ExternalServiceError("飞书文件不是可解析的文本内容", provider="feishu") + + def _get_wiki_node_info(node_token: str, config: FeishuRetrievalConfig) -> dict: """获取 Wiki 节点信息,返回 space_id 和 obj_token""" data = _feishu_get("/open-apis/wiki/v2/spaces/get_node", params={"token": node_token}, config=config) @@ -378,6 +407,11 @@ class FeishuKnowledgeService: provider="feishu", ) return [] + if load_errors: + raise ExternalServiceError( + "部分飞书知识库读取失败:" + ";".join(load_errors[:3]), + provider="feishu", + ) # 关键词匹配评分(简单但够用的检索方式) scored_chunks = cls._score_and_rank(question, all_chunks) @@ -410,14 +444,17 @@ class FeishuKnowledgeService: # 1. 判断 node 类型:先查 wiki 节点信息 try: node_info = cls._get_node_info_with_cache(node_id, config) - except ExternalServiceError: + except ExternalServiceError as node_exc: # 可能是普通 docx token,直接尝试读取 try: content = cls._get_document_content_with_cache(node_id, config) title = node_id return [(title, content, f"{base_url}/docx/{node_id}")] - except ExternalServiceError: - return [] + except ExternalServiceError as docx_exc: + raise ExternalServiceError( + f"飞书节点读取失败:{node_exc};按普通 docx token 读取也失败:{docx_exc}", + provider="feishu", + ) from docx_exc obj_type = node_info.get("obj_type", "") obj_token = node_info.get("obj_token", "") @@ -429,6 +466,10 @@ class FeishuKnowledgeService: content = cls._get_document_content_with_cache(obj_token, config) source_url = f"{base_url}/wiki/{node_id}" return [(title, content, source_url)] + if obj_type == "file": + content = cls._get_file_content_with_cache(obj_token, config) + source_url = f"{base_url}/wiki/{node_id}" + return [(title, content, source_url)] # 如果节点是文件夹,获取子节点 children = cls._get_wiki_children_with_cache(space_id, node_id, config) @@ -447,6 +488,13 @@ class FeishuKnowledgeService: documents.append((child_title, content, source_url)) except ExternalServiceError: continue + elif child_obj_type == "file" and child_obj_token: + try: + content = cls._get_file_content_with_cache(child_obj_token, config) + source_url = f"{base_url}/wiki/{child_node_token}" + documents.append((child_title, content, source_url)) + except ExternalServiceError: + continue # 也读取节点本身的文档(如果有) if obj_type == "docx" and obj_token: @@ -489,6 +537,21 @@ class FeishuKnowledgeService: cls._content_cache[cache_key] = (content, now) return content + @classmethod + def _get_file_content_with_cache(cls, file_token: str, config: FeishuRetrievalConfig) -> str: + """带缓存读取普通文件内容""" + cache_key = f"{config.app_id}:file:{file_token}" + now = time.time() + + if cache_key in cls._content_cache: + content, ts = cls._content_cache[cache_key] + if now - ts < cls._CACHE_TTL: + return content + + content = _get_file_content(file_token, config) + cls._content_cache[cache_key] = (content, now) + return content + @classmethod def _get_wiki_children_with_cache( cls,