该提交实现了完整的ClickUp聊天渠道插件,包含以下核心功能: 1. 基础的@提及提取与格式化能力 2. 账号配对与会话管理 3. 消息流与流式回复支持 4. 重试限流与消息去重 5. 富文本格式转换与内容 sanitize 6. 安全策略与配置校验 7. Webhook接收与自动轮询回退 8. 消息收发、编辑、删除与回复 9. 频道与私信管理、反应功能 10. 完整的状态监控与健康检查
218 lines
7.4 KiB
Python
218 lines
7.4 KiB
Python
import logging
|
|
|
|
import httpx
|
|
|
|
from yuxi.channel.extensions.clickup.config import ClickUpConfigAdapter
|
|
from yuxi.channel.extensions.clickup.types import OutboundResult
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
API_V3_BASE = "https://api.clickup.com/api/v3"
|
|
|
|
|
|
async def get_message(
|
|
workspace_id: str,
|
|
message_id: str,
|
|
api_token: str,
|
|
) -> dict | None:
|
|
url = f"{API_V3_BASE}/workspaces/{workspace_id}/chat/messages/{message_id}"
|
|
|
|
headers = {"Authorization": api_token}
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
try:
|
|
resp = await client.get(url, headers=headers)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
logger.error("ClickUp get_message failed: status=%d", resp.status_code)
|
|
return None
|
|
except Exception as e:
|
|
logger.exception("ClickUp get_message exception: %s", e)
|
|
return None
|
|
|
|
|
|
async def list_channel_messages(
|
|
workspace_id: str,
|
|
channel_id: str,
|
|
api_token: str,
|
|
limit: int = 50,
|
|
cursor: str | None = None,
|
|
) -> dict:
|
|
url = f"{API_V3_BASE}/workspaces/{workspace_id}/chat/channels/{channel_id}/messages"
|
|
params: dict = {"limit": min(limit, 100)}
|
|
if cursor:
|
|
params["cursor"] = cursor
|
|
|
|
headers = {"Authorization": api_token}
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
try:
|
|
resp = await client.get(url, params=params, headers=headers)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
logger.error("ClickUp list_channel_messages failed: status=%d", resp.status_code)
|
|
return {"messages": []}
|
|
except Exception as e:
|
|
logger.exception("ClickUp list_channel_messages exception: %s", e)
|
|
return {"messages": []}
|
|
|
|
|
|
async def list_dm_messages(
|
|
workspace_id: str,
|
|
dm_id: str,
|
|
api_token: str,
|
|
limit: int = 50,
|
|
cursor: str | None = None,
|
|
) -> dict:
|
|
url = f"{API_V3_BASE}/workspaces/{workspace_id}/chat/dm/{dm_id}/messages"
|
|
params: dict = {"limit": min(limit, 100)}
|
|
if cursor:
|
|
params["cursor"] = cursor
|
|
|
|
headers = {"Authorization": api_token}
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
try:
|
|
resp = await client.get(url, params=params, headers=headers)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
logger.error("ClickUp list_dm_messages failed: status=%d", resp.status_code)
|
|
return {"messages": []}
|
|
except Exception as e:
|
|
logger.exception("ClickUp list_dm_messages exception: %s", e)
|
|
return {"messages": []}
|
|
|
|
|
|
async def send_channel_message(
|
|
workspace_id: str,
|
|
channel_id: str,
|
|
content: str,
|
|
api_token: str,
|
|
reply_to_id: str | None = None,
|
|
) -> OutboundResult:
|
|
url = f"{API_V3_BASE}/workspaces/{workspace_id}/chat/channels/{channel_id}/messages"
|
|
|
|
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
|
payload: dict = {
|
|
"type": "message",
|
|
"content": content[:40000],
|
|
"content_format": "text/md",
|
|
}
|
|
|
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
try:
|
|
resp = await client.post(url, json=payload, headers=headers)
|
|
if resp.status_code == 201:
|
|
data = resp.json()
|
|
return OutboundResult(success=True, message_id=data.get("id", ""))
|
|
error_text = resp.text[:500]
|
|
logger.error("ClickUp send_channel_message failed: status=%d body=%s", resp.status_code, error_text)
|
|
return OutboundResult(success=False, error=f"http_{resp.status_code}", detail=error_text)
|
|
except Exception as e:
|
|
logger.exception("ClickUp send_channel_message exception: %s", e)
|
|
return OutboundResult(success=False, error="exception", detail=str(e))
|
|
|
|
|
|
async def list_channels(
|
|
workspace_id: str,
|
|
api_token: str,
|
|
cursor: str | None = None,
|
|
) -> dict:
|
|
url = f"{API_V3_BASE}/workspaces/{workspace_id}/chat/channels"
|
|
params: dict = {}
|
|
if cursor:
|
|
params["cursor"] = cursor
|
|
|
|
headers = {"Authorization": api_token}
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
try:
|
|
resp = await client.get(url, params=params, headers=headers)
|
|
if resp.status_code == 200:
|
|
return resp.json()
|
|
logger.error("ClickUp list_channels failed: status=%d", resp.status_code)
|
|
return {"channels": []}
|
|
except Exception as e:
|
|
logger.exception("ClickUp list_channels exception: %s", e)
|
|
return {"channels": []}
|
|
|
|
|
|
async def create_channel(
|
|
workspace_id: str,
|
|
api_token: str,
|
|
name: str,
|
|
*,
|
|
visibility: str = "public",
|
|
member_ids: list[str] | None = None,
|
|
) -> dict:
|
|
url = f"{API_V3_BASE}/workspaces/{workspace_id}/chat/channels"
|
|
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
|
payload: dict = {
|
|
"name": name,
|
|
"visibility": visibility,
|
|
}
|
|
if member_ids:
|
|
payload["members"] = member_ids
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
try:
|
|
resp = await client.post(url, json=payload, headers=headers)
|
|
if resp.status_code in (200, 201):
|
|
return resp.json()
|
|
error_text = resp.text[:500]
|
|
logger.error("ClickUp create_channel failed: status=%d body=%s", resp.status_code, error_text)
|
|
raise RuntimeError(f"创建频道失败 (HTTP {resp.status_code}): {error_text}")
|
|
except Exception:
|
|
logger.exception("ClickUp create_channel exception")
|
|
raise
|
|
|
|
|
|
async def get_channel_members(
|
|
workspace_id: str,
|
|
api_token: str,
|
|
channel_id: str,
|
|
) -> list[dict]:
|
|
url = f"{API_V3_BASE}/workspaces/{workspace_id}/chat/channels/{channel_id}/members"
|
|
headers = {"Authorization": api_token}
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
try:
|
|
resp = await client.get(url, headers=headers)
|
|
if resp.status_code == 200:
|
|
data = resp.json()
|
|
return data.get("members", [])
|
|
error_text = resp.text[:500]
|
|
logger.error("ClickUp get_channel_members failed: status=%d", resp.status_code)
|
|
raise RuntimeError(f"获取频道成员失败 (HTTP {resp.status_code}): {error_text}")
|
|
except Exception:
|
|
logger.exception("ClickUp get_channel_members exception")
|
|
raise
|
|
|
|
|
|
async def create_dm(
|
|
workspace_id: str,
|
|
api_token: str,
|
|
member_ids: list[str],
|
|
) -> dict:
|
|
url = f"{API_V3_BASE}/workspaces/{workspace_id}/chat/dm"
|
|
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
|
payload = {"members": member_ids}
|
|
|
|
async with httpx.AsyncClient(timeout=15.0) as client:
|
|
try:
|
|
resp = await client.post(url, json=payload, headers=headers)
|
|
if resp.status_code in (200, 201):
|
|
return resp.json()
|
|
error_text = resp.text[:500]
|
|
logger.error("ClickUp create_dm failed: status=%d body=%s", resp.status_code, error_text)
|
|
raise RuntimeError(f"创建私信失败 (HTTP {resp.status_code}): {error_text}")
|
|
except Exception:
|
|
logger.exception("ClickUp create_dm exception")
|
|
raise
|
|
|
|
|
|
async def resolve_account(account_id: str | None = None) -> dict | None:
|
|
adapter = ClickUpConfigAdapter()
|
|
aid = account_id or adapter.default_account_id({})
|
|
return await adapter.resolve_account(aid)
|