Files
JARVIS/docs/superpowers/plans/2026-03-20-daily-todo-plan.md

32 KiB
Raw Blame History

Daily Todo Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 实现每日待办功能——AI 每天早上自动预生成今日待办(看板未完成任务+前一天对话分析),用户可手动增删改。

Architecture:

  • 后端FastAPI Router + SQLAlchemy Model + LLM Service 分析对话
  • 前端Vue 3 Composition APITodoView 独立页面
  • 数据库:新表 daily_todos,按日期过滤查询

Tech Stack: FastAPI + SQLAlchemy + APScheduler + LLM Service / Vue 3 + Motion + axios


文件总览

backend/
  app/
    models/
      __init__.py          # 修改:导出 DailyTodo
      todo.py              # 新建DailyTodo 模型
    schemas/
      todo.py              # 新建Pydantic Schema
    routers/
      todo.py              # 新建API Router
    services/
      todo_service.py      # 新建AI 分析逻辑
    main.py                # 修改:注册 todo router

frontend/
  src/
    api/
      todo.ts              # 新建API 客户端
    views/
      TodoView.vue         # 新建:待办页面
    router/
      index.ts             # 修改:添加 /todo 路由
    components/
      SidebarNav.vue       # 修改:添加待办菜单

Task 1: 后端 - 数据模型

Files:

  • Create: backend/app/models/todo.py

  • Modify: backend/app/models/__init__.py

  • Step 1: 创建 app/models/todo.py

参考现有 app/models/task.py 的写法:

from sqlalchemy import Column, String, Boolean, DateTime, Enum as SQLEnum
from datetime import datetime
from enum import Enum as PyEnum
from app.models.base import BaseModel


class TodoSource(str, PyEnum):
    AI_KANBAN = "ai_kanban"
    AI_CHAT = "ai_chat"
    MANUAL = "manual"


class DailyTodo(BaseModel):
    __tablename__ = "daily_todos"

    user_id = Column(String(36), nullable=False, index=True)
    title = Column(String(500), nullable=False)
    is_completed = Column(Boolean, default=False, nullable=False)
    source = Column(SQLEnum(TodoSource), default=TodoSource.MANUAL, nullable=False)
    source_detail = Column(String(500), nullable=True)
    source_ref_id = Column(String(36), nullable=True)
    todo_date = Column(String(10), nullable=False)  # YYYY-MM-DD
    completed_at = Column(DateTime, nullable=True)
  • Step 2: 修改 app/models/__init__.py,添加导出

在文件末尾添加:

from app.models.todo import DailyTodo
__all__ = [..., "DailyTodo"]
  • Step 3: 提交
git add backend/app/models/todo.py backend/app/models/__init__.py
git commit -m "feat(todo): add DailyTodo model"

Task 2: 后端 - Schema 定义

Files:

  • Create: backend/app/schemas/todo.py

  • Step 1: 创建 app/schemas/todo.py

from pydantic import BaseModel
from datetime import datetime
from app.models.todo import TodoSource


class TodoCreate(BaseModel):
    title: str


class TodoUpdate(BaseModel):
    title: str | None = None
    is_completed: bool | None = None


class TodoOut(BaseModel):
    id: str
    title: str
    is_completed: bool
    source: TodoSource
    source_detail: str | None
    todo_date: str
    completed_at: datetime | None
    created_at: datetime
    updated_at: datetime

    model_config = {"from_attributes": True}


class TodoListOut(BaseModel):
    items: list[TodoOut]
    total: int
    page: int
    page_size: int


class TodoSummaryOut(BaseModel):
    date: str
    total: int
    completed: int
    pending: int
  • Step 2: 提交
git add backend/app/schemas/todo.py
git commit -m "feat(todo): add Pydantic schemas"

Task 3: 后端 - API Router

Files:

  • Create: backend/app/routers/todo.py

  • Step 1: 创建 app/routers/todo.py

完整实现以下端点:

from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func
from datetime import date
from app.database import get_db
from app.models.todo import DailyTodo, TodoSource
from app.models.user import User
from app.routers.auth import get_current_user
from app.schemas.todo import (
    TodoCreate, TodoUpdate, TodoOut, TodoListOut, TodoSummaryOut
)
from app.services.todo_service import generate_daily_todos

router = APIRouter(prefix="/api/todos", tags=["待办"])


@router.get("", response_model=TodoListOut)
async def list_todos(
    date_str: str = Query(default=None),  # YYYY-MM-DD默认当天
    page: int = Query(default=1, ge=1),
    page_size: int = Query(default=50, ge=1, le=100),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    target_date = date_str or date.today().isoformat()
    offset = (page - 1) * page_size

    # 查询总数
    count_q = select(func.count()).select_from(DailyTodo).where(
        DailyTodo.user_id == current_user.id,
        DailyTodo.todo_date == target_date,
    )
    total = (await db.execute(count_q)).scalar()

    # 查询列表
    q = select(DailyTodo).where(
        DailyTodo.user_id == current_user.id,
        DailyTodo.todo_date == target_date,
    ).order_by(DailyTodo.created_at.desc()).offset(offset).limit(page_size)

    items = (await db.execute(q)).scalars().all()
    return TodoListOut(items=items, total=total, page=page, page_size=page_size)


@router.post("", response_model=TodoOut, status_code=201)
async def create_todo(
    data: TodoCreate,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    todo = DailyTodo(
        user_id=current_user.id,
        title=data.title,
        source=TodoSource.MANUAL,
        todo_date=date.today().isoformat(),
    )
    db.add(todo)
    await db.commit()
    await db.refresh(todo)
    return todo


@router.patch("/{todo_id}", response_model=TodoOut)
async def update_todo(
    todo_id: str,
    data: TodoUpdate,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(DailyTodo).where(DailyTodo.id == todo_id, DailyTodo.user_id == current_user.id)
    )
    todo = result.scalar_one_or_none()
    if not todo:
        raise HTTPException(status_code=404, detail="待办不存在")

    # 历史日期不允许修改
    if todo.todo_date != date.today().isoformat():
        raise HTTPException(status_code=403, detail="历史待办不可修改")

    if data.title is not None:
        todo.title = data.title
    if data.is_completed is not None:
        from datetime import datetime
        todo.is_completed = data.is_completed
        todo.completed_at = datetime.utcnow() if data.is_completed else None

    await db.commit()
    await db.refresh(todo)
    return todo


@router.delete("/{todo_id}", status_code=204)
async def delete_todo(
    todo_id: str,
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    result = await db.execute(
        select(DailyTodo).where(DailyTodo.id == todo_id, DailyTodo.user_id == current_user.id)
    )
    todo = result.scalar_one_or_none()
    if not todo:
        raise HTTPException(status_code=404, detail="待办不存在")
    if todo.todo_date != date.today().isoformat():
        raise HTTPException(status_code=403, detail="历史待办不可删除")

    await db.delete(todo)
    await db.commit()


@router.post("/ai-generate", response_model=TodoListOut)
async def ai_generate_todos(
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    target_date = date.today().isoformat()

    # 幂等检查是否已有AI生成记录
    check_q = select(func.count()).select_from(DailyTodo).where(
        DailyTodo.user_id == current_user.id,
        DailyTodo.todo_date == target_date,
        DailyTodo.source.in_([TodoSource.AI_KANBAN, TodoSource.AI_CHAT]),
    )
    count = (await db.execute(check_q)).scalar()

    if count > 0:
        # 已生成,返回现有记录
        q = select(DailyTodo).where(
            DailyTodo.user_id == current_user.id,
            DailyTodo.todo_date == target_date,
        ).order_by(DailyTodo.created_at.desc())
        items = (await db.execute(q)).scalars().all()
        return TodoListOut(items=items, total=len(items), page=1, page_size=50)

    # 执行AI生成
    todos = await generate_daily_todos(current_user.id, db)
    return TodoListOut(items=todos, total=len(todos), page=1, page_size=50)


@router.get("/summary", response_model=TodoSummaryOut)
async def get_summary(
    date_str: str = Query(default=None),
    current_user: User = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    target_date = date_str or date.today().isoformat()
    q = select(DailyTodo).where(
        DailyTodo.user_id == current_user.id,
        DailyTodo.todo_date == target_date,
    )
    todos = (await db.execute(q)).scalars().all()
    completed = sum(1 for t in todos if t.is_completed)
    return TodoSummaryOut(date=target_date, total=len(todos), completed=completed, pending=len(todos) - completed)
  • Step 2: 提交
git add backend/app/routers/todo.py
git commit -m "feat(todo): add API router with CRUD + ai-generate"

Task 4: 后端 - AI 生成逻辑 Service

Files:

  • Create: backend/app/services/todo_service.py

  • Step 1: 创建 app/services/todo_service.py

import json
import logging
from datetime import date, datetime, timedelta
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.todo import DailyTodo, TodoSource
from app.models.task import Task, TaskStatus
from app.models.conversation import Conversation, Message
from app.services.llm_service import llm_service

logger = logging.getLogger(__name__)


async def generate_daily_todos(user_id: str, db: AsyncSession) -> list[DailyTodo]:
    """
    为用户生成今日待办:
    1. 来自前一天未完成的看板任务最多20条
    2. 来自前一天对话记录分析最多3条
    """
    today = date.today()
    yesterday = (today - timedelta(days=1)).isoformat()

    todos: list[DailyTodo] = []

    # 1. 从看板任务导入
    kanban_todos = await _import_kanban_tasks(user_id, yesterday, db)
    todos.extend(kanban_todos)

    # 2. 从对话记录分析
    chat_todos = await _analyze_chat_history(user_id, yesterday, db)
    todos.extend(chat_todos)

    return todos


async def _import_kanban_tasks(user_id: str, date_str: str, db: AsyncSession) -> list[DailyTodo]:
    """导入前一天创建的、未完成的看板任务"""
    q = select(Task).where(
        Task.user_id == user_id,
        Task.status != TaskStatus.DONE,
    ).order_by(Task.created_at.desc()).limit(20)

    tasks = (await db.execute(q)).scalars().all()
    todos = []

    for task in tasks:
        todo = DailyTodo(
            user_id=user_id,
            title=task.title,
            source=TodoSource.AI_KANBAN,
            source_detail=f"看板:{task.title}",
            source_ref_id=task.id,
            todo_date=date.today().isoformat(),
        )
        db.add(todo)
        todos.append(todo)

    if todos:
        await db.commit()
        for todo in todos:
            await db.refresh(todo)

    return todos


async def _analyze_chat_history(user_id: str, date_str: str, db: AsyncSession) -> list[DailyTodo]:
    """分析前一天对话,提取待办事项"""
    try:
        # 查询前一天创建的对话
        conv_q = select(Conversation).where(
            Conversation.user_id == user_id,
        ).order_by(Conversation.created_at.desc()).limit(10)
        convs = (await db.execute(conv_q)).scalars().all()

        # 过滤出昨天的对话
        yesterday_convs = []
        for conv in convs:
            created = conv.created_at
            if hasattr(created, 'date'):
                created_date = created.date() if hasattr(created, 'date') else created
            else:
                created_date = datetime.fromisoformat(str(created)).date()

            if str(created_date) == date_str or (created + timedelta(hours=8)).strftime('%Y-%m-%d') == date_str:
                yesterday_convs.append(conv)

        if not yesterday_convs:
            return []

        # 收集消息内容限制2000字
        messages_content = []
        for conv in yesterday_convs:
            msg_q = select(Message).where(
                Message.conversation_id == conv.id
            ).order_by(Message.created_at.asc()).limit(50)
            msgs = (await db.execute(msg_q)).scalars().all()
            for msg in msgs:
                if msg.content:
                    messages_content.append(f"[{msg.role}]: {msg.content[:500]}")

        if not messages_content:
            return []

        full_text = "\n".join(messages_content)[:2000]

        # 调用 LLM 分析
        prompt = f"""你是一个任务规划助手。请分析以下对话记录,提取其中用户想要完成但尚未明确完成的事项。

要求:
- 最多提取 3 条
- 每条格式:{{"title": "事项描述50字以内", "reason": "来源说明60字以内"}}
- 只提取用户明确表达过需求但还未完成的事项
- 如果没有可提取的内容,返回空数组 []

对话记录:
{full_text}

返回 JSON 数组:"""

        response = await llm_service.chat(prompt=prompt, system=None)
        content = response if isinstance(response, str) else (response.get("content") or "")

        # 尝试解析 JSON
        try:
            # 提取 JSON 数组
            start = content.find('[')
            end = content.rfind(']') + 1
            if start != -1 and end > start:
                items = json.loads(content[start:end])
            else:
                items = []
        except (json.JSONDecodeError, ValueError):
            logger.warning(f"LLM 返回格式异常,跳过对话分析: {content[:200]}")
            items = []

        if not items:
            return []

        todos = []
        for item in items[:3]:
            todo = DailyTodo(
                user_id=user_id,
                title=item.get("title", "")[:500],
                source=TodoSource.AI_CHAT,
                source_detail=f"对话:{item.get('reason', '')[:60]}",
                todo_date=date.today().isoformat(),
            )
            db.add(todo)
            todos.append(todo)

        if todos:
            await db.commit()
            for todo in todos:
                await db.refresh(todo)

        return todos

    except Exception as e:
        logger.error(f"对话分析失败: {e}")
        return []
  • Step 2: 提交
git add backend/app/services/todo_service.py
git commit -m "feat(todo): add AI generation service"

Task 5: 后端 - 注册路由

Files:

  • Modify: backend/app/main.py

  • Step 1: 在 main.py 中添加 todo router

找到其他 router 注册的位置(如 app/routers/conversation 等),添加:

from app.routers.todo import router as todo_router
app.include_router(todo_router)
  • Step 2: 提交
git add backend/app/main.py
git commit -m "feat(todo): register todo router in main"

Task 6: 后端 - 定时任务APScheduler

Files:

  • Modify: backend/app/routers/scheduler.py

  • Create: backend/app/services/scheduler_service.py (或修改现有)

  • Step 1: 查看现有的 scheduler.py 结构

读取 backend/app/routers/scheduler.pybackend/app/services/scheduler_service.py,了解现有定时任务模式。

  • Step 2: 添加每日 AI 生成定时任务

在 scheduler_service 中添加:

async def daily_todo_generation():
    """每天早上8点为所有活跃用户生成待办"""
    from app.database import async_session
    from app.models.user import User
    from sqlalchemy import select
    from app.services.todo_service import generate_daily_todos

    async with async_session() as db:
        result = await db.execute(select(User).where(User.is_active == True))
        users = result.scalars().all()
        for user in users:
            try:
                await generate_daily_todos(user.id, db)
            except Exception as e:
                logger.error(f"用户 {user.id} 定时生成待办失败: {e}")

在 APScheduler 中注册:

scheduler.add_job(
    daily_todo_generation,
    "cron",
    hour=8, minute=0,
    id="daily_todo_generation",
    replace_existing=True,
)
  • Step 3: 提交
git add backend/app/services/scheduler_service.py
git commit -m "feat(todo): add daily AI todo generation scheduler"

Task 7: 前端 - API 客户端

Files:

  • Create: frontend/src/api/todo.ts

  • Step 1: 创建 src/api/todo.ts

参考 src/api/task.ts 的模式:

import api from './index'

export type TodoSource = 'ai_kanban' | 'ai_chat' | 'manual'

export interface Todo {
  id: string
  title: string
  is_completed: boolean
  source: TodoSource
  source_detail: string | null
  todo_date: string
  completed_at: string | null
  created_at: string
  updated_at: string
}

export interface TodoListResponse {
  items: Todo[]
  total: number
  page: number
  page_size: number
}

export interface TodoSummary {
  date: string
  total: number
  completed: number
  pending: number
}

export const todoApi = {
  list(date?: string, page = 1, pageSize = 50) {
    return api.get<TodoListResponse>('/api/todos', {
      params: date ? { date_str: date, page, page_size: pageSize } : { page, page_size: pageSize },
    })
  },

  create(title: string) {
    return api.post<Todo>('/api/todos', { title })
  },

  update(id: string, data: { title?: string; is_completed?: boolean }) {
    return api.patch<Todo>(`/api/todos/${id}`, data)
  },

  delete(id: string) {
    return api.delete(`/api/todos/${id}`)
  },

  aiGenerate() {
    return api.post<TodoListResponse>('/api/todos/ai-generate')
  },

  summary(date?: string) {
    return api.get<TodoSummary>('/api/todos/summary', {
      params: date ? { date_str: date } : {},
    })
  },
}
  • Step 2: 提交
git add frontend/src/api/todo.ts
git commit -m "feat(todo): add todo API client"

Task 8: 前端 - 待办页面 TodoView.vue

Files:

  • Create: frontend/src/views/TodoView.vue

  • Step 1: 创建 TodoView.vue

页面布局:顶部日期导航 + 待办列表,参考 AgentView 的 sci-fi 风格

核心结构:

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { todoApi, type Todo } from '@/api/todo'
import { CheckSquare, Plus, Sparkles, ChevronLeft, ChevronRight, Calendar } from 'lucide-vue-next'
import { animate } from 'motion'

// 状态
const selectedDate = ref(new Date().toISOString().slice(0, 10)) // YYYY-MM-DD
const todos = ref<Todo[]>([])
const loading = ref(false)
const generating = ref(false)
const newTitle = ref('')
const editingId = ref<string | null>(null)
const editTitle = ref('')

const isToday = computed(() => selectedDate.value === new Date().toISOString().slice(0, 10))

// 日期快捷切换
function goToday() { selectedDate.value = new Date().toISOString().slice(0, 10) }
function goYesterday() {
  const d = new Date(); d.setDate(d.getDate() - 1)
  selectedDate.value = d.toISOString().slice(0, 10)
}
function goBeforeYesterday() {
  const d = new Date(); d.setDate(d.getDate() - 2)
  selectedDate.value = d.toISOString().slice(0, 10)
}

// 加载数据
async function loadTodos() {
  loading.value = true
  try {
    const res = await todoApi.list(selectedDate.value)
    todos.value = res.data.items
  } finally {
    loading.value = false
  }
}

// 新增
async function addTodo() {
  if (!newTitle.value.trim()) return
  const res = await todoApi.create(newTitle.value.trim())
  todos.value.unshift(res.data)
  newTitle.value = ''
}

// 切换完成状态
async function toggleComplete(todo: Todo) {
  const res = await todoApi.update(todo.id, { is_completed: !todo.is_completed })
  const idx = todos.value.findIndex(t => t.id === todo.id)
  if (idx !== -1) todos.value[idx] = res.data
}

// 删除
async function deleteTodo(id: string) {
  await todoApi.delete(id)
  todos.value = todos.value.filter(t => t.id !== id)
}

// AI 生成
async function aiGenerate() {
  generating.value = true
  try {
    const res = await todoApi.aiGenerate()
    todos.value = res.data.items
  } finally {
    generating.value = false
  }
}

onMounted(loadTodos)
</script>

<template>
  <div class="todo-view scanlines">
    <!-- 背景复用 AgentView 的背景层 -->
    <div class="bg-grid"></div>
    <div class="bg-glow"></div>

    <!-- Header -->
    <div class="view-header">
      <div class="header-title">
        <CheckSquare :size="16" />
        <span class="title-bracket">[</span>
        <span class="title-text">DAILY TODO</span>
        <span class="title-bracket">]</span>
      </div>
      <div class="header-actions">
        <div v-if="isToday" class="ai-btn" @click="aiGenerate" :class="{ loading: generating }">
          <Sparkles :size="14" :class="{ 'ai-spin': generating }" />
          <span>{{ generating ? '生成中...' : 'AI 规划今日' }}</span>
        </div>
      </div>
    </div>

    <!-- 日期导航 -->
    <div class="date-nav">
      <button class="date-btn" :class="{ active: !isToday }" @click="goBeforeYesterday">前天</button>
      <button class="date-btn" @click="goYesterday">昨天</button>
      <button class="date-btn primary" :class="{ active: isToday }" @click="goToday">
        今天
        <Calendar :size="12" />
      </button>
    </div>

    <!-- 主内容 -->
    <div class="todo-content">
      <!-- 今日新增输入框 -->
      <div v-if="isToday" class="add-form">
        <input
          v-model="newTitle"
          class="add-input"
          placeholder="输入待办事项,按回车添加..."
          @keyup.enter="addTodo"
        />
        <button class="add-btn" @click="addTodo">
          <Plus :size="16" />
        </button>
      </div>

      <!-- 待办列表 -->
      <div class="todo-list">
        <div
          v-for="todo in todos"
          :key="todo.id"
          class="todo-item"
          :class="{ completed: todo.is_completed, 'ai-source': todo.source !== 'manual' }"
        >
          <button class="check-btn" @click="toggleComplete(todo)">
            <span class="check-box" :class="{ checked: todo.is_completed }">
              <span v-if="todo.is_completed" class="check-mark"></span>
            </span>
          </button>
          <div class="todo-content">
            <span class="todo-title">{{ todo.title }}</span>
            <span v-if="todo.source_detail" class="todo-source">{{ todo.source_detail }}</span>
          </div>
          <button v-if="isToday" class="del-btn" @click="deleteTodo(todo.id)">
            <span>×</span>
          </button>
        </div>

        <!-- 空状态 -->
        <div v-if="!loading && todos.length === 0" class="empty-state">
          <span class="empty-icon">[ ]</span>
          <span class="empty-text">{{ isToday ? '今日待办为空,点击上方新增' : '该日无待办记录' }}</span>
        </div>

        <!-- 加载中 -->
        <div v-if="loading" class="loading-state">
          <span class="loading-text">LOADING...</span>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
/* 布局 */
.todo-view {
  display: flex;
  flex-direction: column;
  height: 100%;
  background: var(--bg-void);
  position: relative;
  overflow: hidden;
}

.view-header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 24px;
  border-bottom: 1px solid var(--border-dim);
  background: rgba(5,8,16,0.6);
  backdrop-filter: blur(8px);
  position: relative;
  z-index: 10;
}

.header-title {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: var(--font-display);
  font-size: 13px;
  letter-spacing: 0.2em;
  color: var(--text-primary);
}
.title-bracket { color: var(--accent-cyan); opacity: 0.6; }

.ai-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 6px 14px;
  background: rgba(249,168,37,0.08);
  border: 1px solid rgba(249,168,37,0.3);
  border-radius: var(--radius-sm);
  color: var(--accent-amber);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  cursor: pointer;
  transition: all var(--transition-fast);
}
.ai-btn:hover { background: rgba(249,168,37,0.15); border-color: var(--accent-amber); box-shadow: 0 0 12px rgba(249,168,37,0.2); }
.ai-spin { animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }

/* 日期导航 */
.date-nav {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  padding: 12px;
  border-bottom: 1px solid var(--border-dim);
}
.date-btn {
  padding: 5px 14px;
  background: transparent;
  border: 1px solid var(--border-mid);
  border-radius: var(--radius-sm);
  color: var(--text-dim);
  font-family: var(--font-mono);
  font-size: 11px;
  letter-spacing: 0.1em;
  cursor: pointer;
  transition: all var(--transition-fast);
  display: flex;
  align-items: center;
  gap: 6px;
}
.date-btn:hover { border-color: var(--accent-cyan); color: var(--accent-cyan); }
.date-btn.active { border-color: var(--accent-cyan); color: var(--accent-cyan); background: rgba(0,245,212,0.08); }
.date-btn.primary { font-weight: 600; }

/* 内容区 */
.todo-content {
  flex: 1;
  overflow-y: auto;
  padding: 20px 24px;
}

/* 新增表单 */
.add-form {
  display: flex;
  gap: 8px;
  margin-bottom: 20px;
}
.add-input {
  flex: 1;
  padding: 10px 16px;
  background: var(--bg-card);
  border: 1px solid var(--border-mid);
  border-radius: var(--radius-md);
  color: var(--text-primary);
  font-family: var(--font-mono);
  font-size: 13px;
  transition: all var(--transition-fast);
}
.add-input:focus {
  outline: none;
  border-color: var(--accent-cyan);
  box-shadow: 0 0 0 2px rgba(0,245,212,0.1);
}
.add-input::placeholder { color: var(--text-dim); }

.add-btn {
  width: 40px;
  height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: rgba(0,245,212,0.08);
  border: 1px solid rgba(0,245,212,0.3);
  border-radius: var(--radius-md);
  color: var(--accent-cyan);
  cursor: pointer;
  transition: all var(--transition-fast);
}
.add-btn:hover { background: rgba(0,245,212,0.15); box-shadow: var(--glow-cyan); }

/* 待办列表 */
.todo-list {
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.todo-item {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: var(--bg-card);
  border: 1px solid var(--border-dim);
  border-radius: var(--radius-md);
  transition: all var(--transition-fast);
}
.todo-item:hover { border-color: var(--border-mid); }
.todo-item.ai-source { border-left: 2px solid var(--accent-amber); }
.todo-item.completed { opacity: 0.5; }
.todo-item.completed .todo-title { text-decoration: line-through; color: var(--text-dim); }

.check-btn {
  background: none;
  border: none;
  cursor: pointer;
  padding: 0;
  display: flex;
  align-items: center;
}
.check-box {
  width: 18px;
  height: 18px;
  border: 1px solid var(--border-mid);
  border-radius: 4px;
  display: flex;
  align-items: center;
  justify-content: center;
  transition: all var(--transition-fast);
}
.check-box.checked { background: var(--accent-cyan); border-color: var(--accent-cyan); }
.check-mark { color: var(--bg-void); font-size: 12px; font-weight: bold; }

.todo-content { flex: 1; min-width: 0; }
.todo-title { display: block; font-size: 13px; color: var(--text-primary); font-family: var(--font-mono); }
.todo-source { display: block; font-size: 10px; color: var(--text-dim); margin-top: 3px; font-family: var(--font-mono); }

.del-btn {
  width: 24px;
  height: 24px;
  display: flex;
  align-items: center;
  justify-content: center;
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  font-size: 18px;
  border-radius: 4px;
  transition: all var(--transition-fast);
}
.del-btn:hover { color: var(--accent-red); background: rgba(255,71,87,0.1); }

/* 空/加载状态 */
.empty-state, .loading-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 60px 20px;
  gap: 12px;
}
.empty-icon { font-family: var(--font-mono); font-size: 32px; color: var(--text-dim); opacity: 0.3; }
.empty-text { font-family: var(--font-mono); font-size: 12px; color: var(--text-dim); letter-spacing: 0.1em; }
.loading-text { font-family: var(--font-mono); font-size: 11px; color: var(--accent-cyan); letter-spacing: 0.2em; animation: pulse 1s ease-in-out infinite; }
@keyframes pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 1; } }
</style>
  • Step 2: 提交
git add frontend/src/views/TodoView.vue
git commit -m "feat(todo): add TodoView page"

Task 9: 前端 - 路由和侧边栏

Files:

  • Modify: frontend/src/router/index.ts

  • Modify: frontend/src/components/SidebarNav.vue

  • Step 1: 在 router/index.ts 中添加 /todo 路由

在 children 数组中添加:

{
  path: 'todo',
  name: 'todo',
  component: () => import('@/views/TodoView.vue'),
},
  • Step 2: 在 SidebarNav.vue 中添加待办菜单项

在 navItems 数组中添加:

{ name: '待办', path: '/todo', icon: CheckSquare },

导入 CheckSquare

import { CheckSquare } from 'lucide-vue-next'
  • Step 3: 提交
git add frontend/src/router/index.ts frontend/src/components/SidebarNav.vue
git commit -m "feat(todo): add route and sidebar menu"

Task 10: 数据库迁移

Files:

  • 创建迁移脚本(手动 SQL 或 Alembic

  • Step 1: 创建数据库表

直接执行 SQL参考 app/models/todo.py 的定义):

CREATE TABLE IF NOT EXISTS daily_todos (
    id VARCHAR(36) PRIMARY KEY,
    user_id VARCHAR(36) NOT NULL,
    title VARCHAR(500) NOT NULL,
    is_completed BOOLEAN NOT NULL DEFAULT FALSE,
    source VARCHAR(20) NOT NULL DEFAULT 'manual',
    source_detail VARCHAR(500),
    source_ref_id VARCHAR(36),
    todo_date VARCHAR(10) NOT NULL,
    completed_at TIMESTAMP NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_date (user_id, todo_date),
    INDEX idx_user_id (user_id)
);

使用 sqlite3 执行(假设数据库文件在 backend 目录):

cd backend
sqlite3 myagents.db < create_daily_todos.sql

或通过 Python 直接创建表:

# 在 backend 目录执行
python -c "
import asyncio
from app.database import engine, BaseModel
from app.models.todo import DailyTodo
from sqlalchemy import text

async def create_table():
    async with engine.begin() as conn:
        await conn.execute(text('''
            CREATE TABLE IF NOT EXISTS daily_todos (
                id VARCHAR(36) PRIMARY KEY,
                user_id VARCHAR(36) NOT NULL,
                title VARCHAR(500) NOT NULL,
                is_completed BOOLEAN NOT NULL DEFAULT 0,
                source VARCHAR(20) NOT NULL DEFAULT 'manual',
                source_detail VARCHAR(500),
                source_ref_id VARCHAR(36),
                todo_date VARCHAR(10) NOT NULL,
                completed_at TIMESTAMP NULL,
                created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
                updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
            )
        '''))
        await conn.execute(text('CREATE INDEX IF NOT EXISTS idx_daily_todos_user_date ON daily_todos(user_id, todo_date)'))
        await conn.execute(text('CREATE INDEX IF NOT EXISTS idx_daily_todos_user_id ON daily_todos(user_id)'))
    print('Table created successfully')

asyncio.run(create_table())
"
  • Step 2: 提交迁移脚本
git add docs/superpowers/plans/2026-03-20-daily-todo-migration.sql
git commit -m "feat(todo): add database migration"

验证清单

完成所有 Task 后,验证以下内容:

  1. 后端 API 可访问GET /api/todos 返回 200
  2. CRUD 正常:新增、修改、删除待办均正常
  3. AI 生成正常POST /api/todos/ai-generate 返回待办列表
  4. 前端页面正常/todo 页面可访问
  5. 侧边栏菜单:待办菜单项显示
  6. 样式正常:页面风格与 AgentView 一致sci-fi 全息终端)
  7. 日期切换正常:昨天/前天数据正确加载
  8. 历史只读:修改历史日期的待办返回 403

实现顺序建议

  1. Task 1 → 2 → 3 → 5后端核心
  2. Task 4AI Service
  3. Task 6定时任务
  4. Task 10数据库
  5. Task 7 → 8 → 9前端