ForcePilot/backend/package/yuxi/channel/extensions/webex/outbound.py
Kris 7b5d1e5c69 feat(channel): 添加 Webex、微信 iLink 和微信小程序渠道扩展
新增 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: 会话状态管理
2026-05-21 11:59:04 +08:00

342 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}