新增 Webex、微信 iLink、微信小程序三个渠道扩展。 Webex 渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - media: 媒体资源处理 微信 iLink 渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - context_store: 上下文存储 - aes_ecb: AES-ECB 加解密 - media: 媒体资源处理 - typing: 输入状态 微信小程序渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - crypto: 加解密处理 - dedupe: 消息去重 - message: 消息处理 - passive_reply: 被动回复 - media: 媒体资源处理 - status: 会话状态管理
342 lines
10 KiB
Python
342 lines
10 KiB
Python
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}
|