feishu_api_utils.py 18 KB

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