该提交实现了完整的钉钉聊天渠道插件,包含: 1. 基础配置、账号管理与凭证校验 2. WebSocket长连接网关与消息去重 3. 消息接收/解析/分发与安全校验 4. 媒体文件上传下载与缓存 5. 互动卡片流式更新与回调处理 6. 群管理、命令支持与诊断工具 7. 完整的插件元数据与依赖声明
174 lines
5.1 KiB
Python
174 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
import uuid
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
CREATE_AND_DELIVER_URL = "https://api.dingtalk.com/v1.0/card/instances/createAndDeliver"
|
|
STREAMING_UPDATE_URL = "https://api.dingtalk.com/v1.0/card/streamingUpdate"
|
|
|
|
|
|
class DingTalkCardManager:
|
|
def __init__(self, gateway, http: httpx.AsyncClient | None = None):
|
|
self._gateway = gateway
|
|
self._http = http
|
|
self._card_instance_id: str | None = None
|
|
self._out_track_id: str | None = None
|
|
|
|
def set_http(self, http: httpx.AsyncClient) -> None:
|
|
self._http = http
|
|
|
|
async def _get_http(self) -> httpx.AsyncClient:
|
|
if self._http is not None:
|
|
return self._http
|
|
client = httpx.AsyncClient(timeout=15.0)
|
|
self._http = client
|
|
return client
|
|
|
|
async def create_and_deliver(
|
|
self,
|
|
open_conversation_id: str,
|
|
robot_code: str,
|
|
card_template_id: str,
|
|
title: str = "AI 正在思考...",
|
|
) -> bool:
|
|
token = await self._gateway.token_manager.get_access_token()
|
|
if not token:
|
|
return False
|
|
|
|
self._out_track_id = str(uuid.uuid4())
|
|
|
|
body = {
|
|
"cardTemplateId": card_template_id,
|
|
"outTrackId": self._out_track_id,
|
|
"robotCode": robot_code,
|
|
"openConversationId": open_conversation_id,
|
|
"cardData": json.dumps({"title": title, "content": ""}),
|
|
"imGroupOpenDeliverModel": {"robotCode": robot_code},
|
|
"callbackType": "STREAM",
|
|
}
|
|
|
|
http = await self._get_http()
|
|
resp = await http.post(
|
|
CREATE_AND_DELIVER_URL,
|
|
headers={
|
|
"x-acs-dingtalk-access-token": token,
|
|
"Content-Type": "application/json",
|
|
},
|
|
json=body,
|
|
)
|
|
data = resp.json()
|
|
self._card_instance_id = data.get("cardInstanceId")
|
|
return bool(self._card_instance_id)
|
|
|
|
async def streaming_update(self, content: str, status: str = "PROCESSING") -> bool:
|
|
if not self._card_instance_id:
|
|
return False
|
|
|
|
token = await self._gateway.token_manager.get_access_token()
|
|
if not token:
|
|
return False
|
|
|
|
body = {
|
|
"outTrackId": self._out_track_id,
|
|
"cardInstanceId": self._card_instance_id,
|
|
"content": content,
|
|
"fullContent": False,
|
|
"flowStatus": status,
|
|
}
|
|
|
|
http = await self._get_http()
|
|
resp = await http.put(
|
|
STREAMING_UPDATE_URL,
|
|
headers={
|
|
"x-acs-dingtalk-access-token": token,
|
|
"Content-Type": "application/json",
|
|
},
|
|
json=body,
|
|
)
|
|
return resp.status_code == 200
|
|
|
|
|
|
def build_approval_card_buttons(request_id: str) -> list[dict]:
|
|
return [
|
|
{
|
|
"title": "批准",
|
|
"actionURL": f"dingtalk://yuxi/approval/approve?req={request_id}",
|
|
},
|
|
{
|
|
"title": "拒绝",
|
|
"actionURL": f"dingtalk://yuxi/approval/deny?req={request_id}",
|
|
},
|
|
]
|
|
|
|
|
|
def build_action_card(title: str, text: str, buttons: list[dict], btn_orientation: str = "1") -> dict:
|
|
return {
|
|
"msgtype": "actionCard",
|
|
"actionCard": {
|
|
"title": title,
|
|
"text": text,
|
|
"btnOrientation": btn_orientation,
|
|
"btns": buttons,
|
|
},
|
|
}
|
|
|
|
|
|
def build_markdown_message(title: str, text: str) -> dict:
|
|
return {
|
|
"msgtype": "markdown",
|
|
"markdown": {
|
|
"title": title,
|
|
"text": text,
|
|
},
|
|
}
|
|
|
|
|
|
def build_image_reply_button(image_url: str, prompt_en: str = "") -> dict | None:
|
|
if not image_url or not prompt_en:
|
|
return None
|
|
return {
|
|
"title": "查看原图",
|
|
"actionURL": image_url,
|
|
}
|
|
|
|
|
|
def apply_ai_card_monkey_patch():
|
|
try:
|
|
from dingtalk_stream import AICardReplier, AICardStatus, CardReplier
|
|
|
|
class CustomAICardReplier(CardReplier):
|
|
async def start(self, card_instance_id: str) -> None:
|
|
self._card_instance_id = card_instance_id
|
|
await self._update_card(
|
|
card_instance_id,
|
|
{
|
|
"flowStatus": AICardStatus.PROCESSING,
|
|
"content": "",
|
|
},
|
|
)
|
|
|
|
async def update_content(self, content: str) -> None:
|
|
await self._update_card(
|
|
self._card_instance_id,
|
|
{
|
|
"content": content,
|
|
"flowStatus": AICardStatus.PROCESSING,
|
|
},
|
|
)
|
|
|
|
async def finish(self) -> None:
|
|
await self._update_card(
|
|
self._card_instance_id,
|
|
{"flowStatus": AICardStatus.FINISHED},
|
|
)
|
|
|
|
AICardReplier.start = CustomAICardReplier.start
|
|
logger.info("DingTalk AICardReplier monkey patch applied")
|
|
except ImportError:
|
|
logger.debug("dingtalk_stream SDK not available, skipping monkey patch")
|