ForcePilot/backend/package/yuxi/channel/extensions/webex/outbound.py

342 lines
10 KiB
Python
Raw Normal View History

import asyncio
MAX_TEXT_BYTES = 7400
class WebexOutbound:
def __init__(self, api):
self._api = api
@staticmethod
def build_mention(person_id: str = "", person_email: str = "", display_name: str = "") -> str:
if person_email:
return f"<@personEmail:{person_email}>"
if person_id:
return f"<@personId:{person_id}>"
if display_name:
return f"**{display_name}**"
return ""
@staticmethod
def welcome_card_template(display_name: str = "", bot_name: str = "ForcePilot") -> dict:
greeting = f"你好 {display_name}" if display_name else "你好!"
return {
"type": "AdaptiveCard",
"version": "1.3",
"body": [
{
"type": "TextBlock",
"text": f"👋 {greeting}",
"size": "Large",
"weight": "Bolder",
},
{
"type": "TextBlock",
"text": f"我是 **{bot_name}**,基于 Cisco Webex 的智能助手,有什么可以帮助你的?",
"wrap": True,
},
{
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"width": "auto",
"items": [
{
"type": "TextBlock",
"text": "🚀 可以尝试问我:",
"wrap": True,
},
{
"type": "TextBlock",
"text": "- 知识查询与解答\n- 数据分析与处理\n- 任务提醒与通知",
"wrap": True,
"spacing": "Small",
},
],
}
],
},
],
}
@staticmethod
def confirm_card_template(
title: str, message: str, confirm_label: str = "确认", cancel_label: str = "取消"
) -> dict:
return {
"type": "AdaptiveCard",
"version": "1.3",
"body": [
{
"type": "TextBlock",
"text": title,
"size": "Medium",
"weight": "Bolder",
},
{
"type": "TextBlock",
"text": message,
"wrap": True,
},
],
"actions": [
{
"type": "Action.Submit",
"title": confirm_label,
"data": {"action": "confirm"},
},
{
"type": "Action.Submit",
"title": cancel_label,
"style": "default",
"data": {"action": "cancel"},
},
],
}
async def send_text(
self,
room_id: str,
content: str,
*,
parent_id: str | None = None,
as_markdown: bool = True,
mentions: list[str] | None = None,
to_person_id: str | None = None,
to_person_email: str | None = None,
) -> list[str]:
text = content
if mentions:
text = " ".join(mentions) + " " + text
chunks = self._chunk_text(text)
sent_ids = []
for i, chunk in enumerate(chunks):
pid = parent_id if i == 0 else None
kwargs = self._build_target_kwargs(room_id, to_person_id, to_person_email)
if pid:
kwargs["parentId"] = pid
if as_markdown:
kwargs["markdown"] = chunk
else:
kwargs["text"] = chunk
msg = self._api.messages.create(**kwargs)
sent_ids.append(msg.id)
if i < len(chunks) - 1:
await asyncio.sleep(0.1)
return sent_ids
async def send_html(
self,
room_id: str,
content: str,
*,
parent_id: str | None = None,
to_person_id: str | None = None,
to_person_email: str | None = None,
) -> str:
kwargs = self._build_target_kwargs(room_id, to_person_id, to_person_email)
if parent_id:
kwargs["parentId"] = parent_id
kwargs["html"] = content
msg = self._api.messages.create(**kwargs)
return msg.id
async def send_media(
self,
room_id: str,
media_url: str,
*,
parent_id: str | None = None,
to_person_id: str | None = None,
to_person_email: str | None = None,
) -> str:
kwargs = self._build_target_kwargs(room_id, to_person_id, to_person_email)
kwargs["files"] = [media_url]
if parent_id:
kwargs["parentId"] = parent_id
msg = self._api.messages.create(**kwargs)
return msg.id
async def send_card(
self,
room_id: str,
card_json: dict,
*,
parent_id: str | None = None,
to_person_id: str | None = None,
to_person_email: str | None = None,
) -> str:
kwargs = self._build_target_kwargs(room_id, to_person_id, to_person_email)
kwargs["attachments"] = [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card_json,
}
]
if parent_id:
kwargs["parentId"] = parent_id
msg = self._api.messages.create(**kwargs)
return msg.id
async def edit_message(
self,
message_id: str,
room_id: str,
new_content: str,
*,
as_markdown: bool = True,
) -> str:
kwargs = {"roomId": room_id}
if as_markdown:
kwargs["markdown"] = new_content
else:
kwargs["text"] = new_content
updated = self._api.messages.update(message_id, **kwargs)
return updated.id
async def delete_message(self, message_id: str) -> bool:
try:
self._api.messages.delete(message_id)
return True
except Exception:
return False
async def list_messages(
self,
room_id: str,
*,
parent_id: str | None = None,
before: str | None = None,
max_results: int = 50,
) -> list[dict]:
try:
params = {"roomId": room_id, "max": min(max_results, 100)}
if parent_id:
params["parentId"] = parent_id
if before:
params["before"] = before
messages = list(self._api.messages.list(**params))
return [
{
"id": m.id,
"text": m.text,
"person_id": m.personId,
"person_email": m.personEmail,
"created": m.created,
"parent_id": m.parentId,
}
for m in messages
]
except Exception:
return []
async def get_message(self, message_id: str) -> dict | None:
try:
m = self._api.messages.get(message_id)
return {
"id": m.id,
"text": m.text,
"markdown": m.markdown,
"html": m.html,
"person_id": m.personId,
"person_email": m.personEmail,
"room_id": m.roomId,
"parent_id": m.parentId,
"created": m.created,
"files": m.files,
}
except Exception:
return None
async def get_room(self, room_id: str) -> dict | None:
try:
r = self._api.rooms.get(room_id)
return {
"id": r.id,
"title": r.title,
"type": r.type,
"is_locked": getattr(r, "isLocked", False),
"created": r.created,
}
except Exception:
return None
async def list_members(self, room_id: str) -> list[dict]:
try:
members = list(self._api.memberships.list(roomId=room_id))
return [
{
"id": m.id,
"person_id": m.personId,
"person_email": m.personEmail,
"person_display_name": m.personDisplayName,
"room_id": m.roomId,
"is_moderator": getattr(m, "isModerator", False),
"created": m.created,
}
for m in members
]
except Exception:
return []
async def get_member(self, membership_id: str) -> dict | None:
try:
m = self._api.memberships.get(membership_id)
return {
"id": m.id,
"person_id": m.personId,
"person_email": m.personEmail,
"person_display_name": m.personDisplayName,
"room_id": m.roomId,
"is_moderator": getattr(m, "isModerator", False),
"created": m.created,
}
except Exception:
return None
def _chunk_text(self, content: str) -> list[str]:
encoded = content.encode("utf-8")
if len(encoded) <= MAX_TEXT_BYTES:
return [content]
chunks = []
pos = 0
while pos < len(encoded):
end = min(pos + MAX_TEXT_BYTES, len(encoded))
chunk_bytes = encoded[pos:end]
try:
chunk = chunk_bytes.decode("utf-8")
except UnicodeDecodeError as e:
cut = e.start
chunk_bytes = encoded[pos : pos + cut]
chunk = chunk_bytes.decode("utf-8")
chunks.append(chunk)
if chunk_bytes:
pos += len(chunk_bytes)
else:
pos += MAX_TEXT_BYTES
return chunks
@staticmethod
def _build_target_kwargs(
room_id: str,
to_person_id: str | None = None,
to_person_email: str | None = None,
) -> dict:
if to_person_id:
return {"toPersonId": to_person_id}
if to_person_email:
return {"toPersonEmail": to_person_email}
return {"roomId": room_id}