""" 目标管理工具 (基于滴答清单项目和任务) """ import re import json from typing import List, Dict, Optional, Any, Union, Tuple from fastmcp import FastMCP # 导入其他工具的逻辑函数 from .project_tools import ( get_projects_logic, create_project_logic, update_project_logic, delete_project_logic ) from .task_tools import ( get_tasks_logic, create_task_logic, update_task_logic, delete_task_logic ) # 目标更新走 update_task_logic,无需直接HTTP调用 # 导入辅助函数 from utils.date.date_utils import is_valid_date, format_datetime, get_current_time from utils.text.text_analysis import normalize_keywords, match_keywords, calculate_similarity # --- 常量 --- GOAL_PROJECT_NAME = "🎯 目标管理" # 存放所有目标的项目名称 GOAL_TASK_PREFIX = "" # 目标任务的前缀 METADATA_PATTERN = re.compile(r"\[(.*?): (.*?)\]") GOAL_TYPES = ['phase', 'permanent', 'habit'] # 目标类型保持,用于描述元数据 GOAL_STATUSES = ['active', 'completed', 'abandoned'] # 目标状态 # --- 辅助函数 --- def _format_metadata(data: Dict[str, Any]) -> str: """将元数据字典格式化为任务描述字符串""" parts = [] for key, value in data.items(): if value: # 只包含有值的字段 parts.append(f"[{key.capitalize()}: {value}]") return " ".join(parts) def _parse_metadata(description: Optional[str]) -> Dict[str, Any]: """从任务描述字符串中解析元数据""" metadata = {} if description: matches = METADATA_PATTERN.findall(description) for key, value in matches: metadata[key.lower()] = value.strip() return metadata def _get_goal_project() -> Optional[str]: """ 获取目标管理项目的ID 先精确匹配项目名称,如果找不到,再尝试模糊匹配 如果不存在则返回 None """ projects = get_projects_logic() # 1. 精确匹配项目名称 for project in projects: if project.get('name') == GOAL_PROJECT_NAME: return project # 2. 模糊匹配 - 查找名称中包含"目标"和"🎯"的项目 for project in projects: project_name = project.get('name', '') if '目标' in project_name and '🎯' in project_name: print(f"找到类似的目标管理项目: {project_name}") return project # 3. 更宽松的匹配 - 只要包含"目标" for project in projects: project_name = project.get('name', '') if '目标' in project_name: print(f"找到相关的目标项目: {project_name}") return project return None def _ensure_goal_project_exists() -> str: """ 确保存在目标管理项目 如果不存在则创建,返回项目ID """ project = _get_goal_project() if project: return project print(f"未找到目标管理项目,创建新项目: {GOAL_PROJECT_NAME}") # 创建目标管理项目 project_data = create_project_logic(name=GOAL_PROJECT_NAME) project = project_data.get('id') if not project: raise ValueError(f"创建目标管理项目失败,未返回项目ID。API响应: {project_data}") return project def _enrich_goal_data(task: Dict[str, Any]) -> Dict[str, Any]: """将任务数据丰富为目标数据""" task_id = task.get('id') metadata = _parse_metadata(task.get('content')) goal_data = { "id": task_id, "title": task.get('title', ''), # 直接使用任务标题,不需要移除前缀 "description": task.get('content', ''), # 保留原始描述 "type": metadata.get('type', 'permanent'), # 默认类型 "status": 'completed' if task.get('status') == 2 or task.get('isCompleted') else 'active', "keywords": metadata.get('keywords', ''), "start_date": metadata.get('start_date'), "due_date": task.get('dueDate'), # 使用任务本身的截止日期 "frequency": metadata.get('frequency'), "created_time": task.get('createdTime'), # 使用任务创建时间 "modified_time": task.get('modifiedTime'), "priority": task.get('priority', 0), # 任务优先级 "project_id": task.get('projectId'), # 所属项目ID "raw_task_data": task # 保留原始任务数据 } return {k: v for k, v in goal_data.items() if v is not None} # --- 模块级核心逻辑函数 --- def create_goal_logic( title: str, type: str, keywords: str, description: Optional[str] = None, due_date: Optional[str] = None, start_date: Optional[str] = None, frequency: Optional[str] = None, related_projects: Optional[List[str]] = None ) -> Dict[str, Any]: """ 创建新目标 (作为任务存放在目标管理项目中) Args: title: 目标标题 type: 目标类型(phase/permanent/habit) keywords: 关键词,以逗号分隔 description: 目标描述 (会附加元数据) due_date: 截止日期 (YYYY-MM-DD) start_date: 开始日期 (YYYY-MM-DD) frequency: 频率 (用于习惯目标) related_projects: 相关项目IDs (保留参数,当前版本不使用) Returns: 创建的目标信息 (丰富后的数据) """ # 验证类型 if type not in GOAL_TYPES: raise ValueError(f"无效的目标类型: {type},应为 {GOAL_TYPES} 之一") # 验证日期格式 if due_date and not is_valid_date(due_date): raise ValueError(f"无效的截止日期格式: {due_date},应为YYYY-MM-DD") if start_date and not is_valid_date(start_date): raise ValueError(f"无效的开始日期格式: {start_date},应为YYYY-MM-DD") # 验证特定类型的字段 if type == 'phase' and not due_date: raise ValueError("阶段性目标必须指定截止日期(due_date)") if type == 'habit' and not frequency: raise ValueError("习惯性目标必须指定频率(frequency)") try: # 1. 确保目标管理项目存在 project_id = _ensure_goal_project_exists() # 2. 准备任务标题 - 直接使用传入的标题,不添加前缀 task_title = title # 3. 准备元数据 metadata = { 'type': type, 'keywords': normalize_keywords(keywords), 'start_date': start_date, 'frequency': frequency # due_date 将直接用于任务的dueDate字段 } metadata_str = _format_metadata(metadata) # 4. 组合任务内容 full_content = f"{description}\n\n--- Metadata ---\n{metadata_str}" if description else f"--- Metadata ---\n{metadata_str}" # 5. 创建任务 - 使用正确的参数名称 # 注意:task_tools.create_task_logic 期望 project_name 而不是 projectId created_task = create_task_logic( title=task_title, content=full_content, project_name=GOAL_PROJECT_NAME, # 使用项目名称而不是ID due_date=due_date, start_date=start_date, priority=3 # 默认设置为中等优先级 ) # 6. 返回丰富的目标数据 return _enrich_goal_data(created_task) except Exception as e: raise ValueError(f"创建目标失败: {e}") def get_goals_logic( type: Optional[str] = None, status: Optional[str] = None, keywords: Optional[str] = None ) -> List[Dict[str, Any]]: """ 获取目标列表 (基于任务) Args: type: 目标类型筛选 status: 目标状态筛选 (active/completed) keywords: 关键词筛选 (匹配目标标题或元数据中的关键词) Returns: 目标列表 """ project = _get_goal_project() if not project: print("未找到目标管理项目,返回空列表") return [] # 如果目标管理项目不存在,直接返回空列表 # 2. 确定完成状态参数 completed = None if status == 'completed': completed = True elif status == 'active': completed = False try: # 3. 获取项目下的所有任务 # 使用项目名称,而不是ID,以匹配task_tools的API设计 all_tasks = get_tasks_logic( mode='all', completed=completed, project_name=project.get('name') ) # 如果返回的是None或空值,返回空列表 if not all_tasks: print("项目下未找到任务,返回空列表") return [] # 4. 过滤得到目标任务 goal_list = [] # 处理关键词 search_keywords = keywords or "" if not isinstance(search_keywords, str): search_keywords = "" search_keywords_set = set(normalize_keywords(search_keywords).split(',')) if search_keywords else set() for task in all_tasks: # 由于GOAL_TASK_PREFIX为空,不用startswith判断,而是看任务是否属于目标项目 if task and isinstance(task, dict): try: goal_data = _enrich_goal_data(task) # 类型筛选 if type and goal_data.get('type') != type: continue # 关键词筛选 if search_keywords_set: goal_title_lower = goal_data.get('title', '').lower() goal_meta_keywords = set(k for k in goal_data.get('keywords', '').split(',') if k) # 检查标题或元数据关键词是否包含任何搜索关键词 if not any(sk in goal_title_lower for sk in search_keywords_set) and \ not search_keywords_set.intersection(goal_meta_keywords): continue goal_list.append(goal_data) except Exception as e: print(f"处理任务时出错,跳过: {e}") continue return goal_list except Exception as e: print(f"获取目标列表时出错: {e}") return [] # 出错时返回空列表而不是抛出异常 def get_goal_logic(goal_id: str) -> Optional[Dict[str, Any]]: """ 获取目标详情 (基于任务ID) Args: goal_id: 目标ID (即任务ID) Returns: 目标详情,如果不是目标任务或未找到则返回None """ # 尝试直接获取任务详情 try: # 获取目标项目 goal_project = _get_goal_project() if not goal_project: return None project_id = goal_project.get('id') # 获取所有任务 tasks = get_tasks_logic(mode="all") task = None for t in tasks: if t.get('id') == goal_id: task = t break if not task: return None # 验证是否属于目标项目 if task.get('projectId') != project_id: return None return _enrich_goal_data(task) except Exception as e: print(f"获取目标详情时出错: {e}") return None def update_goal_logic( goal_id: str, title: Optional[str] = None, type: Optional[str] = None, status: Optional[str] = None, keywords: Optional[str] = None, description: Optional[str] = None, due_date: Optional[str] = None, start_date: Optional[str] = None, frequency: Optional[str] = None, progress: Optional[int] = None, related_projects: Optional[List[str]] = None ) -> Dict[str, Any]: """ 更新目标 (基于任务) Args: goal_id: 目标ID (任务ID) title: 新标题 (不含前缀) type: 新类型 status: 新状态 (active/completed) keywords: 新关键词 (逗号分隔) description: 新的基础描述 (元数据会自动附加) due_date: 新截止日期 start_date: 新开始日期 frequency: 新频率 progress: 进度 (忽略) related_projects: 相关项目 (忽略) Returns: 更新后的目标数据 """ try: # 1. 获取当前目标任务 current_goal = get_goal_logic(goal_id) if not current_goal: raise ValueError(f"未找到目标任务: {goal_id}") # 2. 处理任务状态 task_status = None if status is not None: if status == 'completed': task_status = 2 # 已完成 elif status == 'active': task_status = 0 # 未开始/进行中 # 3. 处理元数据更新 new_content = None if any(param is not None for param in [type, keywords, frequency, description]): # 获取当前任务数据和元数据 raw_task_data = current_goal.get("raw_task_data", {}) current_content = raw_task_data.get('content', '') current_metadata = _parse_metadata(current_content) # 分割描述和元数据部分 content_parts = current_content.split("\n\n--- Metadata ---\n") current_desc = content_parts[0] if len(content_parts) > 1 else "" # 更新元数据 if type is not None: if type not in GOAL_TYPES: raise ValueError(f"无效类型: {type}") current_metadata['type'] = type if keywords is not None: current_metadata['keywords'] = normalize_keywords(keywords) if start_date is not None and is_valid_date(start_date): current_metadata['start_date'] = start_date if frequency is not None: current_metadata['frequency'] = frequency # 更新描述 new_desc = description if description is not None else current_desc # 构建新内容 metadata_str = _format_metadata(current_metadata) new_content = f"{new_desc}\n\n--- Metadata ---\n{metadata_str}" if new_desc else f"--- Metadata ---\n{metadata_str}" # 4. 直接调用update_task_logic更新任务 result = update_task_logic( task_id_or_title=goal_id, title=title, content=new_content, status=task_status, due_date=due_date, start_date=start_date ) # 5. 检查结果并返回 if not result.get('success'): raise ValueError(result.get('info', '更新失败,无详细错误信息')) updated_task = result.get('data') return _enrich_goal_data(updated_task) except Exception as e: raise ValueError(f"更新目标 {goal_id} 失败: {e}") def delete_goal_logic(goal_id: str) -> Dict[str, Any]: """ 删除目标 (基于任务) Args: goal_id: 目标ID (任务ID) Returns: 删除操作结果 """ # 先确认是目标任务 goal_data = get_goal_logic(goal_id) if not goal_data: raise ValueError(f"未找到目标任务: {goal_id},无法删除") # 调用任务删除逻辑 - task_id_or_title而不是task_id return delete_task_logic(task_id_or_title=goal_id) def match_task_with_goals_logic( task_title: str, task_content: Optional[str] = None, project_id: Optional[str] = None, min_score: float = 0.3 ) -> List[Dict[str, Any]]: """ 匹配任务与目标 (基于内容相似度和关键词) Args: task_title: 任务标题 task_content: 任务内容 project_id: 任务所属项目ID (不再用于直接匹配,因为所有目标都在目标管理项目下) min_score: 最小匹配分数 Returns: 匹配的目标列表,按匹配度降序排序 """ active_goals = get_goals_logic(status='active') task_text = f"{task_title} {task_content or ''}".lower() matches = [] for goal in active_goals: match_score = 0.0 # 1. 关键词和文本相似度匹配 goal_keywords_str = goal.get('keywords', '') goal_keywords_set = set(k for k in goal_keywords_str.split(',') if k) goal_title = goal.get('title', '') goal_desc = goal.get('description', '').split("\n\n--- Metadata ---\n")[0] # 只用基础描述 goal_text_match = f"{goal_title} {goal_desc}".lower() keyword_score = 0.0 similarity_score = 0.0 if goal_keywords_set: # 简单的关键词包含匹配 if any(kw.lower() in task_text for kw in goal_keywords_set): keyword_score = 0.7 # 基础分 if goal_text_match: similarity_score = calculate_similarity(task_text, goal_text_match) * 0.3 # 相似度占比较低 match_score = keyword_score + similarity_score # 添加到结果如果分数达标 if match_score >= min_score: matches.append({ 'goal': goal, 'score': round(match_score, 3) }) # 按分数排序 matches.sort(key=lambda x: x['score'], reverse=True) return [match['goal'] for match in matches] # --- MCP工具注册 --- def register_goal_tools(server: FastMCP, auth_info: Dict[str, Any]): """ 注册目标管理工具到MCP服务器 (基于任务) Args: server: MCP服务器实例 auth_info: 认证信息 (用于初始化API,如果需要) """ @server.tool() def create_goal( title: str, type: str, keywords: str, description: Optional[str] = None, due_date: Optional[str] = None, start_date: Optional[str] = None, frequency: Optional[str] = None, related_projects: Optional[List[str]] = None ) -> Dict[str, Any]: """ 创建新目标 (作为任务存放在目标管理项目中) Args: title: 目标标题 type: 目标类型 (phase/permanent/habit) keywords: 关键词,以逗号分隔 description: 目标的基础描述 (可选) due_date: 截止日期 (YYYY-MM-DD) (阶段性目标必填) start_date: 开始日期 (YYYY-MM-DD) (可选) frequency: 频率 (daily, weekly:1,3,5 等) (习惯目标必填) related_projects: 相关项目IDs (可选) Returns: 创建的目标信息 """ # 直接调用逻辑函数,逻辑函数应能处理Optional参数 try: return create_goal_logic(title, type, keywords, description, due_date, start_date, frequency) except (ValueError, NotImplementedError) as e: raise e except Exception as e: print(f"调用 create_goal 时发生意外错误: {e}") raise ValueError(f"创建目标时发生内部错误: {e}") @server.tool() def get_goals( type: Optional[str] = None, status: Optional[str] = None, keywords: Optional[str] = None ) -> List[Dict[str, Any]]: """ 获取目标列表 Args: type: 目标类型筛选 (phase/permanent/habit) status: 目标状态筛选 (active/completed) keywords: 关键词筛选 (匹配目标标题或关键词) - 字符串形式 Returns: 目标列表 """ # 直接调用逻辑函数 try: return get_goals_logic(type=type, status=status, keywords=keywords) except Exception as e: print(f"调用 get_goals 时发生意外错误: {e}") raise ValueError(f"获取目标列表时发生内部错误: {e}") @server.tool() def get_goal(goal_id: str) -> Dict[str, Any]: """ 获取目标详情 Args: goal_id: 目标ID (任务ID) Returns: 目标详情 """ try: goal = get_goal_logic(goal_id) if not goal: raise ValueError(f"未找到ID为 '{goal_id}' 的目标") return goal except Exception as e: print(f"调用 get_goal 时发生意外错误: {e}") raise ValueError(f"获取目标 '{goal_id}' 时发生内部错误: {e}") @server.tool() def update_goal( goal_id: str, title: Optional[str] = None, type: Optional[str] = None, status: Optional[str] = None, keywords: Optional[str] = None, description: Optional[str] = None, due_date: Optional[str] = None, start_date: Optional[str] = None, frequency: Optional[str] = None, progress: Optional[int] = None, related_projects: Optional[List[str]] = None ) -> Dict[str, Any]: """ 更新目标 Args: goal_id: 目标ID (任务ID) title: 新标题 (可选) type: 新类型 (phase/permanent/habit) (可选) status: 新状态 (active/completed) (可选) keywords: 新关键词 (逗号分隔) (可选) description: 新的基础描述 (可选) due_date: 新截止日期 (YYYY-MM-DD) (可选) start_date: 新开始日期 (YYYY-MM-DD) (可选) frequency: 新频率 (可选) progress: 进度 (忽略) related_projects: 相关项目 (忽略) Returns: 更新后的目标数据 """ # 直接调用逻辑函数 try: return update_goal_logic(goal_id, title, type, status, keywords, description, due_date, start_date, frequency) except (ValueError, NotImplementedError) as e: raise e except Exception as e: print(f"调用 update_goal 时发生意外错误: {e}") raise ValueError(f"更新目标 '{goal_id}' 时发生内部错误: {e}") @server.tool() def delete_goal(goal_id: str) -> Dict[str, Any]: """ 删除目标 Args: goal_id: 目标ID (任务ID) Returns: 删除操作的结果 """ try: return delete_goal_logic(goal_id) except (ValueError, NotImplementedError) as e: raise e except Exception as e: print(f"调用 delete_goal 时发生意外错误: {e}") raise ValueError(f"删除目标 '{goal_id}' 时发生内部错误: {e}") @server.tool() def match_task_with_goals( task_title: str, task_content: Optional[str] = None, project_id: Optional[str] = None ) -> List[Dict[str, Any]]: """ 匹配任务与目标 Args: task_title: 任务标题 task_content: 任务内容 (可选) project_id: 任务所属项目ID (可选) Returns: 匹配的目标列表 (按匹配度排序) """ # 直接调用逻辑函数 try: return match_task_with_goals_logic(task_title, task_content, project_id) except Exception as e: print(f"调用 match_task_with_goals 时发生意外错误: {e}") raise ValueError(f"匹配任务与目标时发生内部错误: {e}") # 导出 __all__ = [ 'create_goal_logic', 'get_goals_logic', 'get_goal_logic', 'update_goal_logic', 'delete_goal_logic', 'match_task_with_goals_logic', 'register_goal_tools' ]