feishu_doc_tool.py
1 """Feishu Document Tool -- read document content via Feishu/Lark API. 2 3 Provides ``feishu_doc_read`` for reading document content as plain text. 4 Uses the same lazy-import + BaseRequest pattern as feishu_comment.py. 5 """ 6 7 import json 8 import logging 9 import threading 10 11 from tools.registry import registry, tool_error, tool_result 12 13 logger = logging.getLogger(__name__) 14 15 # Thread-local storage for the lark client injected by feishu_comment handler. 16 _local = threading.local() 17 18 19 def set_client(client): 20 """Store a lark client for the current thread (called by feishu_comment).""" 21 _local.client = client 22 23 24 def get_client(): 25 """Return the lark client for the current thread, or None.""" 26 return getattr(_local, "client", None) 27 28 29 # --------------------------------------------------------------------------- 30 # feishu_doc_read 31 # --------------------------------------------------------------------------- 32 33 _RAW_CONTENT_URI = "/open-apis/docx/v1/documents/:document_id/raw_content" 34 35 FEISHU_DOC_READ_SCHEMA = { 36 "name": "feishu_doc_read", 37 "description": ( 38 "Read the full content of a Feishu/Lark document as plain text. " 39 "Useful when you need more context beyond the quoted text in a comment." 40 ), 41 "parameters": { 42 "type": "object", 43 "properties": { 44 "doc_token": { 45 "type": "string", 46 "description": "The document token (from the document URL or comment context).", 47 }, 48 }, 49 "required": ["doc_token"], 50 }, 51 } 52 53 54 def _check_feishu(): 55 try: 56 import lark_oapi # noqa: F401 57 return True 58 except ImportError: 59 return False 60 61 62 def _handle_feishu_doc_read(args: dict, **kwargs) -> str: 63 doc_token = args.get("doc_token", "").strip() 64 if not doc_token: 65 return tool_error("doc_token is required") 66 67 client = get_client() 68 if client is None: 69 return tool_error("Feishu client not available (not in a Feishu comment context)") 70 71 try: 72 from lark_oapi import AccessTokenType 73 from lark_oapi.core.enum import HttpMethod 74 from lark_oapi.core.model.base_request import BaseRequest 75 except ImportError: 76 return tool_error("lark_oapi not installed") 77 78 request = ( 79 BaseRequest.builder() 80 .http_method(HttpMethod.GET) 81 .uri(_RAW_CONTENT_URI) 82 .token_types({AccessTokenType.TENANT}) 83 .paths({"document_id": doc_token}) 84 .build() 85 ) 86 87 # Tool handlers run synchronously in a worker thread (no running event 88 # loop), so call the blocking lark client directly. 89 response = client.request(request) 90 91 code = getattr(response, "code", None) 92 if code != 0: 93 msg = getattr(response, "msg", "unknown error") 94 return tool_error(f"Failed to read document: code={code} msg={msg}") 95 96 raw = getattr(response, "raw", None) 97 if raw and hasattr(raw, "content"): 98 try: 99 body = json.loads(raw.content) 100 content = body.get("data", {}).get("content", "") 101 return tool_result(success=True, content=content) 102 except (json.JSONDecodeError, AttributeError): 103 pass 104 105 # Fallback: try response.data 106 data = getattr(response, "data", None) 107 if data: 108 if isinstance(data, dict): 109 content = data.get("content", "") 110 else: 111 content = getattr(data, "content", str(data)) 112 return tool_result(success=True, content=content) 113 114 return tool_error("No content returned from document API") 115 116 117 # --------------------------------------------------------------------------- 118 # Registration 119 # --------------------------------------------------------------------------- 120 121 registry.register( 122 name="feishu_doc_read", 123 toolset="feishu_doc", 124 schema=FEISHU_DOC_READ_SCHEMA, 125 handler=_handle_feishu_doc_read, 126 check_fn=_check_feishu, 127 requires_env=[], 128 is_async=False, 129 description="Read Feishu document content", 130 emoji="\U0001f4c4", 131 )