Files
moss-ai/mcp/didatodolist-mcp/tools/goal_tools.py

686 lines
24 KiB
Python
Raw Normal View History

2025-12-15 22:05:56 +08:00
"""
目标管理工具 (基于滴答清单项目和任务)
"""
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'
]