/ tools / feishu_doc_tool.py
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  )