新增京东(JD)渠道扩展,支持在 Yuxi 平台中集成京东客服渠道。 包含以下功能模块: - client: 京东 API 客户端封装 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - pairing: 用户配对与绑定 - security: 安全校验 - signature: 请求签名验证 - crypto: 加解密处理 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - business: 业务逻辑处理 - types: 类型定义
148 lines
4.9 KiB
Python
148 lines
4.9 KiB
Python
import json
|
|
import logging
|
|
|
|
from yuxi.channel.extensions.jd.client import JOSClient
|
|
from yuxi.channel.extensions.jd.types import JD_TEXT_MAX_LENGTH
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class JDOutbound:
|
|
def __init__(self, gateway=None):
|
|
self._gateway = gateway
|
|
self._client: JOSClient | None = None
|
|
|
|
async def send_text(
|
|
self,
|
|
target_id: str,
|
|
content: str,
|
|
*,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
account_id: str | None = None,
|
|
chat_id: str | None = None,
|
|
) -> None:
|
|
if not content:
|
|
return
|
|
|
|
if len(content) > JD_TEXT_MAX_LENGTH:
|
|
content = content[: JD_TEXT_MAX_LENGTH - 3] + "..."
|
|
|
|
await self._send_msg(target_id, content, msg_type=1, chat_id=chat_id)
|
|
|
|
async def send_media(
|
|
self,
|
|
target_id: str,
|
|
media_url: str,
|
|
media_type: str,
|
|
reply_to_id: str | None = None,
|
|
thread_id: str | None = None,
|
|
chat_id: str | None = None,
|
|
) -> None:
|
|
if media_type != "image":
|
|
logger.warning("JD only supports image, got media_type=%s", media_type)
|
|
return
|
|
|
|
img_content = json.dumps({"url": media_url}, ensure_ascii=False)
|
|
await self._send_msg(target_id, img_content, msg_type=2, chat_id=chat_id)
|
|
|
|
async def send_image(self, to_user: str, media_url: str, *, chat_id: str | None = None) -> bool:
|
|
img_content = json.dumps({"url": media_url}, ensure_ascii=False)
|
|
return await self._send_msg(to_user, img_content, msg_type=2, chat_id=chat_id)
|
|
|
|
async def send_link(
|
|
self,
|
|
target_id: str,
|
|
url: str,
|
|
text: str,
|
|
*,
|
|
chat_id: str | None = None,
|
|
) -> bool:
|
|
link_content = json.dumps({"url": url, "text": text}, ensure_ascii=False)
|
|
return await self._send_msg(target_id, link_content, msg_type=3, chat_id=chat_id)
|
|
|
|
async def send_product_card(
|
|
self,
|
|
target_id: str,
|
|
url: str,
|
|
pid: str,
|
|
*,
|
|
chat_id: str | None = None,
|
|
) -> bool:
|
|
card_content = json.dumps({"url": url, "pid": pid}, ensure_ascii=False)
|
|
return await self._send_msg(target_id, card_content, msg_type=4, chat_id=chat_id)
|
|
|
|
async def _send_msg(self, to_user: str, content: str, msg_type: int = 1, *, chat_id: str | None = None) -> bool:
|
|
gateway = self._gateway
|
|
if gateway is None or not (token := gateway.get_token()) or not (account := gateway.account):
|
|
logger.error("No JD gateway/token/account available")
|
|
return False
|
|
|
|
if self._client is None:
|
|
self._client = JOSClient(
|
|
app_key=account.app_key,
|
|
app_secret=account.app_secret,
|
|
shop_id=account.shop_id,
|
|
)
|
|
|
|
biz_params = {
|
|
"chat_id": chat_id or "",
|
|
"from_id": account.shop_id,
|
|
"to_id": to_user,
|
|
"msg_type": msg_type,
|
|
"content": content,
|
|
}
|
|
|
|
try:
|
|
data = await self._client.call("jingdong.im.sendMsg", biz_params, token)
|
|
result = data.get("jingdong_im_sendMsg_responce", {}).get("result", {})
|
|
logger.debug("JD message sent: msg_id=%s to=%s", result.get("msg_id", ""), to_user)
|
|
return True
|
|
except Exception:
|
|
logger.exception("JD send_msg failed for user %s", to_user)
|
|
return False
|
|
|
|
async def recall_msg(self, msg_id: str, chat_id: str | None = None) -> bool:
|
|
gateway = self._gateway
|
|
if gateway is None or not (token := gateway.get_token()) or not (account := gateway.account):
|
|
logger.error("No JD gateway/token/account available for recall")
|
|
return False
|
|
|
|
if self._client is None:
|
|
self._client = JOSClient(
|
|
app_key=account.app_key,
|
|
app_secret=account.app_secret,
|
|
shop_id=account.shop_id,
|
|
)
|
|
|
|
biz_params = {"msg_id": msg_id, "chat_id": chat_id or ""}
|
|
try:
|
|
await self._client.call("jingdong.im.recallMsg", biz_params, token)
|
|
logger.debug("JD message recalled: msg_id=%s", msg_id)
|
|
return True
|
|
except Exception:
|
|
logger.exception("JD recall msg failed: msg_id=%s", msg_id)
|
|
return False
|
|
|
|
def chunker(self, text: str, max_chars: int = JD_TEXT_MAX_LENGTH) -> list[str]:
|
|
if len(text) <= max_chars:
|
|
return [text]
|
|
|
|
chunks = []
|
|
while text:
|
|
if len(text) <= max_chars:
|
|
chunks.append(text)
|
|
break
|
|
split_at = text.rfind("\n", 0, max_chars)
|
|
if split_at == -1:
|
|
split_at = max_chars
|
|
chunks.append(text[:split_at])
|
|
text = text[split_at:].lstrip()
|
|
|
|
return chunks
|
|
|
|
async def close(self):
|
|
if self._client:
|
|
await self._client.close()
|
|
self._client = None
|