feishu_api_utils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519
  1. from typing import Optional
  2. import httpx
  3. from core.tools.errors import ToolProviderCredentialValidationError
  4. from extensions.ext_redis import redis_client
  5. def auth(credentials):
  6. app_id = credentials.get("app_id")
  7. app_secret = credentials.get("app_secret")
  8. if not app_id or not app_secret:
  9. raise ToolProviderCredentialValidationError("app_id and app_secret is required")
  10. try:
  11. assert FeishuRequest(app_id, app_secret).tenant_access_token is not None
  12. except Exception as e:
  13. raise ToolProviderCredentialValidationError(str(e))
  14. class FeishuRequest:
  15. API_BASE_URL = "https://lark-plugin-api.solutionsuite.cn/lark-plugin"
  16. def __init__(self, app_id: str, app_secret: str):
  17. self.app_id = app_id
  18. self.app_secret = app_secret
  19. @property
  20. def tenant_access_token(self):
  21. feishu_tenant_access_token = f"tools:{self.app_id}:feishu_tenant_access_token"
  22. if redis_client.exists(feishu_tenant_access_token):
  23. return redis_client.get(feishu_tenant_access_token).decode()
  24. res = self.get_tenant_access_token(self.app_id, self.app_secret)
  25. redis_client.setex(feishu_tenant_access_token, res.get("expire"), res.get("tenant_access_token"))
  26. return res.get("tenant_access_token")
  27. def _send_request(
  28. self,
  29. url: str,
  30. method: str = "post",
  31. require_token: bool = True,
  32. payload: Optional[dict] = None,
  33. params: Optional[dict] = None,
  34. ):
  35. headers = {
  36. "Content-Type": "application/json",
  37. "user-agent": "Dify",
  38. }
  39. if require_token:
  40. headers["tenant-access-token"] = f"{self.tenant_access_token}"
  41. res = httpx.request(method=method, url=url, headers=headers, json=payload, params=params, timeout=30).json()
  42. if res.get("code") != 0:
  43. raise Exception(res)
  44. return res
  45. def get_tenant_access_token(self, app_id: str, app_secret: str) -> dict:
  46. """
  47. API url: https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal
  48. Example Response:
  49. {
  50. "code": 0,
  51. "msg": "ok",
  52. "tenant_access_token": "t-caecc734c2e3328a62489fe0648c4b98779515d3",
  53. "expire": 7200
  54. }
  55. """
  56. url = f"{self.API_BASE_URL}/access_token/get_tenant_access_token"
  57. payload = {"app_id": app_id, "app_secret": app_secret}
  58. res = self._send_request(url, require_token=False, payload=payload)
  59. return res
  60. def create_document(self, title: str, content: str, folder_token: str) -> dict:
  61. """
  62. API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/create
  63. Example Response:
  64. {
  65. "data": {
  66. "title": "title",
  67. "url": "https://svi136aogf123.feishu.cn/docx/VWbvd4fEdoW0WSxaY1McQTz8n7d",
  68. "type": "docx",
  69. "token": "VWbvd4fEdoW0WSxaY1McQTz8n7d"
  70. },
  71. "log_id": "021721281231575fdbddc0200ff00060a9258ec0000103df61b5d",
  72. "code": 0,
  73. "msg": "创建飞书文档成功,请查看"
  74. }
  75. """
  76. url = f"{self.API_BASE_URL}/document/create_document"
  77. payload = {
  78. "title": title,
  79. "content": content,
  80. "folder_token": folder_token,
  81. }
  82. res = self._send_request(url, payload=payload)
  83. return res.get("data")
  84. def write_document(self, document_id: str, content: str, position: str = "end") -> dict:
  85. url = f"{self.API_BASE_URL}/document/write_document"
  86. payload = {"document_id": document_id, "content": content, "position": position}
  87. res = self._send_request(url, payload=payload)
  88. return res
  89. def get_document_content(self, document_id: str, mode: str = "markdown", lang: str = "0") -> dict:
  90. """
  91. API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/raw_content
  92. Example Response:
  93. {
  94. "code": 0,
  95. "msg": "success",
  96. "data": {
  97. "content": "云文档\n多人实时协同,插入一切元素。不仅是在线文档,更是强大的创作和互动工具\n云文档:专为协作而生\n"
  98. }
  99. }
  100. """ # noqa: E501
  101. params = {
  102. "document_id": document_id,
  103. "mode": mode,
  104. "lang": lang,
  105. }
  106. url = f"{self.API_BASE_URL}/document/get_document_content"
  107. res = self._send_request(url, method="GET", params=params)
  108. return res.get("data").get("content")
  109. def list_document_blocks(
  110. self, document_id: str, page_token: str, user_id_type: str = "open_id", page_size: int = 500
  111. ) -> dict:
  112. """
  113. API url: https://open.larkoffice.com/document/server-docs/docs/docs/docx-v1/document/list
  114. """
  115. params = {
  116. "user_id_type": user_id_type,
  117. "document_id": document_id,
  118. "page_size": page_size,
  119. "page_token": page_token,
  120. }
  121. url = f"{self.API_BASE_URL}/document/list_document_blocks"
  122. res = self._send_request(url, method="GET", params=params)
  123. return res.get("data")
  124. def send_bot_message(self, receive_id_type: str, receive_id: str, msg_type: str, content: str) -> dict:
  125. """
  126. API url: https://open.larkoffice.com/document/server-docs/im-v1/message/create
  127. """
  128. url = f"{self.API_BASE_URL}/message/send_bot_message"
  129. params = {
  130. "receive_id_type": receive_id_type,
  131. }
  132. payload = {
  133. "receive_id": receive_id,
  134. "msg_type": msg_type,
  135. "content": content.strip('"').replace(r"\"", '"').replace(r"\\", "\\"),
  136. }
  137. res = self._send_request(url, params=params, payload=payload)
  138. return res.get("data")
  139. def send_webhook_message(self, webhook: str, msg_type: str, content: str) -> dict:
  140. url = f"{self.API_BASE_URL}/message/send_webhook_message"
  141. payload = {
  142. "webhook": webhook,
  143. "msg_type": msg_type,
  144. "content": content.strip('"').replace(r"\"", '"').replace(r"\\", "\\"),
  145. }
  146. res = self._send_request(url, require_token=False, payload=payload)
  147. return res
  148. def get_chat_messages(
  149. self,
  150. container_id: str,
  151. start_time: str,
  152. end_time: str,
  153. page_token: str,
  154. sort_type: str = "ByCreateTimeAsc",
  155. page_size: int = 20,
  156. ) -> dict:
  157. """
  158. API url: https://open.larkoffice.com/document/server-docs/im-v1/message/list
  159. """
  160. url = f"{self.API_BASE_URL}/message/get_chat_messages"
  161. params = {
  162. "container_id": container_id,
  163. "start_time": start_time,
  164. "end_time": end_time,
  165. "sort_type": sort_type,
  166. "page_token": page_token,
  167. "page_size": page_size,
  168. }
  169. res = self._send_request(url, method="GET", params=params)
  170. return res.get("data")
  171. def get_thread_messages(
  172. self, container_id: str, page_token: str, sort_type: str = "ByCreateTimeAsc", page_size: int = 20
  173. ) -> dict:
  174. """
  175. API url: https://open.larkoffice.com/document/server-docs/im-v1/message/list
  176. """
  177. url = f"{self.API_BASE_URL}/message/get_thread_messages"
  178. params = {
  179. "container_id": container_id,
  180. "sort_type": sort_type,
  181. "page_token": page_token,
  182. "page_size": page_size,
  183. }
  184. res = self._send_request(url, method="GET", params=params)
  185. return res.get("data")
  186. def create_task(self, summary: str, start_time: str, end_time: str, completed_time: str, description: str) -> dict:
  187. # 创建任务
  188. url = f"{self.API_BASE_URL}/task/create_task"
  189. payload = {
  190. "summary": summary,
  191. "start_time": start_time,
  192. "end_time": end_time,
  193. "completed_at": completed_time,
  194. "description": description,
  195. }
  196. res = self._send_request(url, payload=payload)
  197. return res.get("data")
  198. def update_task(
  199. self, task_guid: str, summary: str, start_time: str, end_time: str, completed_time: str, description: str
  200. ) -> dict:
  201. # 更新任务
  202. url = f"{self.API_BASE_URL}/task/update_task"
  203. payload = {
  204. "task_guid": task_guid,
  205. "summary": summary,
  206. "start_time": start_time,
  207. "end_time": end_time,
  208. "completed_time": completed_time,
  209. "description": description,
  210. }
  211. res = self._send_request(url, method="PATCH", payload=payload)
  212. return res.get("data")
  213. def delete_task(self, task_guid: str) -> dict:
  214. # 删除任务
  215. url = f"{self.API_BASE_URL}/task/delete_task"
  216. payload = {
  217. "task_guid": task_guid,
  218. }
  219. res = self._send_request(url, method="DELETE", payload=payload)
  220. return res
  221. def add_members(self, task_guid: str, member_phone_or_email: str, member_role: str) -> dict:
  222. # 删除任务
  223. url = f"{self.API_BASE_URL}/task/add_members"
  224. payload = {
  225. "task_guid": task_guid,
  226. "member_phone_or_email": member_phone_or_email,
  227. "member_role": member_role,
  228. }
  229. res = self._send_request(url, payload=payload)
  230. return res
  231. def get_wiki_nodes(self, space_id: str, parent_node_token: str, page_token: str, page_size: int = 20) -> dict:
  232. # 获取知识库全部子节点列表
  233. url = f"{self.API_BASE_URL}/wiki/get_wiki_nodes"
  234. payload = {
  235. "space_id": space_id,
  236. "parent_node_token": parent_node_token,
  237. "page_token": page_token,
  238. "page_size": page_size,
  239. }
  240. res = self._send_request(url, payload=payload)
  241. return res.get("data")
  242. def get_primary_calendar(self, user_id_type: str = "open_id") -> dict:
  243. url = f"{self.API_BASE_URL}/calendar/get_primary_calendar"
  244. params = {
  245. "user_id_type": user_id_type,
  246. }
  247. res = self._send_request(url, method="GET", params=params)
  248. return res.get("data")
  249. def create_event(
  250. self,
  251. summary: str,
  252. description: str,
  253. start_time: str,
  254. end_time: str,
  255. attendee_ability: str,
  256. need_notification: bool = True,
  257. auto_record: bool = False,
  258. ) -> dict:
  259. url = f"{self.API_BASE_URL}/calendar/create_event"
  260. payload = {
  261. "summary": summary,
  262. "description": description,
  263. "need_notification": need_notification,
  264. "start_time": start_time,
  265. "end_time": end_time,
  266. "auto_record": auto_record,
  267. "attendee_ability": attendee_ability,
  268. }
  269. res = self._send_request(url, payload=payload)
  270. return res.get("data")
  271. def update_event(
  272. self,
  273. event_id: str,
  274. summary: str,
  275. description: str,
  276. need_notification: bool,
  277. start_time: str,
  278. end_time: str,
  279. auto_record: bool,
  280. ) -> dict:
  281. url = f"{self.API_BASE_URL}/calendar/update_event/{event_id}"
  282. payload = {}
  283. if summary:
  284. payload["summary"] = summary
  285. if description:
  286. payload["description"] = description
  287. if start_time:
  288. payload["start_time"] = start_time
  289. if end_time:
  290. payload["end_time"] = end_time
  291. if need_notification:
  292. payload["need_notification"] = need_notification
  293. if auto_record:
  294. payload["auto_record"] = auto_record
  295. res = self._send_request(url, method="PATCH", payload=payload)
  296. return res
  297. def delete_event(self, event_id: str, need_notification: bool = True) -> dict:
  298. url = f"{self.API_BASE_URL}/calendar/delete_event/{event_id}"
  299. params = {
  300. "need_notification": need_notification,
  301. }
  302. res = self._send_request(url, method="DELETE", params=params)
  303. return res
  304. def list_events(self, start_time: str, end_time: str, page_token: str, page_size: int = 50) -> dict:
  305. url = f"{self.API_BASE_URL}/calendar/list_events"
  306. params = {
  307. "start_time": start_time,
  308. "end_time": end_time,
  309. "page_token": page_token,
  310. "page_size": page_size,
  311. }
  312. res = self._send_request(url, method="GET", params=params)
  313. return res.get("data")
  314. def search_events(
  315. self,
  316. query: str,
  317. start_time: str,
  318. end_time: str,
  319. page_token: str,
  320. user_id_type: str = "open_id",
  321. page_size: int = 20,
  322. ) -> dict:
  323. url = f"{self.API_BASE_URL}/calendar/search_events"
  324. payload = {
  325. "query": query,
  326. "start_time": start_time,
  327. "end_time": end_time,
  328. "page_token": page_token,
  329. "user_id_type": user_id_type,
  330. "page_size": page_size,
  331. }
  332. res = self._send_request(url, payload=payload)
  333. return res.get("data")
  334. def add_event_attendees(self, event_id: str, attendee_phone_or_email: str, need_notification: bool = True) -> dict:
  335. # 参加日程参会人
  336. url = f"{self.API_BASE_URL}/calendar/add_event_attendees"
  337. payload = {
  338. "event_id": event_id,
  339. "attendee_phone_or_email": attendee_phone_or_email,
  340. "need_notification": need_notification,
  341. }
  342. res = self._send_request(url, payload=payload)
  343. return res.get("data")
  344. def create_spreadsheet(
  345. self,
  346. title: str,
  347. folder_token: str,
  348. ) -> dict:
  349. # 创建电子表格
  350. url = f"{self.API_BASE_URL}/spreadsheet/create_spreadsheet"
  351. payload = {
  352. "title": title,
  353. "folder_token": folder_token,
  354. }
  355. res = self._send_request(url, payload=payload)
  356. return res.get("data")
  357. def get_spreadsheet(
  358. self,
  359. spreadsheet_token: str,
  360. user_id_type: str = "open_id",
  361. ) -> dict:
  362. # 获取电子表格信息
  363. url = f"{self.API_BASE_URL}/spreadsheet/get_spreadsheet"
  364. params = {
  365. "spreadsheet_token": spreadsheet_token,
  366. "user_id_type": user_id_type,
  367. }
  368. res = self._send_request(url, method="GET", params=params)
  369. return res.get("data")
  370. def list_spreadsheet_sheets(
  371. self,
  372. spreadsheet_token: str,
  373. ) -> dict:
  374. # 列出电子表格的所有工作表
  375. url = f"{self.API_BASE_URL}/spreadsheet/list_spreadsheet_sheets"
  376. params = {
  377. "spreadsheet_token": spreadsheet_token,
  378. }
  379. res = self._send_request(url, method="GET", params=params)
  380. return res.get("data")
  381. def add_rows(
  382. self,
  383. spreadsheet_token: str,
  384. sheet_id: str,
  385. sheet_name: str,
  386. length: int,
  387. values: str,
  388. ) -> dict:
  389. # 增加行,在工作表最后添加
  390. url = f"{self.API_BASE_URL}/spreadsheet/add_rows"
  391. payload = {
  392. "spreadsheet_token": spreadsheet_token,
  393. "sheet_id": sheet_id,
  394. "sheet_name": sheet_name,
  395. "length": length,
  396. "values": values,
  397. }
  398. res = self._send_request(url, payload=payload)
  399. return res.get("data")
  400. def add_cols(
  401. self,
  402. spreadsheet_token: str,
  403. sheet_id: str,
  404. sheet_name: str,
  405. length: int,
  406. values: str,
  407. ) -> dict:
  408. # 增加列,在工作表最后添加
  409. url = f"{self.API_BASE_URL}/spreadsheet/add_cols"
  410. payload = {
  411. "spreadsheet_token": spreadsheet_token,
  412. "sheet_id": sheet_id,
  413. "sheet_name": sheet_name,
  414. "length": length,
  415. "values": values,
  416. }
  417. res = self._send_request(url, payload=payload)
  418. return res.get("data")
  419. def read_rows(
  420. self,
  421. spreadsheet_token: str,
  422. sheet_id: str,
  423. sheet_name: str,
  424. start_row: int,
  425. num_rows: int,
  426. user_id_type: str = "open_id",
  427. ) -> dict:
  428. # 读取工作表行数据
  429. url = f"{self.API_BASE_URL}/spreadsheet/read_rows"
  430. params = {
  431. "spreadsheet_token": spreadsheet_token,
  432. "sheet_id": sheet_id,
  433. "sheet_name": sheet_name,
  434. "start_row": start_row,
  435. "num_rows": num_rows,
  436. "user_id_type": user_id_type,
  437. }
  438. res = self._send_request(url, method="GET", params=params)
  439. return res.get("data")
  440. def read_cols(
  441. self,
  442. spreadsheet_token: str,
  443. sheet_id: str,
  444. sheet_name: str,
  445. start_col: int,
  446. num_cols: int,
  447. user_id_type: str = "open_id",
  448. ) -> dict:
  449. # 读取工作表列数据
  450. url = f"{self.API_BASE_URL}/spreadsheet/read_cols"
  451. params = {
  452. "spreadsheet_token": spreadsheet_token,
  453. "sheet_id": sheet_id,
  454. "sheet_name": sheet_name,
  455. "start_col": start_col,
  456. "num_cols": num_cols,
  457. "user_id_type": user_id_type,
  458. }
  459. res = self._send_request(url, method="GET", params=params)
  460. return res.get("data")
  461. def read_table(
  462. self,
  463. spreadsheet_token: str,
  464. sheet_id: str,
  465. sheet_name: str,
  466. num_range: str,
  467. query: str,
  468. user_id_type: str = "open_id",
  469. ) -> dict:
  470. # 自定义读取行列数据
  471. url = f"{self.API_BASE_URL}/spreadsheet/read_table"
  472. params = {
  473. "spreadsheet_token": spreadsheet_token,
  474. "sheet_id": sheet_id,
  475. "sheet_name": sheet_name,
  476. "range": num_range,
  477. "query": query,
  478. "user_id_type": user_id_type,
  479. }
  480. res = self._send_request(url, method="GET", params=params)
  481. return res.get("data")