该提交实现了完整的ClickUp聊天渠道插件,包含以下核心功能: 1. 基础的@提及提取与格式化能力 2. 账号配对与会话管理 3. 消息流与流式回复支持 4. 重试限流与消息去重 5. 富文本格式转换与内容 sanitize 6. 安全策略与配置校验 7. Webhook接收与自动轮询回退 8. 消息收发、编辑、删除与回复 9. 频道与私信管理、反应功能 10. 完整的状态监控与健康检查
1087 lines
42 KiB
Python
1087 lines
42 KiB
Python
import logging
|
||
|
||
import httpx
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
API_V2_BASE = "https://api.clickup.com/api/v2"
|
||
|
||
|
||
def build_clickup_agent_tools(account: dict) -> list[dict]:
|
||
_ = account.get("workspace_id", "")
|
||
return [
|
||
{
|
||
"name": "clickup_create_task",
|
||
"description": "在 ClickUp 列表中创建新任务。需要 list_id、任务名称和可选的描述、优先级、负责人等信息。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"list_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 列表 ID(数字字符串)。可通过 clickup_get_lists 工具获取。",
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"description": "任务名称/标题",
|
||
},
|
||
"description": {
|
||
"type": "string",
|
||
"description": "任务描述(支持 Markdown),可选",
|
||
},
|
||
"priority": {
|
||
"type": "integer",
|
||
"description": "任务优先级:1=紧急, 2=高, 3=正常, 4=低。默认 3(正常)",
|
||
"default": 3,
|
||
},
|
||
"assignees": {
|
||
"type": "array",
|
||
"items": {"type": "string"},
|
||
"description": "负责人用户 ID 列表,可选",
|
||
},
|
||
"tags": {
|
||
"type": "array",
|
||
"items": {"type": "string"},
|
||
"description": "标签名称列表,可选",
|
||
},
|
||
"due_date": {
|
||
"type": "string",
|
||
"description": "截止日期时间(Unix 毫秒时间戳字符串),可选",
|
||
},
|
||
},
|
||
"required": ["list_id", "name"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_get_task",
|
||
"description": "获取 ClickUp 任务的详细信息,包括状态、负责人、描述、评论等。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID(如 #task_id 或 9 位数字字符串)",
|
||
},
|
||
},
|
||
"required": ["task_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_update_task",
|
||
"description": "更新 ClickUp 任务的信息,如名称、描述、状态、优先级、负责人等。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID",
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"description": "新的任务名称,可选",
|
||
},
|
||
"description": {
|
||
"type": "string",
|
||
"description": "新的任务描述(支持 Markdown),可选",
|
||
},
|
||
"status": {
|
||
"type": "string",
|
||
"description": "新的任务状态名称,可选",
|
||
},
|
||
"priority": {
|
||
"type": "integer",
|
||
"description": "新的优先级:1=紧急, 2=高, 3=正常, 4=低,可选",
|
||
},
|
||
"assignees": {
|
||
"type": "object",
|
||
"properties": {
|
||
"add": {
|
||
"type": "array",
|
||
"items": {"type": "string"},
|
||
"description": "添加的负责人 ID 列表",
|
||
},
|
||
"rem": {
|
||
"type": "array",
|
||
"items": {"type": "string"},
|
||
"description": "移除的负责人 ID 列表",
|
||
},
|
||
},
|
||
"description": "负责人变更,可选",
|
||
},
|
||
},
|
||
"required": ["task_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_list_tasks",
|
||
"description": "获取 ClickUp 列表中的任务列表,支持筛选状态和分页。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"list_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 列表 ID",
|
||
},
|
||
"status": {
|
||
"type": "string",
|
||
"description": "按状态筛选(如 'open', 'in progress', 'closed'),可选",
|
||
},
|
||
"page": {
|
||
"type": "integer",
|
||
"description": "分页页码(从 0 开始)",
|
||
"default": 0,
|
||
},
|
||
"limit": {
|
||
"type": "integer",
|
||
"description": "每页任务数(最大 100)",
|
||
"default": 25,
|
||
},
|
||
},
|
||
"required": ["list_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_search_tasks",
|
||
"description": "在 ClickUp Workspace 中搜索任务。可通过关键词、状态、负责人等条件搜索。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"query": {
|
||
"type": "string",
|
||
"description": "搜索关键词",
|
||
},
|
||
"status": {
|
||
"type": "string",
|
||
"description": "按状态筛选,可选",
|
||
},
|
||
"assignee": {
|
||
"type": "string",
|
||
"description": "按负责人 ID 筛选,可选",
|
||
},
|
||
"list_id": {
|
||
"type": "string",
|
||
"description": "限定搜索的列表 ID,可选",
|
||
},
|
||
},
|
||
"required": ["query"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_add_task_comment",
|
||
"description": "为 ClickUp 任务添加评论。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID",
|
||
},
|
||
"content": {
|
||
"type": "string",
|
||
"description": "评论内容(支持 Markdown)",
|
||
},
|
||
},
|
||
"required": ["task_id", "content"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_get_task_comments",
|
||
"description": "获取 ClickUp 任务的评论列表。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID",
|
||
},
|
||
},
|
||
"required": ["task_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_get_lists",
|
||
"description": "获取 ClickUp Workspace 中的列表清单,用于确定任务操作的 list_id。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"space_id": {
|
||
"type": "string",
|
||
"description": "限定 Space ID 筛选,可选。不提供则获取整个 Workspace 的列表。",
|
||
},
|
||
},
|
||
"required": [],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_delete_task",
|
||
"description": "删除 ClickUp 任务。删除后无法恢复,请谨慎操作。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID(如 #task_id 或 9 位数字字符串)",
|
||
},
|
||
},
|
||
"required": ["task_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_get_spaces",
|
||
"description": "获取 ClickUp Workspace 中所有 Space 的列表,用于了解项目结构并定位 space_id。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {},
|
||
"required": [],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_create_list",
|
||
"description": "在 ClickUp Space 或 Folder 中创建新列表。需要 space_id,可选 folder_id。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"space_id": {
|
||
"type": "string",
|
||
"description": "Space ID(数字字符串),通过 clickup_get_spaces 获取",
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"description": "列表名称",
|
||
},
|
||
"folder_id": {
|
||
"type": "string",
|
||
"description": "Folder ID,可选。不提供则创建在 Space 根目录下",
|
||
},
|
||
},
|
||
"required": ["space_id", "name"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_update_list",
|
||
"description": "更新 ClickUp 列表的名称或其他属性。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"list_id": {
|
||
"type": "string",
|
||
"description": "列表 ID",
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"description": "新的列表名称,可选",
|
||
},
|
||
},
|
||
"required": ["list_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_get_custom_fields",
|
||
"description": "获取指定 ClickUp 列表的自定义字段定义,返回字段名称、类型、可选值等信息。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"list_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 列表 ID",
|
||
},
|
||
},
|
||
"required": ["list_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_set_custom_field",
|
||
"description": "设置 ClickUp 任务的自定义字段值。需要先通过 clickup_get_custom_fields 了解字段 ID 和类型。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID",
|
||
},
|
||
"field_id": {
|
||
"type": "string",
|
||
"description": "自定义字段 ID(UUID 格式),通过 clickup_get_custom_fields 获取",
|
||
},
|
||
"value": {
|
||
"description": "字段值,类型取决于字段定义(文本传字符串,下拉传选项名,数字传数字等)",
|
||
},
|
||
},
|
||
"required": ["task_id", "field_id", "value"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_get_members",
|
||
"description": "获取 ClickUp Workspace 中的成员列表,用于查找用户 ID、分配任务负责人等。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {},
|
||
"required": [],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_move_task",
|
||
"description": "将 ClickUp 任务移动到新列表。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID",
|
||
},
|
||
"target_list_id": {
|
||
"type": "string",
|
||
"description": "目标列表 ID",
|
||
},
|
||
},
|
||
"required": ["task_id", "target_list_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_add_task_tag",
|
||
"description": "为 ClickUp 任务添加标签。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID",
|
||
},
|
||
"tag_name": {
|
||
"type": "string",
|
||
"description": "标签名称",
|
||
},
|
||
},
|
||
"required": ["task_id", "tag_name"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_remove_task_tag",
|
||
"description": "移除 ClickUp 任务的标签。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID",
|
||
},
|
||
"tag_name": {
|
||
"type": "string",
|
||
"description": "标签名称",
|
||
},
|
||
},
|
||
"required": ["task_id", "tag_name"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_create_checklist",
|
||
"description": "为 ClickUp 任务创建检查清单。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "ClickUp 任务 ID",
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"description": "检查清单名称",
|
||
},
|
||
},
|
||
"required": ["task_id", "name"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_add_checklist_item",
|
||
"description": "向 ClickUp 检查清单添加条目。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"checklist_id": {
|
||
"type": "string",
|
||
"description": "检查清单 ID",
|
||
},
|
||
"name": {
|
||
"type": "string",
|
||
"description": "条目名称",
|
||
},
|
||
},
|
||
"required": ["checklist_id", "name"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_add_task_dependency",
|
||
"description": "为 ClickUp 任务添加依赖关系。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "当前任务 ID",
|
||
},
|
||
"depends_on": {
|
||
"type": "string",
|
||
"description": "依赖的任务 ID",
|
||
},
|
||
},
|
||
"required": ["task_id", "depends_on"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_delete_task_dependency",
|
||
"description": "删除 ClickUp 任务的依赖关系。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "当前任务 ID",
|
||
},
|
||
"depends_on": {
|
||
"type": "string",
|
||
"description": "依赖的任务 ID",
|
||
},
|
||
},
|
||
"required": ["task_id", "depends_on"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_add_task_link",
|
||
"description": "关联两个 ClickUp 任务。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "当前任务 ID",
|
||
},
|
||
"links_to": {
|
||
"type": "string",
|
||
"description": "要关联的目标任务 ID",
|
||
},
|
||
},
|
||
"required": ["task_id", "links_to"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_delete_task_link",
|
||
"description": "删除 ClickUp 任务之间的关联。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"task_id": {
|
||
"type": "string",
|
||
"description": "当前任务 ID",
|
||
},
|
||
"links_to": {
|
||
"type": "string",
|
||
"description": "要取消关联的目标任务 ID",
|
||
},
|
||
},
|
||
"required": ["task_id", "links_to"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_update_comment",
|
||
"description": "更新 ClickUp 任务评论。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"comment_id": {
|
||
"type": "string",
|
||
"description": "评论 ID",
|
||
},
|
||
"content": {
|
||
"type": "string",
|
||
"description": "新的评论内容",
|
||
},
|
||
},
|
||
"required": ["comment_id", "content"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_delete_comment",
|
||
"description": "删除 ClickUp 任务评论。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"comment_id": {
|
||
"type": "string",
|
||
"description": "评论 ID",
|
||
},
|
||
},
|
||
"required": ["comment_id"],
|
||
},
|
||
},
|
||
{
|
||
"name": "clickup_get_folders",
|
||
"description": "获取 ClickUp Space 中的文件夹列表。",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"space_id": {
|
||
"type": "string",
|
||
"description": "Space ID",
|
||
},
|
||
},
|
||
"required": ["space_id"],
|
||
},
|
||
},
|
||
]
|
||
|
||
|
||
async def execute_clickup_tool(
|
||
tool_name: str,
|
||
arguments: dict,
|
||
account: dict,
|
||
) -> dict:
|
||
api_token = account.get("api_token", "")
|
||
workspace_id = account.get("workspace_id", "")
|
||
|
||
if not api_token or not workspace_id:
|
||
return {"success": False, "error": "ClickUp 账户未配置(缺少 api_token 或 workspace_id)"}
|
||
|
||
tool_map = {
|
||
"clickup_create_task": _create_task,
|
||
"clickup_get_task": _get_task,
|
||
"clickup_update_task": _update_task,
|
||
"clickup_list_tasks": _list_tasks,
|
||
"clickup_search_tasks": _search_tasks,
|
||
"clickup_add_task_comment": _add_task_comment,
|
||
"clickup_get_task_comments": _get_task_comments,
|
||
"clickup_get_lists": _get_lists,
|
||
"clickup_delete_task": _delete_task,
|
||
"clickup_get_spaces": _get_spaces,
|
||
"clickup_create_list": _create_list,
|
||
"clickup_update_list": _update_list,
|
||
"clickup_get_custom_fields": _get_custom_fields,
|
||
"clickup_set_custom_field": _set_custom_field,
|
||
"clickup_get_members": _get_members,
|
||
"clickup_move_task": _move_task,
|
||
"clickup_add_task_tag": _add_task_tag,
|
||
"clickup_remove_task_tag": _remove_task_tag,
|
||
"clickup_create_checklist": _create_checklist,
|
||
"clickup_add_checklist_item": _add_checklist_item,
|
||
"clickup_add_task_dependency": _add_task_dependency,
|
||
"clickup_delete_task_dependency": _delete_task_dependency,
|
||
"clickup_add_task_link": _add_task_link,
|
||
"clickup_delete_task_link": _delete_task_link,
|
||
"clickup_update_comment": _update_comment,
|
||
"clickup_delete_comment": _delete_comment,
|
||
"clickup_get_folders": _get_folders,
|
||
}
|
||
|
||
handler = tool_map.get(tool_name)
|
||
if not handler:
|
||
return {"success": False, "error": f"未知工具: {tool_name}"}
|
||
|
||
try:
|
||
result = await handler(arguments, api_token, workspace_id)
|
||
return {"success": True, "result": result}
|
||
except Exception as e:
|
||
logger.exception("clickup tool %s failed", tool_name)
|
||
return {"success": False, "error": str(e)}
|
||
|
||
|
||
async def _create_task(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
list_id = arguments["list_id"]
|
||
url = f"{API_V2_BASE}/list/{list_id}/task"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload: dict = {
|
||
"name": arguments["name"],
|
||
}
|
||
|
||
if "description" in arguments:
|
||
payload["description"] = arguments["description"]
|
||
if "priority" in arguments:
|
||
payload["priority"] = arguments["priority"]
|
||
if "assignees" in arguments and arguments["assignees"]:
|
||
payload["assignees"] = arguments["assignees"]
|
||
if "tags" in arguments and arguments["tags"]:
|
||
payload["tags"] = arguments["tags"]
|
||
if "due_date" in arguments:
|
||
payload["due_date"] = arguments["due_date"]
|
||
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
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_task failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"创建任务失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _get_task(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
url = f"{API_V2_BASE}/task/{task_id}"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, headers=headers)
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
logger.error("clickup get_task failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"获取任务失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _update_task(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
url = f"{API_V2_BASE}/task/{task_id}"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload: dict = {}
|
||
|
||
if "name" in arguments:
|
||
payload["name"] = arguments["name"]
|
||
if "description" in arguments:
|
||
payload["description"] = arguments["description"]
|
||
if "status" in arguments:
|
||
payload["status"] = arguments["status"]
|
||
if "priority" in arguments:
|
||
payload["priority"] = arguments["priority"]
|
||
if "assignees" in arguments:
|
||
payload["assignees"] = arguments["assignees"]
|
||
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
resp = await client.put(url, json=payload, headers=headers)
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
logger.error("clickup update_task failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"更新任务失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _list_tasks(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
list_id = arguments["list_id"]
|
||
url = f"{API_V2_BASE}/list/{list_id}/task"
|
||
|
||
headers = {"Authorization": api_token}
|
||
params: dict = {
|
||
"page": arguments.get("page", 0),
|
||
}
|
||
|
||
if "status" in arguments and arguments["status"]:
|
||
params["statuses"] = [arguments["status"]]
|
||
limit = min(arguments.get("limit", 25), 100)
|
||
params["limit"] = limit
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, params=params, headers=headers)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
tasks = data.get("tasks", [])
|
||
return {
|
||
"tasks": tasks,
|
||
"total": len(tasks),
|
||
"page": arguments.get("page", 0),
|
||
}
|
||
error_text = resp.text[:500]
|
||
logger.error("clickup list_tasks failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"获取任务列表失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _search_tasks(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
query = arguments["query"]
|
||
url = f"{API_V2_BASE}/team/{workspace_id}/task"
|
||
|
||
headers = {"Authorization": api_token}
|
||
params: dict = {
|
||
"page": 0,
|
||
"order_by": "updated",
|
||
"reverse": True,
|
||
}
|
||
|
||
if "status" in arguments and arguments["status"]:
|
||
params["statuses"] = [arguments["status"]]
|
||
if "assignee" in arguments and arguments["assignee"]:
|
||
params["assignees"] = [arguments["assignee"]]
|
||
if "list_id" in arguments:
|
||
params["list_ids"] = [arguments["list_id"]]
|
||
if "query" in arguments:
|
||
params["query"] = arguments["query"]
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, params=params, headers=headers)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
tasks = data.get("tasks", [])
|
||
if query:
|
||
tasks = [t for t in tasks if query.lower() in t.get("name", "").lower()]
|
||
return {
|
||
"tasks": tasks,
|
||
"total": len(tasks),
|
||
}
|
||
error_text = resp.text[:500]
|
||
logger.error("clickup search_tasks failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"搜索任务失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _add_task_comment(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
url = f"{API_V2_BASE}/task/{task_id}/comment"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload = {
|
||
"comment_text": arguments["content"],
|
||
}
|
||
|
||
async with httpx.AsyncClient(timeout=30.0) as client:
|
||
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 add_task_comment failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"添加任务评论失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _get_task_comments(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
url = f"{API_V2_BASE}/task/{task_id}/comment"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, headers=headers)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
return {
|
||
"comments": data.get("comments", []),
|
||
"total": len(data.get("comments", [])),
|
||
}
|
||
error_text = resp.text[:500]
|
||
logger.error("clickup get_task_comments failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"获取任务评论失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _get_lists(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
space_id = arguments.get("space_id")
|
||
if space_id:
|
||
url = f"{API_V2_BASE}/space/{space_id}/list"
|
||
else:
|
||
url = f"{API_V2_BASE}/team/{workspace_id}/list"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, headers=headers)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
return {
|
||
"lists": data.get("lists", []),
|
||
"total": len(data.get("lists", [])),
|
||
}
|
||
error_text = resp.text[:500]
|
||
logger.error("clickup get_lists failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"获取列表清单失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _delete_task(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
url = f"{API_V2_BASE}/task/{task_id}"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.delete(url, headers=headers)
|
||
if resp.status_code in (200, 204):
|
||
return {"deleted": True, "task_id": task_id}
|
||
error_text = resp.text[:500]
|
||
logger.error("clickup delete_task failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"删除任务失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _get_spaces(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
url = f"{API_V2_BASE}/team/{workspace_id}/space"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, headers=headers)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
return {
|
||
"spaces": data.get("spaces", []),
|
||
"total": len(data.get("spaces", [])),
|
||
}
|
||
error_text = resp.text[:500]
|
||
logger.error("clickup get_spaces failed: status=%d body=%s", resp.status_code, error_text)
|
||
raise RuntimeError(f"获取 Space 列表失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _create_list(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
space_id = arguments["space_id"]
|
||
folder_id = arguments.get("folder_id")
|
||
if folder_id:
|
||
url = f"{API_V2_BASE}/folder/{folder_id}/list"
|
||
else:
|
||
url = f"{API_V2_BASE}/space/{space_id}/list"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload = {"name": arguments["name"]}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.post(url, json=payload, headers=headers)
|
||
if resp.status_code in (200, 201):
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"创建列表失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _update_list(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
list_id = arguments["list_id"]
|
||
url = f"{API_V2_BASE}/list/{list_id}"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload: dict = {}
|
||
if "name" in arguments:
|
||
payload["name"] = arguments["name"]
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.put(url, json=payload, headers=headers)
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"更新列表失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _get_custom_fields(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
list_id = arguments["list_id"]
|
||
url = f"{API_V2_BASE}/list/{list_id}/field"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, headers=headers)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
return {
|
||
"fields": data.get("fields", []),
|
||
"total": len(data.get("fields", [])),
|
||
}
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"获取自定义字段失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _set_custom_field(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
field_id = arguments["field_id"]
|
||
url = f"{API_V2_BASE}/task/{task_id}/field/{field_id}"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload = {"value": arguments["value"]}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.post(url, json=payload, headers=headers)
|
||
if resp.status_code in (200, 201):
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"设置自定义字段失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _get_members(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
url = f"{API_V2_BASE}/team/{workspace_id}/user"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, headers=headers)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
users = data.get("users", [])
|
||
members = [
|
||
{"id": u.get("id"), "username": u.get("username", ""), "email": u.get("email", "")} for u in users
|
||
]
|
||
return {"members": members, "total": len(members)}
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"获取成员列表失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _move_task(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
target_list_id = arguments["target_list_id"]
|
||
url = f"{API_V2_BASE}/task/{task_id}/list/{target_list_id}"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.post(url, headers=headers)
|
||
if resp.status_code in (200, 201):
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"移动任务失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _add_task_tag(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
tag_name = arguments["tag_name"]
|
||
url = f"{API_V2_BASE}/task/{task_id}/tag/{tag_name}"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.post(url, headers=headers)
|
||
if resp.status_code in (200, 201):
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"添加标签失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _remove_task_tag(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
tag_name = arguments["tag_name"]
|
||
url = f"{API_V2_BASE}/task/{task_id}/tag/{tag_name}"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.delete(url, headers=headers)
|
||
if resp.status_code in (200, 204):
|
||
return {"removed": True, "tag": tag_name}
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"移除标签失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _create_checklist(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
url = f"{API_V2_BASE}/task/{task_id}/checklist"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload = {"name": arguments["name"]}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.post(url, json=payload, headers=headers)
|
||
if resp.status_code in (200, 201):
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"创建检查清单失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _add_checklist_item(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
checklist_id = arguments["checklist_id"]
|
||
url = f"{API_V2_BASE}/checklist/{checklist_id}/checklist_item"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload = {"name": arguments["name"]}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.post(url, json=payload, headers=headers)
|
||
if resp.status_code in (200, 201):
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"添加检查清单条目失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _add_task_dependency(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
depends_on = _clean_task_id(arguments["depends_on"])
|
||
url = f"{API_V2_BASE}/task/{task_id}/dependency"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload = {"depends_on": depends_on}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.post(url, json=payload, headers=headers)
|
||
if resp.status_code in (200, 201):
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"添加依赖失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _delete_task_dependency(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
depends_on = _clean_task_id(arguments["depends_on"])
|
||
url = f"{API_V2_BASE}/task/{task_id}/dependency"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload = {"depends_on": depends_on}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.delete(url, json=payload, headers=headers)
|
||
if resp.status_code in (200, 204):
|
||
return {"deleted": True, "depends_on": depends_on}
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"删除依赖失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _add_task_link(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
links_to = _clean_task_id(arguments["links_to"])
|
||
url = f"{API_V2_BASE}/task/{task_id}/link/{links_to}"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.post(url, headers=headers)
|
||
if resp.status_code in (200, 201):
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"关联任务失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _delete_task_link(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
task_id = _clean_task_id(arguments["task_id"])
|
||
links_to = _clean_task_id(arguments["links_to"])
|
||
url = f"{API_V2_BASE}/task/{task_id}/link/{links_to}"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.delete(url, headers=headers)
|
||
if resp.status_code in (200, 204):
|
||
return {"deleted": True, "links_to": links_to}
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"删除关联失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _update_comment(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
comment_id = arguments["comment_id"]
|
||
url = f"{API_V2_BASE}/comment/{comment_id}"
|
||
|
||
headers = {"Authorization": api_token, "Content-Type": "application/json"}
|
||
payload = {"comment_text": arguments["content"]}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.put(url, json=payload, headers=headers)
|
||
if resp.status_code == 200:
|
||
return resp.json()
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"更新评论失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _delete_comment(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
comment_id = arguments["comment_id"]
|
||
url = f"{API_V2_BASE}/comment/{comment_id}"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.delete(url, headers=headers)
|
||
if resp.status_code in (200, 204):
|
||
return {"deleted": True, "comment_id": comment_id}
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"删除评论失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
async def _get_folders(arguments: dict, api_token: str, workspace_id: str) -> dict:
|
||
space_id = arguments["space_id"]
|
||
url = f"{API_V2_BASE}/space/{space_id}/folder"
|
||
|
||
headers = {"Authorization": api_token}
|
||
|
||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||
resp = await client.get(url, headers=headers)
|
||
if resp.status_code == 200:
|
||
data = resp.json()
|
||
return {
|
||
"folders": data.get("folders", []),
|
||
"total": len(data.get("folders", [])),
|
||
}
|
||
error_text = resp.text[:500]
|
||
raise RuntimeError(f"获取文件夹列表失败 (HTTP {resp.status_code}): {error_text}")
|
||
|
||
|
||
def _clean_task_id(task_id: str) -> str:
|
||
return task_id.lstrip("#")
|