该提交实现了完整的ClickUp聊天渠道插件,包含以下核心功能: 1. 基础的@提及提取与格式化能力 2. 账号配对与会话管理 3. 消息流与流式回复支持 4. 重试限流与消息去重 5. 富文本格式转换与内容 sanitize 6. 安全策略与配置校验 7. Webhook接收与自动轮询回退 8. 消息收发、编辑、删除与回复 9. 频道与私信管理、反应功能 10. 完整的状态监控与健康检查
160 lines
6.9 KiB
Python
160 lines
6.9 KiB
Python
import logging
|
||
import os
|
||
|
||
from yuxi.channel.extensions.clickup.types import ClickUpAccount
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
ENV_MAP = {
|
||
"api_token": "CLICKUP_API_TOKEN",
|
||
"workspace_id": "CLICKUP_WORKSPACE_ID",
|
||
"webhook_secret": "CLICKUP_WEBHOOK_SECRET",
|
||
"auth_mode": "CLICKUP_AUTH_MODE",
|
||
"oauth_client_id": "CLICKUP_OAUTH_CLIENT_ID",
|
||
"oauth_client_secret": "CLICKUP_OAUTH_CLIENT_SECRET",
|
||
"dm_policy": "CLICKUP_DM_POLICY",
|
||
"allow_from": "CLICKUP_ALLOW_FROM",
|
||
"channel_policy": "CLICKUP_CHANNEL_POLICY",
|
||
"poll_fallback_enabled": "CLICKUP_POLL_FALLBACK_ENABLED",
|
||
"poll_interval": "CLICKUP_POLL_INTERVAL",
|
||
}
|
||
|
||
|
||
def _env_or_config(key: str) -> str | None:
|
||
env_key = ENV_MAP.get(key, "")
|
||
if env_key:
|
||
return os.getenv(env_key)
|
||
return None
|
||
|
||
|
||
class ClickUpConfigAdapter:
|
||
def list_account_ids(self, config: dict) -> list[str]:
|
||
accounts = config.get("accounts", {})
|
||
if not accounts:
|
||
return ["default"] if self._env_token_exists("default") else []
|
||
return list(accounts.keys())
|
||
|
||
async def resolve_account(self, account_id: str) -> dict:
|
||
account = self._build_account(account_id, {})
|
||
return {
|
||
"account_id": account.account_id,
|
||
"api_token": account.api_token,
|
||
"workspace_id": account.workspace_id,
|
||
"webhook_secret": account.webhook_secret,
|
||
"auth_mode": account.auth_mode,
|
||
"oauth_client_id": account.oauth_client_id,
|
||
"oauth_client_secret": account.oauth_client_secret,
|
||
"name": account.name,
|
||
"dm_policy": account.dm_policy,
|
||
"allow_from": account.allow_from,
|
||
"channel_policy": account.channel_policy,
|
||
"channel_allowlist": account.channel_allowlist,
|
||
"poll_fallback_enabled": account.poll_fallback_enabled,
|
||
"poll_interval": account.poll_interval,
|
||
"enabled": True,
|
||
}
|
||
|
||
def is_configured(self, account: dict) -> bool:
|
||
return bool(account.get("api_token") and account.get("workspace_id"))
|
||
|
||
def is_enabled(self, account: dict) -> bool:
|
||
return account.get("enabled", True)
|
||
|
||
def describe_account(self, account: dict) -> dict:
|
||
return {
|
||
"account_id": account.get("account_id", ""),
|
||
"workspace_id": account.get("workspace_id", ""),
|
||
"dm_policy": account.get("dm_policy", "open"),
|
||
"channel_policy": account.get("channel_policy", "activate_on_mention"),
|
||
"configured": self.is_configured(account),
|
||
}
|
||
|
||
def default_account_id(self, config: dict) -> str:
|
||
return config.get("default_account", "default")
|
||
|
||
def resolve_allow_from(self, config: dict, account_id: str | None = None) -> list[str]:
|
||
aid = account_id or self.default_account_id(config)
|
||
accounts = config.get("accounts", {})
|
||
account = accounts.get(aid, {})
|
||
return account.get("allow_from", [])
|
||
|
||
def config_schema(self) -> dict:
|
||
return {
|
||
"$schema": "https://json-schema.org/draft-07/schema#",
|
||
"type": "object",
|
||
"title": "ClickUp Chat 渠道配置",
|
||
"properties": {
|
||
"api_token": {
|
||
"type": "string",
|
||
"title": "Personal API Token",
|
||
"description": "ClickUp Personal API Token(pk_ 前缀)。在 ClickUp → Settings → Apps 中生成。",
|
||
"x-ui-password": True,
|
||
},
|
||
"workspace_id": {
|
||
"type": "string",
|
||
"title": "Workspace ID",
|
||
"description": "ClickUp Workspace/Team ID(数字)。可通过 GET /api/v2/team 获取。",
|
||
},
|
||
"webhook_secret": {
|
||
"type": "string",
|
||
"title": "Webhook Secret",
|
||
"description": "用于验证 Automation Webhook 的自定义密钥(可选但强烈推荐)。",
|
||
"x-ui-password": True,
|
||
},
|
||
"dm_policy": {
|
||
"type": "string",
|
||
"title": "DM 安全策略",
|
||
"enum": ["open", "pairing", "allowlist", "disabled"],
|
||
"default": "open",
|
||
},
|
||
"allow_from": {
|
||
"type": "string",
|
||
"title": "DM 白名单",
|
||
"description": "逗号分隔的 ClickUp user_id 列表。仅 dm_policy=allowlist 时生效。",
|
||
},
|
||
"channel_policy": {
|
||
"type": "string",
|
||
"title": "Channel 响应策略",
|
||
"enum": ["always", "activate_on_mention", "disabled"],
|
||
"default": "activate_on_mention",
|
||
},
|
||
"poll_fallback_enabled": {
|
||
"type": "boolean",
|
||
"title": "启用轮询回退",
|
||
"description": "Automation Webhook 不可用时,通过拉取消息列表作为回退方案(P2)。",
|
||
"default": False,
|
||
},
|
||
},
|
||
}
|
||
|
||
def _build_account(self, account_id: str, raw: dict) -> ClickUpAccount:
|
||
allow_from_str = _env_or_config("allow_from") or raw.get("allow_from", "")
|
||
if isinstance(allow_from_str, list):
|
||
allow_from_list = allow_from_str
|
||
else:
|
||
allow_from_list = [u.strip() for u in allow_from_str.split(",") if u.strip()]
|
||
|
||
poll_enabled_str = _env_or_config("poll_fallback_enabled") or str(raw.get("poll_fallback_enabled", False))
|
||
|
||
return ClickUpAccount(
|
||
account_id=account_id,
|
||
api_token=_env_or_config("api_token") or raw.get("api_token", ""),
|
||
workspace_id=_env_or_config("workspace_id") or raw.get("workspace_id", ""),
|
||
webhook_secret=_env_or_config("webhook_secret") or raw.get("webhook_secret", ""),
|
||
auth_mode=_env_or_config("auth_mode") or raw.get("auth_mode", "api_token"),
|
||
oauth_client_id=_env_or_config("oauth_client_id") or raw.get("oauth_client_id", ""),
|
||
oauth_client_secret=_env_or_config("oauth_client_secret") or raw.get("oauth_client_secret", ""),
|
||
name=raw.get("name", account_id),
|
||
dm_policy=_env_or_config("dm_policy") or raw.get("dm_policy", "open"),
|
||
allow_from=allow_from_list,
|
||
channel_policy=_env_or_config("channel_policy") or raw.get("channel_policy", "activate_on_mention"),
|
||
channel_allowlist=raw.get("channel_allowlist", []),
|
||
poll_fallback_enabled=poll_enabled_str.lower() == "true",
|
||
poll_interval=float(_env_or_config("poll_interval") or raw.get("poll_interval", "15.0")),
|
||
)
|
||
|
||
@staticmethod
|
||
def _env_token_exists(account_id: str) -> bool:
|
||
key = f"CLICKUP_API_TOKEN_{account_id.upper()}"
|
||
return bool(os.environ.get(key, "") or os.environ.get("CLICKUP_API_TOKEN", ""))
|