Support Feishu file node retrieval
This commit is contained in:
@@ -128,6 +128,24 @@ def _feishu_post(path: str, payload: dict | None = None, config: FeishuRetrieval
|
|||||||
return data
|
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:
|
def _response_error_detail(response: httpx.Response) -> str:
|
||||||
try:
|
try:
|
||||||
body = response.json()
|
body = response.json()
|
||||||
@@ -220,6 +238,17 @@ def _get_docx_content(doc_token: str, config: FeishuRetrievalConfig) -> str:
|
|||||||
return _parse_docx_content(items)
|
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:
|
def _get_wiki_node_info(node_token: str, config: FeishuRetrievalConfig) -> dict:
|
||||||
"""获取 Wiki 节点信息,返回 space_id 和 obj_token"""
|
"""获取 Wiki 节点信息,返回 space_id 和 obj_token"""
|
||||||
data = _feishu_get("/open-apis/wiki/v2/spaces/get_node", params={"token": node_token}, config=config)
|
data = _feishu_get("/open-apis/wiki/v2/spaces/get_node", params={"token": node_token}, config=config)
|
||||||
@@ -378,6 +407,11 @@ class FeishuKnowledgeService:
|
|||||||
provider="feishu",
|
provider="feishu",
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
if load_errors:
|
||||||
|
raise ExternalServiceError(
|
||||||
|
"部分飞书知识库读取失败:" + ";".join(load_errors[:3]),
|
||||||
|
provider="feishu",
|
||||||
|
)
|
||||||
|
|
||||||
# 关键词匹配评分(简单但够用的检索方式)
|
# 关键词匹配评分(简单但够用的检索方式)
|
||||||
scored_chunks = cls._score_and_rank(question, all_chunks)
|
scored_chunks = cls._score_and_rank(question, all_chunks)
|
||||||
@@ -410,14 +444,17 @@ class FeishuKnowledgeService:
|
|||||||
# 1. 判断 node 类型:先查 wiki 节点信息
|
# 1. 判断 node 类型:先查 wiki 节点信息
|
||||||
try:
|
try:
|
||||||
node_info = cls._get_node_info_with_cache(node_id, config)
|
node_info = cls._get_node_info_with_cache(node_id, config)
|
||||||
except ExternalServiceError:
|
except ExternalServiceError as node_exc:
|
||||||
# 可能是普通 docx token,直接尝试读取
|
# 可能是普通 docx token,直接尝试读取
|
||||||
try:
|
try:
|
||||||
content = cls._get_document_content_with_cache(node_id, config)
|
content = cls._get_document_content_with_cache(node_id, config)
|
||||||
title = node_id
|
title = node_id
|
||||||
return [(title, content, f"{base_url}/docx/{node_id}")]
|
return [(title, content, f"{base_url}/docx/{node_id}")]
|
||||||
except ExternalServiceError:
|
except ExternalServiceError as docx_exc:
|
||||||
return []
|
raise ExternalServiceError(
|
||||||
|
f"飞书节点读取失败:{node_exc};按普通 docx token 读取也失败:{docx_exc}",
|
||||||
|
provider="feishu",
|
||||||
|
) from docx_exc
|
||||||
|
|
||||||
obj_type = node_info.get("obj_type", "")
|
obj_type = node_info.get("obj_type", "")
|
||||||
obj_token = node_info.get("obj_token", "")
|
obj_token = node_info.get("obj_token", "")
|
||||||
@@ -429,6 +466,10 @@ class FeishuKnowledgeService:
|
|||||||
content = cls._get_document_content_with_cache(obj_token, config)
|
content = cls._get_document_content_with_cache(obj_token, config)
|
||||||
source_url = f"{base_url}/wiki/{node_id}"
|
source_url = f"{base_url}/wiki/{node_id}"
|
||||||
return [(title, content, source_url)]
|
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)
|
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))
|
documents.append((child_title, content, source_url))
|
||||||
except ExternalServiceError:
|
except ExternalServiceError:
|
||||||
continue
|
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:
|
if obj_type == "docx" and obj_token:
|
||||||
@@ -489,6 +537,21 @@ class FeishuKnowledgeService:
|
|||||||
cls._content_cache[cache_key] = (content, now)
|
cls._content_cache[cache_key] = (content, now)
|
||||||
return content
|
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
|
@classmethod
|
||||||
def _get_wiki_children_with_cache(
|
def _get_wiki_children_with_cache(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
Reference in New Issue
Block a user