Files
moss-ai/mcp/didatodolist-mcp/tools/goal_tools.py
雷雨 8635b84b2d init
2025-12-15 22:05:56 +08:00

686 lines
24 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
目标管理工具 (基于滴答清单项目和任务)
"""
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'
]