diff --git a/backend/package/yuxi/channels/adapters/feishu/tools/calendar.py b/backend/package/yuxi/channels/adapters/feishu/tools/calendar.py new file mode 100644 index 00000000..f6c6d0fe --- /dev/null +++ b/backend/package/yuxi/channels/adapters/feishu/tools/calendar.py @@ -0,0 +1,168 @@ +from __future__ import annotations + +from typing import Any + +from ._validate import validate_params + +try: + import lark_oapi + + HAS_LARK_SDK = True +except ImportError: + HAS_LARK_SDK = False + lark_oapi = None # type: ignore + + +@validate_params +async def list_calendar_events( + client: Any, + calendar_id: str = "primary", + start_time: str = "", + end_time: str = "", + page_size: int = 50, + page_token: str = "", +) -> dict[str, Any]: + if not HAS_LARK_SDK or not client: + return {"events": [], "has_more": False, "page_token": ""} + + try: + builder = ( + lark_oapi.api.calendar.v4.ListCalendarEventRequest.builder() + .calendar_id(calendar_id) + .page_size(min(page_size, 100)) + ) + if start_time: + builder.start_time(start_time) + if end_time: + builder.end_time(end_time) + if page_token: + builder.page_token(page_token) + + request = builder.build() + resp = await client.calendar.v4.calendar_event.list(request) + if not resp.success(): + raise RuntimeError(f"\u67e5\u8be2\u65e5\u5386\u4e8b\u4ef6\u5931\u8d25: {resp.msg}") + + data = resp.data if hasattr(resp, "data") else {} + items = data.get("items", []) + events = [] + for item in items: + events.append( + { + "event_id": item.get("event_id", ""), + "summary": item.get("summary", ""), + "description": item.get("description", ""), + "start_time": _format_event_time(item.get("start_time")), + "end_time": _format_event_time(item.get("end_time")), + "location": item.get("location", ""), + "organizer": _get_display_name(item.get("organizer")), + "status": item.get("status", ""), + } + ) + + return { + "events": events, + "has_more": data.get("has_more", False), + "page_token": data.get("page_token", ""), + "sync_token": data.get("sync_token", ""), + } + except Exception as e: + raise RuntimeError(f"\u67e5\u8be2\u65e5\u5386\u4e8b\u4ef6\u5931\u8d25: {e}") from e + + +@validate_params +async def get_calendar_event( + client: Any, + calendar_id: str, + event_id: str, +) -> dict[str, Any]: + if not HAS_LARK_SDK or not client: + raise RuntimeError("SDK \u4e0d\u53ef\u7528") + + try: + request = ( + lark_oapi.api.calendar.v4.GetCalendarEventRequest.builder() + .calendar_id(calendar_id) + .event_id(event_id) + .build() + ) + resp = await client.calendar.v4.calendar_event.get(request) + if not resp.success(): + raise RuntimeError(f"\u83b7\u53d6\u65e5\u5386\u4e8b\u4ef6\u5931\u8d25: {resp.msg}") + + data = resp.data if hasattr(resp, "data") else {} + item = data.get("event", data) + return { + "event_id": item.get("event_id", ""), + "summary": item.get("summary", ""), + "description": item.get("description", ""), + "start_time": _format_event_time(item.get("start_time")), + "end_time": _format_event_time(item.get("end_time")), + "location": item.get("location", ""), + "organizer": _get_display_name(item.get("organizer")), + "attendees": [ + _get_display_name(a) for a in (item.get("attendees") or []) + ], + "status": item.get("status", ""), + } + except Exception as e: + raise RuntimeError(f"\u83b7\u53d6\u65e5\u5386\u4e8b\u4ef6\u5931\u8d25: {e}") from e + + +@validate_params +async def list_calendars( + client: Any, + page_size: int = 50, + page_token: str = "", +) -> dict[str, Any]: + if not HAS_LARK_SDK or not client: + return {"calendars": [], "has_more": False, "page_token": ""} + + try: + builder = ( + lark_oapi.api.calendar.v4.ListCalendarRequest.builder() + .page_size(min(page_size, 100)) + ) + if page_token: + builder.page_token(page_token) + + request = builder.build() + resp = await client.calendar.v4.calendar.list(request) + if not resp.success(): + raise RuntimeError(f"\u67e5\u8be2\u65e5\u5386\u5217\u8868\u5931\u8d25: {resp.msg}") + + data = resp.data if hasattr(resp, "data") else {} + items = data.get("calendar_list", []) + calendars = [] + for item in items: + calendars.append( + { + "calendar_id": item.get("calendar_id", ""), + "summary": item.get("summary", ""), + "description": item.get("description", ""), + } + ) + + return { + "calendars": calendars, + "has_more": data.get("has_more", False), + "page_token": data.get("page_token", ""), + } + except Exception as e: + raise RuntimeError(f"\u67e5\u8be2\u65e5\u5386\u5217\u8868\u5931\u8d25: {e}") from e + + +def _format_event_time(ts: Any) -> str: + if ts is None: + return "" + if isinstance(ts, dict): + return ts.get("date_time") or ts.get("date") or "" + return str(ts) + + +def _get_display_name(obj: Any) -> str: + if obj is None: + return "" + if isinstance(obj, dict): + return obj.get("display_name") or obj.get("name") or "" + return str(obj) diff --git a/backend/package/yuxi/channels/adapters/feishu/tools/directory.py b/backend/package/yuxi/channels/adapters/feishu/tools/directory.py new file mode 100644 index 00000000..cede6174 --- /dev/null +++ b/backend/package/yuxi/channels/adapters/feishu/tools/directory.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Any + +from ._validate import validate_params + +try: + import lark_oapi + + HAS_LARK_SDK = True +except ImportError: + HAS_LARK_SDK = False + lark_oapi = None # type: ignore + + +@validate_params +async def search_users( + client: Any, + query: str, + page_size: int = 20, + page_token: str = "", +) -> dict[str, Any]: + if not HAS_LARK_SDK or not client: + return {"users": [], "has_more": False, "page_token": ""} + + try: + request = ( + lark_oapi.api.contact.v3.SearchUserRequest.builder() + .query(query) + .page_size(min(page_size, 100)) + .page_token(page_token) + .build() + ) + resp = await client.contact.v3.user.search(request) + if not resp.success(): + raise RuntimeError(f"搜索用户失败: {resp.msg}") + + data = resp.data if hasattr(resp, "data") else {} + items = data.get("items", []) + users = [ + { + "open_id": item.get("open_id", ""), + "user_id": item.get("user_id", ""), + "name": item.get("name", ""), + "email": item.get("email", ""), + "avatar_url": ( + item.get("avatar", {}).get("avatar_240", "") if isinstance(item.get("avatar"), dict) else "" + ), + } + for item in items + ] + + return { + "users": users, + "has_more": data.get("has_more", False), + "page_token": data.get("page_token", ""), + } + except Exception as e: + raise RuntimeError(f"搜索用户失败: {e}") from e