Underground-Digital/Workflow-Engine
0
1import json2from typing import Optional3 4import httpx5 6from core.tools.errors import ToolProviderCredentialValidationError7from extensions.ext_redis import redis_client8 9 10def auth(credentials):11 app_id = credentials.get("app_id")12 app_secret = credentials.get("app_secret")13 if not app_id or not app_secret:14 raise ToolProviderCredentialValidationError("app_id and app_secret is required")15 try:16 assert FeishuRequest(app_id, app_secret).tenant_access_token is not None17 except Exception as e:18 raise ToolProviderCredentialValidationError(str(e))19 20 21def convert_add_records(json_str):22 try:23 data = json.loads(json_str)24 if not isinstance(data, list):25 raise ValueError("Parsed data must be a list")26 converted_data = [{"fields": json.dumps(item, ensure_ascii=False)} for item in data]27 return converted_data28 except json.JSONDecodeError:29 raise ValueError("The input string is not valid JSON")30 except Exception as e:31 raise ValueError(f"An error occurred while processing the data: {e}")32 33 34def convert_update_records(json_str):35 try:36 data = json.loads(json_str)37 if not isinstance(data, list):38 raise ValueError("Parsed data must be a list")39 40 converted_data = [41 {"fields": json.dumps(record["fields"], ensure_ascii=False), "record_id": record["record_id"]}42 for record in data43 if "fields" in record and "record_id" in record44 ]45 46 if len(converted_data) != len(data):47 raise ValueError("Each record must contain 'fields' and 'record_id'")48 49 return converted_data50 except json.JSONDecodeError:51 raise ValueError("The input string is not valid JSON")52 except Exception as e:53 raise ValueError(f"An error occurred while processing the data: {e}")54 55 56class FeishuRequest:57 API_BASE_URL = "https://lark-plugin-api.solutionsuite.cn/lark-plugin"58 59 def __init__(self, app_id: str, app_secret: str):60 self.app_id = app_id61 self.app_secret = app_secret62 63 @property64 def tenant_access_token(self):65 feishu_tenant_access_token = f"tools:{self.app_id}:feishu_tenant_access_token"66 if redis_client.exists(feishu_tenant_access_token):67 return redis_client.get(feishu_tenant_access_token).decode()68 res = self.get_tenant_access_token(self.app_id, self.app_secret)69 redis_client.setex(feishu_tenant_access_token, res.get("expire"), res.get("tenant_access_token"))70 return res.get("tenant_access_token")71 72 def _send_request(73 self,74 url: str,75 method: str = "post",76 require_token: bool = True,77 payload: Optional[dict] = None,78 params: Optional[dict] = None,79 ):80 headers = {81 "Content-Type": "application/json",82 "user-agent": "Dify",83 }84 if require_token:85 headers["tenant-access-token"] = f"{self.tenant_access_token}"86 res = httpx.request(method=method, url=url, headers=headers, json=payload, params=params, timeout=30).json()87 if res.get("code") != 0:88 raise Exception(res)89 return res90 91 def get_tenant_access_token(self, app_id: str, app_secret: str) -> dict:92 """93 API url: https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal94 Example Response:95 {96 "code": 0,97 "msg": "ok",98 "tenant_access_token": "t-caecc734c2e3328a62489fe0648c4b98779515d3",99 "expire": 7200100 }101 """102 url = f"{self.API_BASE_URL}/access_token/get_tenant_access_token"103 payload = {"app_id": app_id, "app_secret": app_secret}104 res = self._send_request(url, require_token=False, payload=payload)105 return res106 107 def create_document(self, title: str, content: str, folder_token: str) -> dict:108 """109 API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/create110 Example Response:111 {112 "data": {113 "title": "title",114 "url": "https://svi136aogf123.feishu.cn/docx/VWbvd4fEdoW0WSxaY1McQTz8n7d",115 "type": "docx",116 "token": "VWbvd4fEdoW0WSxaY1McQTz8n7d"117 },118 "log_id": "021721281231575fdbddc0200ff00060a9258ec0000103df61b5d",119 "code": 0,120 "msg": "创建飞书文档成功,请查看"121 }122 """123 url = f"{self.API_BASE_URL}/document/create_document"124 payload = {125 "title": title,126 "content": content,127 "folder_token": folder_token,128 }129 res = self._send_request(url, payload=payload)130 return res.get("data")131 132 def write_document(self, document_id: str, content: str, position: str = "end") -> dict:133 url = f"{self.API_BASE_URL}/document/write_document"134 payload = {"document_id": document_id, "content": content, "position": position}135 res = self._send_request(url, payload=payload)136 return res137 138 def get_document_content(self, document_id: str, mode: str = "markdown", lang: str = "0") -> dict:139 """140 API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/raw_content141 Example Response:142 {143 "code": 0,144 "msg": "success",145 "data": {146 "content": "云文档\n多人实时协同,插入一切元素。不仅是在线文档,更是强大的创作和互动工具\n云文档:专为协作而生\n"147 }148 }149 """ # noqa: E501150 params = {151 "document_id": document_id,152 "mode": mode,153 "lang": lang,154 }155 url = f"{self.API_BASE_URL}/document/get_document_content"156 res = self._send_request(url, method="GET", params=params)157 return res.get("data").get("content")158 159 def list_document_blocks(160 self, document_id: str, page_token: str, user_id_type: str = "open_id", page_size: int = 500161 ) -> dict:162 """163 API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/list164 """165 params = {166 "user_id_type": user_id_type,167 "document_id": document_id,168 "page_size": page_size,169 "page_token": page_token,170 }171 url = f"{self.API_BASE_URL}/document/list_document_blocks"172 res = self._send_request(url, method="GET", params=params)173 return res.get("data")174 175 def send_bot_message(self, receive_id_type: str, receive_id: str, msg_type: str, content: str) -> dict:176 """177 API url: https://open.larkoffice.com/document/server-docs/im-v1/message/create178 """179 url = f"{self.API_BASE_URL}/message/send_bot_message"180 params = {181 "receive_id_type": receive_id_type,182 }183 payload = {184 "receive_id": receive_id,185 "msg_type": msg_type,186 "content": content.strip('"').replace(r"\"", '"').replace(r"\\", "\\"),187 }188 res = self._send_request(url, params=params, payload=payload)189 return res.get("data")190 191 def send_webhook_message(self, webhook: str, msg_type: str, content: str) -> dict:192 url = f"{self.API_BASE_URL}/message/send_webhook_message"193 payload = {194 "webhook": webhook,195 "msg_type": msg_type,196 "content": content.strip('"').replace(r"\"", '"').replace(r"\\", "\\"),197 }198 res = self._send_request(url, require_token=False, payload=payload)199 return res200 201 def get_chat_messages(202 self,203 container_id: str,204 start_time: str,205 end_time: str,206 page_token: str,207 sort_type: str = "ByCreateTimeAsc",208 page_size: int = 20,209 ) -> dict:210 """211 API url: https://open.larkoffice.com/document/server-docs/im-v1/message/list212 """213 url = f"{self.API_BASE_URL}/message/get_chat_messages"214 params = {215 "container_id": container_id,216 "start_time": start_time,217 "end_time": end_time,218 "sort_type": sort_type,219 "page_token": page_token,220 "page_size": page_size,221 }222 res = self._send_request(url, method="GET", params=params)223 return res.get("data")224 225 def get_thread_messages(226 self, container_id: str, page_token: str, sort_type: str = "ByCreateTimeAsc", page_size: int = 20227 ) -> dict:228 """229 API url: https://open.larkoffice.com/document/server-docs/im-v1/message/list230 """231 url = f"{self.API_BASE_URL}/message/get_thread_messages"232 params = {233 "container_id": container_id,234 "sort_type": sort_type,235 "page_token": page_token,236 "page_size": page_size,237 }238 res = self._send_request(url, method="GET", params=params)239 return res.get("data")240 241 def create_task(self, summary: str, start_time: str, end_time: str, completed_time: str, description: str) -> dict:242 # 创建任务243 url = f"{self.API_BASE_URL}/task/create_task"244 payload = {245 "summary": summary,246 "start_time": start_time,247 "end_time": end_time,248 "completed_at": completed_time,249 "description": description,250 }251 res = self._send_request(url, payload=payload)252 return res.get("data")253 254 def update_task(255 self, task_guid: str, summary: str, start_time: str, end_time: str, completed_time: str, description: str256 ) -> dict:257 # 更新任务258 url = f"{self.API_BASE_URL}/task/update_task"259 payload = {260 "task_guid": task_guid,261 "summary": summary,262 "start_time": start_time,263 "end_time": end_time,264 "completed_time": completed_time,265 "description": description,266 }267 res = self._send_request(url, method="PATCH", payload=payload)268 return res.get("data")269 270 def delete_task(self, task_guid: str) -> dict:271 # 删除任务272 url = f"{self.API_BASE_URL}/task/delete_task"273 payload = {274 "task_guid": task_guid,275 }276 res = self._send_request(url, method="DELETE", payload=payload)277 return res278 279 def add_members(self, task_guid: str, member_phone_or_email: str, member_role: str) -> dict:280 # 删除任务281 url = f"{self.API_BASE_URL}/task/add_members"282 payload = {283 "task_guid": task_guid,284 "member_phone_or_email": member_phone_or_email,285 "member_role": member_role,286 }287 res = self._send_request(url, payload=payload)288 return res289 290 def get_wiki_nodes(self, space_id: str, parent_node_token: str, page_token: str, page_size: int = 20) -> dict:291 # 获取知识库全部子节点列表292 url = f"{self.API_BASE_URL}/wiki/get_wiki_nodes"293 payload = {294 "space_id": space_id,295 "parent_node_token": parent_node_token,296 "page_token": page_token,297 "page_size": page_size,298 }299 res = self._send_request(url, payload=payload)300 return res.get("data")301 302 def get_primary_calendar(self, user_id_type: str = "open_id") -> dict:303 url = f"{self.API_BASE_URL}/calendar/get_primary_calendar"304 params = {305 "user_id_type": user_id_type,306 }307 res = self._send_request(url, method="GET", params=params)308 return res.get("data")309 310 def create_event(311 self,312 summary: str,313 description: str,314 start_time: str,315 end_time: str,316 attendee_ability: str,317 need_notification: bool = True,318 auto_record: bool = False,319 ) -> dict:320 url = f"{self.API_BASE_URL}/calendar/create_event"321 payload = {322 "summary": summary,323 "description": description,324 "need_notification": need_notification,325 "start_time": start_time,326 "end_time": end_time,327 "auto_record": auto_record,328 "attendee_ability": attendee_ability,329 }330 res = self._send_request(url, payload=payload)331 return res.get("data")332 333 def update_event(334 self,335 event_id: str,336 summary: str,337 description: str,338 need_notification: bool,339 start_time: str,340 end_time: str,341 auto_record: bool,342 ) -> dict:343 url = f"{self.API_BASE_URL}/calendar/update_event/{event_id}"344 payload = {}345 if summary:346 payload["summary"] = summary347 if description:348 payload["description"] = description349 if start_time:350 payload["start_time"] = start_time351 if end_time:352 payload["end_time"] = end_time353 if need_notification:354 payload["need_notification"] = need_notification355 if auto_record:356 payload["auto_record"] = auto_record357 res = self._send_request(url, method="PATCH", payload=payload)358 return res359 360 def delete_event(self, event_id: str, need_notification: bool = True) -> dict:361 url = f"{self.API_BASE_URL}/calendar/delete_event/{event_id}"362 params = {363 "need_notification": need_notification,364 }365 res = self._send_request(url, method="DELETE", params=params)366 return res367 368 def list_events(self, start_time: str, end_time: str, page_token: str, page_size: int = 50) -> dict:369 url = f"{self.API_BASE_URL}/calendar/list_events"370 params = {371 "start_time": start_time,372 "end_time": end_time,373 "page_token": page_token,374 "page_size": page_size,375 }376 res = self._send_request(url, method="GET", params=params)377 return res.get("data")378 379 def search_events(380 self,381 query: str,382 start_time: str,383 end_time: str,384 page_token: str,385 user_id_type: str = "open_id",386 page_size: int = 20,387 ) -> dict:388 url = f"{self.API_BASE_URL}/calendar/search_events"389 payload = {390 "query": query,391 "start_time": start_time,392 "end_time": end_time,393 "page_token": page_token,394 "user_id_type": user_id_type,395 "page_size": page_size,396 }397 res = self._send_request(url, payload=payload)398 return res.get("data")399 400 def add_event_attendees(self, event_id: str, attendee_phone_or_email: str, need_notification: bool = True) -> dict:401 # 参加日程参会人402 url = f"{self.API_BASE_URL}/calendar/add_event_attendees"403 payload = {404 "event_id": event_id,405 "attendee_phone_or_email": attendee_phone_or_email,406 "need_notification": need_notification,407 }408 res = self._send_request(url, payload=payload)409 return res.get("data")410 411 def create_spreadsheet(412 self,413 title: str,414 folder_token: str,415 ) -> dict:416 # 创建电子表格417 url = f"{self.API_BASE_URL}/spreadsheet/create_spreadsheet"418 payload = {419 "title": title,420 "folder_token": folder_token,421 }422 res = self._send_request(url, payload=payload)423 return res.get("data")424 425 def get_spreadsheet(426 self,427 spreadsheet_token: str,428 user_id_type: str = "open_id",429 ) -> dict:430 # 获取电子表格信息431 url = f"{self.API_BASE_URL}/spreadsheet/get_spreadsheet"432 params = {433 "spreadsheet_token": spreadsheet_token,434 "user_id_type": user_id_type,435 }436 res = self._send_request(url, method="GET", params=params)437 return res.get("data")438 439 def list_spreadsheet_sheets(440 self,441 spreadsheet_token: str,442 ) -> dict:443 # 列出电子表格的所有工作表444 url = f"{self.API_BASE_URL}/spreadsheet/list_spreadsheet_sheets"445 params = {446 "spreadsheet_token": spreadsheet_token,447 }448 res = self._send_request(url, method="GET", params=params)449 return res.get("data")450 451 def add_rows(452 self,453 spreadsheet_token: str,454 sheet_id: str,455 sheet_name: str,456 length: int,457 values: str,458 ) -> dict:459 # 增加行,在工作表最后添加460 url = f"{self.API_BASE_URL}/spreadsheet/add_rows"461 payload = {462 "spreadsheet_token": spreadsheet_token,463 "sheet_id": sheet_id,464 "sheet_name": sheet_name,465 "length": length,466 "values": values,467 }468 res = self._send_request(url, payload=payload)469 return res.get("data")470 471 def add_cols(472 self,473 spreadsheet_token: str,474 sheet_id: str,475 sheet_name: str,476 length: int,477 values: str,478 ) -> dict:479 # 增加列,在工作表最后添加480 url = f"{self.API_BASE_URL}/spreadsheet/add_cols"481 payload = {482 "spreadsheet_token": spreadsheet_token,483 "sheet_id": sheet_id,484 "sheet_name": sheet_name,485 "length": length,486 "values": values,487 }488 res = self._send_request(url, payload=payload)489 return res.get("data")490 491 def read_rows(492 self,493 spreadsheet_token: str,494 sheet_id: str,495 sheet_name: str,496 start_row: int,497 num_rows: int,498 user_id_type: str = "open_id",499 ) -> dict:500 # 读取工作表行数据501 url = f"{self.API_BASE_URL}/spreadsheet/read_rows"502 params = {503 "spreadsheet_token": spreadsheet_token,504 "sheet_id": sheet_id,505 "sheet_name": sheet_name,506 "start_row": start_row,507 "num_rows": num_rows,508 "user_id_type": user_id_type,509 }510 res = self._send_request(url, method="GET", params=params)511 return res.get("data")512 513 def read_cols(514 self,515 spreadsheet_token: str,516 sheet_id: str,517 sheet_name: str,518 start_col: int,519 num_cols: int,520 user_id_type: str = "open_id",521 ) -> dict:522 # 读取工作表列数据523 url = f"{self.API_BASE_URL}/spreadsheet/read_cols"524 params = {525 "spreadsheet_token": spreadsheet_token,526 "sheet_id": sheet_id,527 "sheet_name": sheet_name,528 "start_col": start_col,529 "num_cols": num_cols,530 "user_id_type": user_id_type,531 }532 res = self._send_request(url, method="GET", params=params)533 return res.get("data")534 535 def read_table(536 self,537 spreadsheet_token: str,538 sheet_id: str,539 sheet_name: str,540 num_range: str,541 query: str,542 user_id_type: str = "open_id",543 ) -> dict:544 # 自定义读取行列数据545 url = f"{self.API_BASE_URL}/spreadsheet/read_table"546 params = {547 "spreadsheet_token": spreadsheet_token,548 "sheet_id": sheet_id,549 "sheet_name": sheet_name,550 "range": num_range,551 "query": query,552 "user_id_type": user_id_type,553 }554 res = self._send_request(url, method="GET", params=params)555 return res.get("data")556 557 def create_base(558 self,559 name: str,560 folder_token: str,561 ) -> dict:562 # 创建多维表格563 url = f"{self.API_BASE_URL}/base/create_base"564 payload = {565 "name": name,566 "folder_token": folder_token,567 }568 res = self._send_request(url, payload=payload)569 return res.get("data")570 571 def add_records(572 self,573 app_token: str,574 table_id: str,575 table_name: str,576 records: str,577 user_id_type: str = "open_id",578 ) -> dict:579 # 新增多条记录580 url = f"{self.API_BASE_URL}/base/add_records"581 params = {582 "app_token": app_token,583 "table_id": table_id,584 "table_name": table_name,585 "user_id_type": user_id_type,586 }587 payload = {588 "records": convert_add_records(records),589 }590 res = self._send_request(url, params=params, payload=payload)591 return res.get("data")592 593 def update_records(594 self,595 app_token: str,596 table_id: str,597 table_name: str,598 records: str,599 user_id_type: str,600 ) -> dict:601 # 更新多条记录602 url = f"{self.API_BASE_URL}/base/update_records"603 params = {604 "app_token": app_token,605 "table_id": table_id,606 "table_name": table_name,607 "user_id_type": user_id_type,608 }609 payload = {610 "records": convert_update_records(records),611 }612 res = self._send_request(url, params=params, payload=payload)613 return res.get("data")614 615 def delete_records(616 self,617 app_token: str,618 table_id: str,619 table_name: str,620 record_ids: str,621 ) -> dict:622 # 删除多条记录623 url = f"{self.API_BASE_URL}/base/delete_records"624 params = {625 "app_token": app_token,626 "table_id": table_id,627 "table_name": table_name,628 }629 if not record_ids:630 record_id_list = []631 else:632 try:633 record_id_list = json.loads(record_ids)634 except json.JSONDecodeError:635 raise ValueError("The input string is not valid JSON")636 payload = {637 "records": record_id_list,638 }639 res = self._send_request(url, params=params, payload=payload)640 return res.get("data")641 642 def search_record(643 self,644 app_token: str,645 table_id: str,646 table_name: str,647 view_id: str,648 field_names: str,649 sort: str,650 filters: str,651 page_token: str,652 automatic_fields: bool = False,653 user_id_type: str = "open_id",654 page_size: int = 20,655 ) -> dict:656 # 查询记录,单次最多查询 500 行记录。657 url = f"{self.API_BASE_URL}/base/search_record"658 params = {659 "app_token": app_token,660 "table_id": table_id,661 "table_name": table_name,662 "user_id_type": user_id_type,663 "page_token": page_token,664 "page_size": page_size,665 }666 667 if not field_names:668 field_name_list = []669 else:670 try:671 field_name_list = json.loads(field_names)672 except json.JSONDecodeError:673 raise ValueError("The input string is not valid JSON")674 675 if not sort:676 sort_list = []677 else:678 try:679 sort_list = json.loads(sort)680 except json.JSONDecodeError:681 raise ValueError("The input string is not valid JSON")682 683 if not filters:684 filter_dict = {}685 else:686 try:687 filter_dict = json.loads(filters)688 except json.JSONDecodeError:689 raise ValueError("The input string is not valid JSON")690 691 payload = {}692 693 if view_id:694 payload["view_id"] = view_id695 if field_names:696 payload["field_names"] = field_name_list697 if sort:698 payload["sort"] = sort_list699 if filters:700 payload["filter"] = filter_dict701 if automatic_fields:702 payload["automatic_fields"] = automatic_fields703 res = self._send_request(url, params=params, payload=payload)704 return res.get("data")705 706 def get_base_info(707 self,708 app_token: str,709 ) -> dict:710 # 获取多维表格元数据711 url = f"{self.API_BASE_URL}/base/get_base_info"712 params = {713 "app_token": app_token,714 }715 res = self._send_request(url, method="GET", params=params)716 return res.get("data")717 718 def create_table(719 self,720 app_token: str,721 table_name: str,722 default_view_name: str,723 fields: str,724 ) -> dict:725 # 新增一个数据表726 url = f"{self.API_BASE_URL}/base/create_table"727 params = {728 "app_token": app_token,729 }730 if not fields:731 fields_list = []732 else:733 try:734 fields_list = json.loads(fields)735 except json.JSONDecodeError:736 raise ValueError("The input string is not valid JSON")737 payload = {738 "name": table_name,739 "fields": fields_list,740 }741 if default_view_name:742 payload["default_view_name"] = default_view_name743 res = self._send_request(url, params=params, payload=payload)744 return res.get("data")745 746 def delete_tables(747 self,748 app_token: str,749 table_ids: str,750 table_names: str,751 ) -> dict:752 # 删除多个数据表753 url = f"{self.API_BASE_URL}/base/delete_tables"754 params = {755 "app_token": app_token,756 }757 if not table_ids:758 table_id_list = []759 else:760 try:761 table_id_list = json.loads(table_ids)762 except json.JSONDecodeError:763 raise ValueError("The input string is not valid JSON")764 765 if not table_names:766 table_name_list = []767 else:768 try:769 table_name_list = json.loads(table_names)770 except json.JSONDecodeError:771 raise ValueError("The input string is not valid JSON")772 773 payload = {774 "table_ids": table_id_list,775 "table_names": table_name_list,776 }777 res = self._send_request(url, params=params, payload=payload)778 return res.get("data")779 780 def list_tables(781 self,782 app_token: str,783 page_token: str,784 page_size: int = 20,785 ) -> dict:786 # 列出多维表格下的全部数据表787 url = f"{self.API_BASE_URL}/base/list_tables"788 params = {789 "app_token": app_token,790 "page_token": page_token,791 "page_size": page_size,792 }793 res = self._send_request(url, method="GET", params=params)794 return res.get("data")795 796 def read_records(797 self,798 app_token: str,799 table_id: str,800 table_name: str,801 record_ids: str,802 user_id_type: str = "open_id",803 ) -> dict:804 url = f"{self.API_BASE_URL}/base/read_records"805 params = {806 "app_token": app_token,807 "table_id": table_id,808 "table_name": table_name,809 }810 if not record_ids:811 record_id_list = []812 else:813 try:814 record_id_list = json.loads(record_ids)815 except json.JSONDecodeError:816 raise ValueError("The input string is not valid JSON")817 payload = {818 "record_ids": record_id_list,819 "user_id_type": user_id_type,820 }821 res = self._send_request(url, method="GET", params=params, payload=payload)822 return res.get("data")823 