941 lines
35 KiB
Python
941 lines
35 KiB
Python
"""
|
||
任务相关MCP工具
|
||
"""
|
||
|
||
from typing import Dict, List, Optional, Any, Union
|
||
from datetime import datetime, timedelta
|
||
import pytz
|
||
from fastmcp import FastMCP
|
||
from .adapter import adapter, APIError
|
||
|
||
# --- 模块级辅助函数 ---
|
||
|
||
_completed_columns = set()
|
||
|
||
def _parse_date(date_str: Optional[str]) -> Optional[datetime]:
|
||
"""解析日期字符串为datetime对象,将UTC时间转换为北京时间"""
|
||
if not date_str:
|
||
return None
|
||
|
||
local_tz = pytz.timezone('Asia/Shanghai')
|
||
|
||
try:
|
||
# 处理ISO格式
|
||
if 'T' in date_str:
|
||
base_time = date_str.split('.')[0]
|
||
if date_str.endswith('Z') or '+0000' in date_str:
|
||
# UTC时间,需要转换
|
||
dt = datetime.strptime(base_time.replace('T', ' '), "%Y-%m-%d %H:%M:%S")
|
||
dt = pytz.UTC.localize(dt)
|
||
return dt.astimezone(local_tz)
|
||
else:
|
||
# 尝试使用fromisoformat
|
||
try:
|
||
dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
|
||
return dt.astimezone(local_tz)
|
||
except ValueError:
|
||
# 假定为本地时间
|
||
dt = datetime.strptime(base_time.replace('T', ' '), "%Y-%m-%d %H:%M:%S")
|
||
return local_tz.localize(dt)
|
||
except ValueError:
|
||
pass
|
||
|
||
try:
|
||
# 标准格式 (假定为本地时间)
|
||
dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
|
||
return local_tz.localize(dt)
|
||
except ValueError:
|
||
# 尝试其他可能的格式
|
||
try:
|
||
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||
return local_tz.localize(dt)
|
||
except ValueError:
|
||
return None
|
||
|
||
def _format_date_for_api(date_str: Optional[str]) -> Optional[str]:
|
||
"""
|
||
将'YYYY-MM-DD HH:MM:SS'格式的日期转换为API所需的'YYYY-MM-DDThh:mm:ss.000+0000'格式
|
||
|
||
Args:
|
||
date_str: 'YYYY-MM-DD HH:MM:SS'格式的日期字符串
|
||
|
||
Returns:
|
||
转换后的API格式日期字符串
|
||
"""
|
||
if not date_str:
|
||
return None
|
||
|
||
try:
|
||
# 解析输入的日期字符串为datetime对象(假设为本地时间)
|
||
local_tz = pytz.timezone('Asia/Shanghai')
|
||
|
||
# 尝试解析不同格式
|
||
try:
|
||
# 带时间的完整格式
|
||
dt = datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S")
|
||
except ValueError:
|
||
try:
|
||
# 仅日期格式
|
||
dt = datetime.strptime(date_str, "%Y-%m-%d")
|
||
except ValueError:
|
||
# 如果已经是ISO格式,直接返回
|
||
if 'T' in date_str:
|
||
return date_str
|
||
# 其他格式无法解析
|
||
return date_str
|
||
|
||
# 将解析的datetime转换为本地时区的datetime
|
||
dt = local_tz.localize(dt)
|
||
|
||
# 转换为UTC时间
|
||
utc_dt = dt.astimezone(pytz.UTC)
|
||
|
||
# 格式化为API需要的格式:YYYY-MM-DDThh:mm:ss.000+0000
|
||
return utc_dt.strftime("%Y-%m-%dT%H:%M:%S.000+0000")
|
||
except Exception as e:
|
||
# 日期格式转换错误(禁用print避免干扰JSONRPC)
|
||
pass
|
||
return date_str
|
||
|
||
def _simplify_task_data(task_data: Dict[str, Any], projects_data: List[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||
"""简化任务数据,保留重要字段并格式化日期"""
|
||
# 格式化日期函数
|
||
def format_date(date_str, is_due_date=False):
|
||
if not date_str:
|
||
return None
|
||
|
||
dt = _parse_date(date_str)
|
||
if not dt:
|
||
return date_str
|
||
|
||
# 如果是截止日期且时间是0点,考虑添加一天
|
||
if is_due_date and dt.hour == 0 and dt.minute == 0 and dt.second == 0:
|
||
dt = dt + timedelta(days=1)
|
||
|
||
return dt.strftime("%Y-%m-%d %H:%M:%S")
|
||
|
||
# 如果提供了项目数据列表,且当前任务只有projectId但没有projectName,则匹配项目名称
|
||
if projects_data and task_data.get('projectId') and not task_data.get('projectName'):
|
||
task_data = _merge_project_info_logic(task_data, projects_data)
|
||
|
||
# 处理子任务
|
||
children = []
|
||
if task_data.get('items'):
|
||
for item in task_data['items']:
|
||
# 递归调用,同时传递项目数据
|
||
child_task = _simplify_task_data(item, projects_data)
|
||
children.append(child_task)
|
||
# 创建基础任务数据
|
||
simplified = {
|
||
"id": task_data.get("id"),
|
||
"title": task_data.get("title"),
|
||
"content": task_data.get("content"),
|
||
"priority": task_data.get("priority"),
|
||
"status": task_data.get("status"),
|
||
"completed": task_data.get("isCompleted", False),
|
||
"projectId": task_data.get("projectId"),
|
||
"projectName": task_data.get("projectName", "默认清单"),
|
||
"projectKind": task_data.get("projectKind"),
|
||
"columnId": task_data.get("columnId"),
|
||
"tags": task_data.get("tags", []),
|
||
"tagDetails": task_data.get("tagDetails", []),
|
||
"startDate": format_date(task_data.get("startDate")),
|
||
"dueDate": format_date(task_data.get("dueDate"), is_due_date=True),
|
||
"completedTime": format_date(task_data.get("completedTime")),
|
||
"createdTime": format_date(task_data.get("createdTime")),
|
||
"modifiedTime": format_date(task_data.get("modifiedTime")),
|
||
"isAllDay": task_data.get("isAllDay", False),
|
||
"reminder": task_data.get("reminder"),
|
||
"progress": task_data.get("progress", 0),
|
||
"kind": task_data.get("kind"),
|
||
"isCompleted": task_data.get("isCompleted", False),
|
||
"items": children,
|
||
"timeZone": "Asia/Shanghai",
|
||
"reminders": task_data.get("reminders", []),
|
||
"creator": task_data.get("creator"),
|
||
"sortOrder": task_data.get("sortOrder", 0),
|
||
"parentId": task_data.get("parentId"),
|
||
"children": children
|
||
}
|
||
|
||
# 移除None值
|
||
simplified = {k: v for k, v in simplified.items() if v is not None}
|
||
return simplified
|
||
|
||
def _get_completed_tasks_info_logic() -> Dict[str, Any]:
|
||
"""(已废弃路径) 兼容占位:官方API不提供旧批量接口,返回空。"""
|
||
return {}
|
||
|
||
def _update_column_info_logic(projects: List[Dict[str, Any]], completed_columns_set: set):
|
||
"""更新栏目信息,识别已完成状态的栏目 (逻辑部分)"""
|
||
for project in projects:
|
||
if 'columns' in project:
|
||
for column in project.get('columns', []):
|
||
# 检查栏目是否为已完成状态
|
||
if column.get('type') == 'COMPLETED':
|
||
completed_columns_set.add(column.get('id'))
|
||
|
||
def _merge_project_info_logic(task_data: Dict[str, Any], projects: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||
"""将项目信息合并到任务数据中 (逻辑部分)"""
|
||
if not task_data.get('projectId'):
|
||
return task_data
|
||
for project in projects:
|
||
if project.get('id') == task_data['projectId']:
|
||
task_data['projectName'] = project.get('name')
|
||
task_data['projectKind'] = project.get('kind')
|
||
break
|
||
return task_data
|
||
|
||
def _merge_tag_info_logic(task_data: Dict[str, Any], tags: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||
"""将标签详细信息合并到任务数据中 (逻辑部分)"""
|
||
if not task_data.get('tags'):
|
||
return task_data
|
||
tag_details = []
|
||
for tag_name in task_data['tags']:
|
||
for tag in tags:
|
||
if tag.get('name') == tag_name:
|
||
tag_details.append({
|
||
'name': tag.get('name'),
|
||
'label': tag.get('label')
|
||
})
|
||
break
|
||
task_data['tagDetails'] = tag_details
|
||
return task_data
|
||
|
||
def _get_all_tasks_logic() -> tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]]:
|
||
"""获取所有任务,包括已完成和未完成的任务,并合并相关信息 (逻辑部分)"""
|
||
global _completed_columns
|
||
# 使用官方接口获取项目与任务
|
||
projects_data = []
|
||
try:
|
||
projects_data = adapter.list_projects()
|
||
except Exception:
|
||
projects_data = []
|
||
|
||
# 官方任务接口通常需要分别获取未完成/已完成,视文档具体筛选实现
|
||
tasks_data: List[Dict[str, Any]] = []
|
||
tags_data: List[Dict[str, Any]] = [] # 若官方没有标签API,则保留空集合
|
||
try:
|
||
# 遍历项目聚合任务(官方无全局列表端点)
|
||
incomplete = adapter.list_tasks(completed=False)
|
||
complete = adapter.list_tasks(completed=True)
|
||
tasks_data = (incomplete or []) + (complete or [])
|
||
except Exception as e:
|
||
# 获取任务列表失败(禁用print避免干扰JSONRPC)
|
||
pass
|
||
tasks_data = []
|
||
|
||
# 更新栏目信息 (使用全局变量)
|
||
_update_column_info_logic(projects_data, _completed_columns)
|
||
|
||
# 旧逻辑依赖批量端点合并“已完成”,现在已通过 adapter 分开获取,这里置空
|
||
completed_tasks_info = {}
|
||
|
||
# 处理所有任务(未完成+已完成)
|
||
all_tasks = []
|
||
|
||
# 处理未完成任务
|
||
for task in tasks_data:
|
||
# 只处理文本类型的任务
|
||
if task.get('kind') != 'TEXT':
|
||
continue
|
||
|
||
# 合并项目和标签信息
|
||
task = _merge_project_info_logic(task, projects_data)
|
||
task = _merge_tag_info_logic(task, tags_data)
|
||
|
||
# 检查是否在已完成任务列表中
|
||
key = f"{task.get('creator')}_{task.get('title')}"
|
||
# adapter.normalize_task_status 已处理 isCompleted/status,这里不再强制覆盖
|
||
|
||
all_tasks.append(task)
|
||
|
||
# 不再需要从已完成任务列表补充,adapter 已统一返回
|
||
|
||
return all_tasks, projects_data, tags_data
|
||
|
||
# --- 模块级核心逻辑函数 ---
|
||
|
||
def get_tasks_logic(
|
||
mode: Optional[str] = "all",
|
||
keyword: Optional[str] = None,
|
||
priority: Optional[int] = None,
|
||
project_name: Optional[str] = None,
|
||
completed: Optional[bool] = None
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
获取任务列表 (逻辑部分)
|
||
|
||
Args:
|
||
mode: 任务模式,支持 'all'(所有), 'today'(今天), 'yesterday'(昨天), 'recent_7_days'(最近7天)
|
||
keyword: 关键词筛选
|
||
priority: 优先级筛选 (0-最低, 1-低, 3-中, 5-高)
|
||
project_name: 项目名称筛选
|
||
completed: 是否已完成,True表示已完成,False表示未完成,None表示全部
|
||
|
||
Returns:
|
||
符合条件的任务列表
|
||
"""
|
||
try:
|
||
# 如果是查询今天的任务,默认只显示未完成的任务
|
||
if mode == "today" and completed is None:
|
||
completed = False
|
||
|
||
# 获取所有任务
|
||
all_tasks, projects_data, tags_data = _get_all_tasks_logic()
|
||
|
||
# 确保所有任务都有正确的project_name (在过滤之前)
|
||
for task in all_tasks:
|
||
if task.get('projectId') and not task.get('projectName'):
|
||
_merge_project_info_logic(task, projects_data)
|
||
|
||
def is_today(task):
|
||
# 检查任务是否为今天
|
||
local_tz = pytz.timezone('Asia/Shanghai')
|
||
now = datetime.now(local_tz)
|
||
today = now.date()
|
||
|
||
# 解析日期并获取日期部分
|
||
start_date = _parse_date(task.get('startDate'))
|
||
due_date = _parse_date(task.get('dueDate'))
|
||
|
||
# 简化判断逻辑:使用截止日期或开始日期判断
|
||
task_date = due_date or start_date
|
||
|
||
# 判断日期是否为今天
|
||
if task_date and task_date.date() == today:
|
||
return True
|
||
|
||
# 如果任务跨越今天(开始日期在今天之前,截止日期在今天之后或无截止日期)
|
||
if start_date and start_date.date() < today:
|
||
if not due_date or due_date.date() >= today:
|
||
return True
|
||
|
||
return False
|
||
|
||
def is_yesterday(task):
|
||
# 检查任务是否为昨天
|
||
local_tz = pytz.timezone('Asia/Shanghai')
|
||
now = datetime.now(local_tz)
|
||
yesterday = (now - timedelta(days=1)).date()
|
||
|
||
# 解析日期
|
||
start_date = _parse_date(task.get('startDate'))
|
||
due_date = _parse_date(task.get('dueDate'))
|
||
|
||
# 简化判断逻辑:使用截止日期或开始日期判断
|
||
task_date = due_date or start_date
|
||
|
||
return task_date and task_date.date() == yesterday
|
||
|
||
def is_recent_7_days(task):
|
||
# 检查任务是否属于最近7天
|
||
local_tz = pytz.timezone('Asia/Shanghai')
|
||
now = datetime.now(local_tz)
|
||
seven_days_ago = (now - timedelta(days=7))
|
||
|
||
# 解析日期
|
||
start_date = _parse_date(task.get('startDate'))
|
||
due_date = _parse_date(task.get('dueDate'))
|
||
|
||
# 简化判断逻辑:使用截止日期或开始日期判断
|
||
task_date = due_date or start_date
|
||
|
||
# 最近7天的任务
|
||
if task_date and task_date >= seven_days_ago:
|
||
return True
|
||
|
||
# 跨越这7天的任务
|
||
if start_date and start_date < seven_days_ago:
|
||
if due_date and due_date >= seven_days_ago:
|
||
return True
|
||
|
||
return False
|
||
|
||
# 过滤任务
|
||
result = []
|
||
for task in all_tasks:
|
||
# 根据完成状态筛选
|
||
if completed is not None:
|
||
is_task_completed = task.get('isCompleted', False)
|
||
if is_task_completed != completed:
|
||
continue
|
||
|
||
# 根据模式筛选
|
||
if mode == "today" and not is_today(task):
|
||
continue
|
||
elif mode == "yesterday" and not is_yesterday(task):
|
||
continue
|
||
elif mode == "recent_7_days" and not is_recent_7_days(task):
|
||
continue
|
||
|
||
# 根据其他条件筛选
|
||
if keyword and keyword.lower() not in task.get('title', '').lower() and keyword.lower() not in task.get('content', '').lower():
|
||
continue
|
||
|
||
if priority is not None and task.get('priority') != priority:
|
||
continue
|
||
|
||
|
||
|
||
# 根据项目名称筛选 (现在任务已经有了正确的project_name)
|
||
if project_name and project_name not in task.get('projectName', ''):
|
||
continue
|
||
# 保留简化后的任务数据,传递项目数据
|
||
simplified_task = _simplify_task_data(task, projects_data)
|
||
result.append(simplified_task)
|
||
|
||
return result
|
||
except Exception as e:
|
||
# 获取任务列表时发生错误(禁用print避免干扰JSONRPC)
|
||
pass
|
||
return []
|
||
|
||
def create_task_logic(
|
||
title: Optional[str] = None,
|
||
content: Optional[str] = None,
|
||
priority: Optional[int] = None,
|
||
project_name: Optional[str] = None,
|
||
tag_names: Optional[List[str]] = None,
|
||
start_date: Optional[str] = None,
|
||
due_date: Optional[str] = None,
|
||
is_all_day: Optional[bool] = None,
|
||
reminder: Optional[str] = None,
|
||
kind: Optional[str] = None,
|
||
# 官方字段(新增)
|
||
project_id: Optional[str] = None,
|
||
desc: Optional[str] = None,
|
||
time_zone: Optional[str] = None,
|
||
reminders: Optional[List[str]] = None,
|
||
repeat_flag: Optional[str] = None,
|
||
sort_order: Optional[int] = None,
|
||
items: Optional[List[Dict[str, Any]]] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
创建新任务 (逻辑部分)
|
||
|
||
Args:
|
||
title: 任务标题
|
||
content: 任务内容
|
||
priority: 优先级 (0-最低, 1-低, 3-中, 5-高)
|
||
project_name: 项目名称
|
||
tag_names: 标签名称列表
|
||
start_date: 开始日期,格式 'YYYY-MM-DD HH:MM:SS'
|
||
due_date: 截止日期,格式 'YYYY-MM-DD HH:MM:SS'
|
||
is_all_day: 是否为全天任务
|
||
reminder: 提醒选项,如 "0"(准时), "-5M"(提前5分钟), "-1H"(提前1小时), "-1D"(提前1天)
|
||
|
||
Returns:
|
||
创建的任务信息
|
||
"""
|
||
resolved_project_id = project_id
|
||
projects_data = adapter.list_projects()
|
||
|
||
if not resolved_project_id and project_name:
|
||
# 尝试精确匹配
|
||
for project in projects_data:
|
||
if project.get('name') == project_name:
|
||
resolved_project_id = project.get('id')
|
||
break
|
||
|
||
# 如果精确匹配失败,尝试不区分大小写的匹配
|
||
if not resolved_project_id and project_name:
|
||
project_name_lower = project_name.lower()
|
||
for project in projects_data:
|
||
if project.get('name', '').lower() == project_name_lower:
|
||
# 不区分大小写匹配到项目(禁用print避免干扰JSONRPC)
|
||
pass
|
||
resolved_project_id = project.get('id')
|
||
break
|
||
|
||
# 如果仍然失败,尝试部分匹配
|
||
if not resolved_project_id and project_name:
|
||
for project in projects_data:
|
||
if project_name.lower() in project.get('name', '').lower() or project.get('name', '').lower() in project_name.lower():
|
||
# 部分匹配到项目(禁用print避免干扰JSONRPC)
|
||
pass
|
||
resolved_project_id = project.get('id')
|
||
break
|
||
|
||
# 准备任务数据
|
||
task_data = {
|
||
"title": title,
|
||
"content": content,
|
||
"priority": priority,
|
||
"projectId": resolved_project_id,
|
||
# 官方任务未公开 tags 字段,保留为兼容逻辑但不强制发送
|
||
# "tags": tag_names or [],
|
||
"isAllDay": is_all_day,
|
||
"reminder": reminder,
|
||
"status": 0,
|
||
"kind": kind,
|
||
# 官方字段
|
||
"desc": desc,
|
||
"timeZone": time_zone,
|
||
"reminders": reminders,
|
||
"repeatFlag": repeat_flag,
|
||
"sortOrder": sort_order,
|
||
"items": items,
|
||
}
|
||
|
||
# 处理日期和提醒
|
||
if start_date:
|
||
# 转换为API所需的格式
|
||
task_data["startDate"] = _format_date_for_api(start_date)
|
||
|
||
if due_date:
|
||
# 转换为API所需的格式
|
||
task_data["dueDate"] = _format_date_for_api(due_date)
|
||
|
||
if reminder:
|
||
task_data["reminder"] = reminder
|
||
|
||
# 移除None值的字段
|
||
task_data = {k: v for k, v in task_data.items() if v is not None}
|
||
# Debug: task_data (禁用print以避免干扰JSONRPC)
|
||
|
||
# 发送创建请求
|
||
response = adapter.create_task(task_data)
|
||
|
||
# 返回简化后的响应,同时传递项目数据
|
||
return _simplify_task_data(response, projects_data)
|
||
|
||
def update_task_logic(
|
||
task_id_or_title: str,
|
||
title: Optional[str] = None,
|
||
content: Optional[str] = None,
|
||
priority: Optional[int] = None,
|
||
project_name: Optional[str] = None,
|
||
tag_names: Optional[List[str]] = None,
|
||
start_date: Optional[str] = None,
|
||
due_date: Optional[str] = None,
|
||
is_all_day: Optional[bool] = None,
|
||
reminder: Optional[str] = None,
|
||
status: Optional[int] = None,
|
||
# 官方字段(新增)
|
||
project_id: Optional[str] = None,
|
||
desc: Optional[str] = None,
|
||
time_zone: Optional[str] = None,
|
||
reminders: Optional[List[str]] = None,
|
||
repeat_flag: Optional[str] = None,
|
||
sort_order: Optional[int] = None,
|
||
items: Optional[List[Dict[str, Any]]] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
更新任务 (逻辑部分)
|
||
|
||
Args:
|
||
task_id_or_title: 任务ID或任务标题
|
||
title: 新任务标题
|
||
content: 新任务内容
|
||
priority: 新优先级 (0-最低, 1-低, 3-中, 5-高)
|
||
project_name: 新项目名称
|
||
tag_names: 新标签名称列表
|
||
start_date: 新开始日期,格式 'YYYY-MM-DD HH:MM:SS'
|
||
due_date: 新截止日期,格式 'YYYY-MM-DD HH:MM:SS'
|
||
is_all_day: 是否为全天任务
|
||
reminder: 新提醒选项
|
||
status: 新状态,0表示未完成,2表示已完成
|
||
|
||
Returns:
|
||
更新后的任务信息字典 (包含 success, info, data)
|
||
"""
|
||
try:
|
||
# 获取所有任务
|
||
all_tasks, projects_data, _ = _get_all_tasks_logic()
|
||
|
||
# 查找任务
|
||
task = None
|
||
# 先尝试按ID查找
|
||
for t in all_tasks:
|
||
if t.get('id') == task_id_or_title:
|
||
task = t
|
||
break
|
||
|
||
# 如果没找到,按标题查找
|
||
if not task:
|
||
for t in all_tasks:
|
||
if t.get('title') == task_id_or_title:
|
||
task = t
|
||
break
|
||
|
||
if not task:
|
||
return {
|
||
"success": False,
|
||
"info": f"未找到ID或标题为 '{task_id_or_title}' 的任务",
|
||
"data": None
|
||
}
|
||
|
||
task_id = task.get('id')
|
||
|
||
# 查找项目ID(如果指定了项目名称)
|
||
project_id = project_id or task.get('projectId')
|
||
if project_name and not project_id:
|
||
for project in projects_data:
|
||
if project.get('name') == project_name:
|
||
project_id = project.get('id')
|
||
break
|
||
|
||
# 准备更新数据
|
||
update_data = {
|
||
"id": task_id,
|
||
"title": title if title is not None else task.get('title'),
|
||
"content": content if content is not None else task.get('content'),
|
||
"priority": priority if priority is not None else task.get('priority'),
|
||
"projectId": project_id,
|
||
# 官方未公开 tags 写入,去除发送
|
||
"isAllDay": is_all_day if is_all_day is not None else task.get('isAllDay'),
|
||
"status": status if status is not None else task.get('status'),
|
||
# 官方字段
|
||
"desc": desc if desc is not None else task.get('desc'),
|
||
"timeZone": time_zone if time_zone is not None else task.get('timeZone'),
|
||
"reminders": reminders if reminders is not None else task.get('reminders'),
|
||
"repeatFlag": repeat_flag if repeat_flag is not None else task.get('repeatFlag'),
|
||
"sortOrder": sort_order if sort_order is not None else task.get('sortOrder'),
|
||
"items": items if items is not None else task.get('items'),
|
||
}
|
||
|
||
# 处理日期和提醒
|
||
if start_date is not None:
|
||
# 转换为API所需的格式
|
||
update_data["startDate"] = _format_date_for_api(start_date)
|
||
elif 'startDate' in task:
|
||
update_data["startDate"] = task['startDate']
|
||
|
||
if due_date is not None:
|
||
# 转换为API所需的格式
|
||
update_data["dueDate"] = _format_date_for_api(due_date)
|
||
elif 'dueDate' in task:
|
||
update_data["dueDate"] = task['dueDate']
|
||
|
||
if reminder is not None:
|
||
update_data["reminder"] = reminder
|
||
elif 'reminder' in task:
|
||
update_data["reminder"] = task['reminder']
|
||
|
||
# 移除None值的字段
|
||
update_data = {k: v for k, v in update_data.items() if v is not None}
|
||
|
||
# 状态变更:对齐官方逻辑(仅支持完成)
|
||
if status is not None:
|
||
if status == 2:
|
||
if not project_id:
|
||
return {
|
||
"success": False,
|
||
"info": "完成任务失败:缺少 projectId",
|
||
"data": None
|
||
}
|
||
try:
|
||
adapter.complete_task(project_id, task_id)
|
||
# 完成后刷新一次该项目任务,返回最新数据
|
||
refreshed = adapter.list_tasks(project_id=project_id)
|
||
fresh = next((t for t in refreshed if t.get('id') == task_id), None)
|
||
if fresh:
|
||
return {
|
||
"success": True,
|
||
"info": "任务已完成",
|
||
"data": _simplify_task_data(fresh, projects_data)
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"info": f"完成任务失败: {e}",
|
||
"data": None
|
||
}
|
||
# 若无法刷新,基于原任务构造完成态返回
|
||
task_after = dict(task)
|
||
task_after['status'] = 2
|
||
task_after['isCompleted'] = True
|
||
return {
|
||
"success": True,
|
||
"info": "任务已完成",
|
||
"data": _simplify_task_data(task_after, projects_data)
|
||
}
|
||
elif status == 0:
|
||
return {
|
||
"success": False,
|
||
"info": "取消完成未在官方开放API中提供,暂不支持",
|
||
"data": None
|
||
}
|
||
|
||
# 非状态变更:走常规更新
|
||
# Debug: 更新数据(禁用print避免干扰JSONRPC)
|
||
pass
|
||
response = adapter.update_task(task_id, update_data)
|
||
|
||
# 返回更新结果
|
||
return {
|
||
"success": True,
|
||
"info": "任务更新成功",
|
||
"data": _simplify_task_data(response, projects_data)
|
||
}
|
||
except Exception as e:
|
||
# 更新任务失败(禁用print避免干扰JSONRPC)
|
||
return {
|
||
"success": False,
|
||
"info": f"更新任务失败: {str(e)}",
|
||
"data": None
|
||
}
|
||
|
||
def delete_task_logic(task_id_or_title: str) -> Dict[str, Any]:
|
||
"""
|
||
删除任务 (逻辑部分)
|
||
|
||
Args:
|
||
task_id_or_title: 任务ID或任务标题
|
||
|
||
Returns:
|
||
删除操作的响应字典 (包含 success, info, data)
|
||
"""
|
||
try:
|
||
# 获取所有任务
|
||
all_tasks, projects_data, _ = _get_all_tasks_logic()
|
||
|
||
task = None
|
||
# 先尝试按ID查找
|
||
for t in all_tasks:
|
||
if t.get('id') == task_id_or_title:
|
||
task = t
|
||
break
|
||
|
||
# 如果没找到,按标题查找
|
||
if not task:
|
||
for t in all_tasks:
|
||
if t.get('title') == task_id_or_title:
|
||
task = t
|
||
break
|
||
|
||
if not task:
|
||
return {
|
||
"success": False,
|
||
"info": f"未找到ID或标题为 '{task_id_or_title}' 的任务",
|
||
"data": None
|
||
}
|
||
|
||
task_id = task.get('id')
|
||
project_id = task.get('projectId')
|
||
|
||
# 发送删除请求
|
||
# 官方删除需要 projectId
|
||
adapter.delete_task(project_id, task_id)
|
||
|
||
# 返回删除结果
|
||
return {
|
||
"success": True,
|
||
"info": f"成功删除任务 '{task.get('title')}'",
|
||
"data": _simplify_task_data(task, projects_data)
|
||
}
|
||
|
||
except Exception as e:
|
||
# 删除任务失败(禁用print避免干扰JSONRPC)
|
||
return {
|
||
"success": False,
|
||
"info": f"删除任务失败: {str(e)}",
|
||
"data": None
|
||
}
|
||
|
||
|
||
def complete_task_logic(task_id_or_title: str) -> Dict[str, Any]:
|
||
"""
|
||
完成任务(调用官方 complete 接口)
|
||
|
||
Args:
|
||
task_id_or_title: 任务ID或任务标题
|
||
Returns:
|
||
操作结果字典
|
||
"""
|
||
try:
|
||
all_tasks, projects_data, _ = _get_all_tasks_logic()
|
||
task = None
|
||
for t in all_tasks:
|
||
if t.get('id') == task_id_or_title or t.get('title') == task_id_or_title:
|
||
task = t
|
||
break
|
||
if not task:
|
||
return {
|
||
"success": False,
|
||
"info": f"未找到ID或标题为 '{task_id_or_title}' 的任务",
|
||
"data": None
|
||
}
|
||
task_id = task.get('id')
|
||
project_id = task.get('projectId')
|
||
if not project_id:
|
||
return {
|
||
"success": False,
|
||
"info": "完成任务失败:缺少 projectId",
|
||
"data": None
|
||
}
|
||
adapter.complete_task(project_id, task_id)
|
||
# 刷新该项目任务,返回最新数据
|
||
refreshed = adapter.list_tasks(project_id=project_id)
|
||
fresh = next((t for t in refreshed if t.get('id') == task_id), None)
|
||
if fresh:
|
||
return {
|
||
"success": True,
|
||
"info": "任务已完成",
|
||
"data": _simplify_task_data(fresh, projects_data)
|
||
}
|
||
# 无法刷新则回退构造
|
||
task_after = dict(task)
|
||
task_after['status'] = 2
|
||
task_after['isCompleted'] = True
|
||
return {
|
||
"success": True,
|
||
"info": "任务已完成",
|
||
"data": _simplify_task_data(task_after, projects_data)
|
||
}
|
||
except Exception as e:
|
||
return {
|
||
"success": False,
|
||
"info": f"完成任务失败: {e}",
|
||
"data": None
|
||
}
|
||
|
||
|
||
# --- MCP工具注册 ---
|
||
|
||
def register_task_tools(server: FastMCP, auth_info: Dict[str, Any]):
|
||
"""
|
||
注册任务相关工具到MCP服务器
|
||
|
||
Args:
|
||
server: MCP服务器实例
|
||
auth_info: 认证信息字典,包含token或email/password
|
||
"""
|
||
# 适配层按需初始化,无需在此显式初始化
|
||
|
||
@server.tool()
|
||
def get_tasks(
|
||
mode: Optional[str] = "all",
|
||
keyword: Optional[str] = None,
|
||
priority: Optional[int] = None,
|
||
project_name: Optional[str] = None,
|
||
completed: Optional[bool] = None
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
获取任务列表
|
||
(调用模块级逻辑函数)
|
||
|
||
Args:
|
||
mode: 任务模式,支持 'all'(所有), 'today'(今天), 'yesterday'(昨天), 'recent_7_days'(最近7天)
|
||
keyword: 关键词筛选
|
||
priority: 优先级筛选 (0-最低, 1-低, 3-中, 5-高)
|
||
project_name: 项目名称筛选
|
||
completed: 是否已完成,True表示已完成,False表示未完成,None表示全部
|
||
|
||
Returns:
|
||
符合条件的任务列表
|
||
"""
|
||
return get_tasks_logic(mode=mode, keyword=keyword, priority=priority, project_name=project_name, completed=completed)
|
||
|
||
@server.tool()
|
||
def create_task(
|
||
title: Optional[str] = None,
|
||
content: Optional[str] = None,
|
||
priority: Optional[int] = None,
|
||
project_name: Optional[str] = None,
|
||
tag_names: Optional[List[str]] = None,
|
||
start_date: Optional[str] = None,
|
||
due_date: Optional[str] = None,
|
||
is_all_day: Optional[bool] = None,
|
||
reminder: Optional[str] = None,
|
||
# 官方字段(新增)
|
||
project_id: Optional[str] = None,
|
||
desc: Optional[str] = None,
|
||
time_zone: Optional[str] = None,
|
||
reminders: Optional[List[str]] = None,
|
||
repeat_flag: Optional[str] = None,
|
||
sort_order: Optional[int] = None,
|
||
items: Optional[List[Dict[str, Any]]] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
创建新任务
|
||
(调用模块级逻辑函数)
|
||
|
||
Args:
|
||
title: 任务标题
|
||
content: 任务内容
|
||
priority: 优先级 (0-最低, 1-低, 3-中, 5-高)
|
||
project_name: 项目名称
|
||
tag_names: 标签名称列表
|
||
start_date: 开始日期,格式 'YYYY-MM-DD HH:MM:SS',会自动转换为API所需的时区和格式
|
||
due_date: 截止日期,格式 'YYYY-MM-DD HH:MM:SS',会自动转换为API所需的时区和格式
|
||
is_all_day: 是否为全天任务
|
||
reminder: 提醒选项,如 "0"(准时), "-5M"(提前5分钟), "-1H"(提前1小时), "-1D"(提前1天)
|
||
|
||
Returns:
|
||
创建的任务信息
|
||
"""
|
||
return create_task_logic(title=title, content=content, priority=priority, project_name=project_name, tag_names=tag_names, start_date=start_date, due_date=due_date, is_all_day=is_all_day, reminder=reminder, project_id=project_id, desc=desc, time_zone=time_zone, reminders=reminders, repeat_flag=repeat_flag, sort_order=sort_order, items=items)
|
||
|
||
@server.tool()
|
||
def update_task(
|
||
task_id_or_title: str,
|
||
title: Optional[str] = None,
|
||
content: Optional[str] = None,
|
||
priority: Optional[int] = None,
|
||
project_name: Optional[str] = None,
|
||
tag_names: Optional[List[str]] = None,
|
||
start_date: Optional[str] = None,
|
||
due_date: Optional[str] = None,
|
||
is_all_day: Optional[bool] = None,
|
||
reminder: Optional[str] = None,
|
||
status: Optional[int] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
更新任务
|
||
(调用模块级逻辑函数)
|
||
|
||
Args:
|
||
task_id_or_title: 任务ID或任务标题
|
||
title: 新任务标题
|
||
content: 新任务内容
|
||
priority: 新优先级 (0-最低, 1-低, 3-中, 5-高)
|
||
project_name: 新项目名称
|
||
tag_names: 新标签名称列表
|
||
start_date: 新开始日期,格式 'YYYY-MM-DD HH:MM:SS',会自动转换为API所需的时区和格式
|
||
due_date: 新截止日期,格式 'YYYY-MM-DD HH:MM:SS',会自动转换为API所需的时区和格式
|
||
is_all_day: 是否为全天任务
|
||
reminder: 新提醒选项
|
||
status: 新状态,0表示未完成,2表示已完成
|
||
|
||
Returns:
|
||
更新后的任务信息
|
||
"""
|
||
return update_task_logic(task_id_or_title=task_id_or_title, title=title, content=content, priority=priority, project_name=project_name, tag_names=tag_names, start_date=start_date, due_date=due_date, is_all_day=is_all_day, reminder=reminder, status=status)
|
||
|
||
@server.tool()
|
||
def delete_task(task_id_or_title: str) -> Dict[str, Any]:
|
||
"""
|
||
删除任务
|
||
(调用模块级逻辑函数)
|
||
|
||
Args:
|
||
task_id_or_title: 任务ID或任务标题
|
||
|
||
Returns:
|
||
删除操作的响应
|
||
"""
|
||
return delete_task_logic(task_id_or_title=task_id_or_title)
|
||
|
||
@server.tool()
|
||
def complete_task(task_id_or_title: str) -> Dict[str, Any]:
|
||
"""
|
||
完成任务(官方:POST /open/v1/project/{projectId}/task/{taskId}/complete)
|
||
Args:
|
||
task_id_or_title: 任务ID或任务标题
|
||
Returns:
|
||
操作结果
|
||
"""
|
||
return complete_task_logic(task_id_or_title)
|
||
|
||
# 导出可供外部引用的函数
|
||
__all__ = [
|
||
'get_tasks_logic',
|
||
'create_task_logic',
|
||
'update_task_logic',
|
||
'delete_task_logic',
|
||
'complete_task_logic',
|
||
'register_task_tools',
|
||
'_get_all_tasks_logic' # 如果需要外部访问
|
||
] |