219 lines
7.6 KiB
Python
219 lines
7.6 KiB
Python
# modules/todo_manager.py - TODO 列表管理
|
||
|
||
from __future__ import annotations
|
||
|
||
from copy import deepcopy
|
||
from typing import Dict, List, Any, Optional
|
||
|
||
try:
|
||
from config import (
|
||
TODO_MAX_TASKS,
|
||
TODO_MAX_OVERVIEW_LENGTH,
|
||
TODO_MAX_TASK_LENGTH,
|
||
)
|
||
except ImportError: # pragma: no cover
|
||
import sys
|
||
from pathlib import Path
|
||
|
||
project_root = Path(__file__).resolve().parents[1]
|
||
if str(project_root) not in sys.path:
|
||
sys.path.insert(0, str(project_root))
|
||
from config import ( # type: ignore
|
||
TODO_MAX_TASKS,
|
||
TODO_MAX_OVERVIEW_LENGTH,
|
||
TODO_MAX_TASK_LENGTH,
|
||
)
|
||
|
||
|
||
class TodoManager:
|
||
"""负责创建、更新和结束 TODO 列表"""
|
||
|
||
MAX_TASKS = TODO_MAX_TASKS
|
||
MAX_OVERVIEW_LENGTH = TODO_MAX_OVERVIEW_LENGTH
|
||
MAX_TASK_LENGTH = TODO_MAX_TASK_LENGTH
|
||
|
||
def __init__(self, context_manager):
|
||
self.context_manager = context_manager
|
||
|
||
def _get_current(self) -> Optional[Dict[str, Any]]:
|
||
todo = getattr(self.context_manager, "todo_list", None)
|
||
return deepcopy(todo) if todo else None
|
||
|
||
def _save(self, todo: Optional[Dict[str, Any]]):
|
||
self.context_manager.set_todo_list(todo)
|
||
|
||
def _normalize_tasks(self, tasks: List[Any]) -> List[str]:
|
||
normalized = []
|
||
for item in tasks:
|
||
title = ""
|
||
if isinstance(item, dict):
|
||
title = item.get("title", "")
|
||
else:
|
||
title = str(item)
|
||
title = title.strip()
|
||
if not title:
|
||
continue
|
||
normalized.append(title)
|
||
if len(normalized) >= self.MAX_TASKS:
|
||
break
|
||
return normalized
|
||
|
||
def create_todo_list(self, overview: str, tasks: List[Any]) -> Dict[str, Any]:
|
||
current = self._get_current()
|
||
if current and current.get("status") == "active":
|
||
return {
|
||
"success": False,
|
||
"error": "已有进行中的 TODO 列表,请先完成或结束后再创建新的列表。"
|
||
}
|
||
|
||
overview = (overview or "").strip()
|
||
if not overview:
|
||
return {"success": False, "error": "任务概述不能为空。"}
|
||
if len(overview) > self.MAX_OVERVIEW_LENGTH:
|
||
return {
|
||
"success": False,
|
||
"error": f"任务概述过长(当前 {len(overview)} 字),请精简至 {self.MAX_OVERVIEW_LENGTH} 字以内。"
|
||
}
|
||
|
||
normalized_tasks = self._normalize_tasks(tasks or [])
|
||
if not normalized_tasks:
|
||
return {"success": False, "error": "需要至少提供一个任务。"}
|
||
if len(tasks or []) > self.MAX_TASKS:
|
||
return {
|
||
"success": False,
|
||
"error": f"任务数量过多,最多允许 {self.MAX_TASKS} 个任务。"
|
||
}
|
||
|
||
for title in normalized_tasks:
|
||
if len(title) > self.MAX_TASK_LENGTH:
|
||
return {
|
||
"success": False,
|
||
"error": f"任务「{title}」过长,请控制在 {self.MAX_TASK_LENGTH} 字以内。"
|
||
}
|
||
|
||
todo = {
|
||
"overview": overview,
|
||
"tasks": [
|
||
{
|
||
"index": idx,
|
||
"title": title,
|
||
"status": "pending"
|
||
}
|
||
for idx, title in enumerate(normalized_tasks, start=1)
|
||
],
|
||
"status": "active",
|
||
"forced_finish": False,
|
||
"forced_reason": None
|
||
}
|
||
|
||
self._save(todo)
|
||
return {
|
||
"success": True,
|
||
"message": "待办列表已创建。请先完成某项任务,再调用待办工具将其标记完成。",
|
||
"todo_list": todo
|
||
}
|
||
|
||
def update_task_status(self, task_index: int, completed: bool) -> Dict[str, Any]:
|
||
todo = self._get_current()
|
||
if not todo:
|
||
return {"success": False, "error": "当前没有待办列表,请先创建。"}
|
||
if todo.get("status") in {"completed", "closed"}:
|
||
return {"success": False, "error": "待办列表已结束,无法继续修改。"}
|
||
|
||
if not isinstance(task_index, int):
|
||
return {"success": False, "error": "task_index 必须是数字。"}
|
||
if task_index < 1 or task_index > len(todo["tasks"]):
|
||
return {"success": False, "error": f"task_index 超出范围(1-{len(todo['tasks'])})。"}
|
||
|
||
task = todo["tasks"][task_index - 1]
|
||
new_status = "done" if completed else "pending"
|
||
if task["status"] == new_status:
|
||
return {
|
||
"success": True,
|
||
"message": "任务状态未发生变化。",
|
||
"todo_list": todo
|
||
}
|
||
|
||
task["status"] = new_status
|
||
|
||
self._save(todo)
|
||
return {
|
||
"success": True,
|
||
"message": f"任务 task{task_index} 已标记为 {'完成' if completed else '未完成'}。",
|
||
"todo_list": todo
|
||
}
|
||
|
||
def finish_todo(self, reason: Optional[str] = None) -> Dict[str, Any]:
|
||
todo = self._get_current()
|
||
if not todo:
|
||
return {"success": False, "error": "当前没有待办列表。"}
|
||
|
||
if todo.get("status") in {"completed", "closed"}:
|
||
return {
|
||
"success": True,
|
||
"message": "待办列表已结束,无需重复操作。",
|
||
"todo_list": todo
|
||
}
|
||
|
||
all_done = all(task["status"] == "done" for task in todo["tasks"])
|
||
if all_done:
|
||
todo["status"] = "completed"
|
||
todo["forced_finish"] = False
|
||
todo["forced_reason"] = None
|
||
self._save(todo)
|
||
system_note = "✅ TODO 列表中的所有任务已完成,可以整理成果并向用户汇报。"
|
||
return {
|
||
"success": True,
|
||
"message": "所有任务已完成,待办列表已结束。",
|
||
"todo_list": todo,
|
||
"system_note": system_note
|
||
}
|
||
|
||
remaining = [
|
||
f"task{task['index']}"
|
||
for task in todo["tasks"]
|
||
if task["status"] != "done"
|
||
]
|
||
return {
|
||
"success": False,
|
||
"requires_confirmation": True,
|
||
"message": "仍有未完成的任务,确认要提前结束吗?",
|
||
"remaining": remaining,
|
||
"todo_list": todo
|
||
}
|
||
|
||
def confirm_finish(self, confirm: bool, reason: Optional[str] = None) -> Dict[str, Any]:
|
||
todo = self._get_current()
|
||
if not todo:
|
||
return {"success": False, "error": "当前没有待办列表。"}
|
||
if todo.get("status") in {"completed", "closed"}:
|
||
return {
|
||
"success": True,
|
||
"message": "待办列表已结束,无需重复操作。",
|
||
"todo_list": todo
|
||
}
|
||
|
||
if not confirm:
|
||
return {
|
||
"success": True,
|
||
"message": "已取消结束待办列表,继续执行剩余任务。",
|
||
"todo_list": todo
|
||
}
|
||
|
||
todo["status"] = "closed"
|
||
todo["forced_finish"] = True
|
||
todo["forced_reason"] = (reason or "").strip() or None
|
||
self._save(todo)
|
||
|
||
system_note = "⚠️ TODO 列表在任务未全部完成的情况下被结束,请在总结中说明原因。"
|
||
self.context_manager.add_conversation("system", system_note)
|
||
return {
|
||
"success": True,
|
||
"message": "待办列表已强制结束。",
|
||
"todo_list": todo,
|
||
"system_note": system_note
|
||
}
|
||
|
||
def get_snapshot(self) -> Optional[Dict[str, Any]]:
|
||
return self._get_current()
|