import fs from 'fs/promises' import path from 'path' import { resolveNotebookPath } from '../../utils/pathSafety.js' import { NotFoundError } from '../../../shared/errors/index.js' import { parseTodoContent, generateTodoContent } from './parser.js' import type { DayTodo, TodoFilePath, ParsedTodoFile, GetTodoResult } from './types.js' export interface TodoServiceDependencies { } export class TodoService { constructor(private deps: TodoServiceDependencies = {}) {} getTodoFilePath(year: number, month: number): TodoFilePath { const yearStr = year.toString() const monthStr = month.toString().padStart(2, '0') const relPath = `TODO/${yearStr}/${yearStr}${monthStr}TODO.md` const { fullPath } = resolveNotebookPath(relPath) return { relPath, fullPath } } async ensureTodoFileExists(fullPath: string): Promise { const dir = path.dirname(fullPath) await fs.mkdir(dir, { recursive: true }) try { await fs.access(fullPath) } catch { await fs.writeFile(fullPath, '', 'utf-8') } } async loadAndParseTodoFile(year: number, month: number): Promise { const { fullPath } = this.getTodoFilePath(year, month) try { await fs.access(fullPath) } catch { throw new NotFoundError('TODO file not found') } const content = await fs.readFile(fullPath, 'utf-8') return { fullPath, dayTodos: parseTodoContent(content) } } async saveTodoFile(fullPath: string, dayTodos: DayTodo[]): Promise { const content = generateTodoContent(dayTodos) await fs.writeFile(fullPath, content, 'utf-8') } async getTodo(year: number, month: number): Promise { const { fullPath } = this.getTodoFilePath(year, month) let dayTodos: DayTodo[] = [] try { await fs.access(fullPath) const content = await fs.readFile(fullPath, 'utf-8') dayTodos = parseTodoContent(content) } catch { // 文件不存在 } const now = new Date() const todayStr = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')}` const yesterday = new Date(now) yesterday.setDate(yesterday.getDate() - 1) const yesterdayStr = `${yesterday.getFullYear()}-${(yesterday.getMonth() + 1).toString().padStart(2, '0')}-${yesterday.getDate().toString().padStart(2, '0')}` if (year === now.getFullYear() && month === now.getMonth() + 1) { const migrated = this.migrateIncompleteItems(dayTodos, todayStr, yesterdayStr) if (migrated) { const newContent = generateTodoContent(dayTodos) await this.ensureTodoFileExists(fullPath) await fs.writeFile(fullPath, newContent, 'utf-8') } } return { dayTodos, year, month } } private migrateIncompleteItems(dayTodos: DayTodo[], todayStr: string, yesterdayStr: string): boolean { let migrated = false const yesterdayTodo = dayTodos.find(d => d.date === yesterdayStr) if (yesterdayTodo) { const incompleteItems = yesterdayTodo.items.filter(item => !item.completed) if (incompleteItems.length > 0) { const todayTodo = dayTodos.find(d => d.date === todayStr) if (todayTodo) { const existingIds = new Set(todayTodo.items.map(i => i.id)) const itemsToAdd = incompleteItems.map((item, idx) => ({ ...item, id: existingIds.has(item.id) ? `${todayStr}-migrated-${idx}` : item.id })) todayTodo.items = [...itemsToAdd, ...todayTodo.items] } else { dayTodos.push({ date: todayStr, items: incompleteItems.map((item, idx) => ({ ...item, id: `${todayStr}-migrated-${idx}` })) }) } yesterdayTodo.items = yesterdayTodo.items.filter(item => item.completed) if (yesterdayTodo.items.length === 0) { const index = dayTodos.findIndex(d => d.date === yesterdayStr) if (index !== -1) { dayTodos.splice(index, 1) } } migrated = true } } return migrated } async saveTodo(year: number, month: number, dayTodos: DayTodo[]): Promise { const { fullPath } = this.getTodoFilePath(year, month) await this.ensureTodoFileExists(fullPath) const content = generateTodoContent(dayTodos) await fs.writeFile(fullPath, content, 'utf-8') } async addTodo(year: number, month: number, date: string, todoContent: string): Promise { const { fullPath } = this.getTodoFilePath(year, month) await this.ensureTodoFileExists(fullPath) let fileContent = await fs.readFile(fullPath, 'utf-8') const dayTodos = parseTodoContent(fileContent) const existingDay = dayTodos.find(d => d.date === date) if (existingDay) { const newId = `${date}-${existingDay.items.length}` existingDay.items.push({ id: newId, content: todoContent, completed: false }) } else { dayTodos.push({ date, items: [{ id: `${date}-0`, content: todoContent, completed: false }] }) } fileContent = generateTodoContent(dayTodos) await fs.writeFile(fullPath, fileContent, 'utf-8') return dayTodos } async toggleTodo(year: number, month: number, date: string, itemIndex: number, completed: boolean): Promise { const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month) const day = dayTodos.find(d => d.date === date) if (!day || itemIndex >= day.items.length) { throw new NotFoundError('TODO item not found') } day.items[itemIndex].completed = completed await this.saveTodoFile(fullPath, dayTodos) return dayTodos } async updateTodo(year: number, month: number, date: string, itemIndex: number, newContent: string): Promise { const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month) const day = dayTodos.find(d => d.date === date) if (!day || itemIndex >= day.items.length) { throw new NotFoundError('TODO item not found') } day.items[itemIndex].content = newContent await this.saveTodoFile(fullPath, dayTodos) return dayTodos } async deleteTodo(year: number, month: number, date: string, itemIndex: number): Promise { const { fullPath, dayTodos } = await this.loadAndParseTodoFile(year, month) const dayIndex = dayTodos.findIndex(d => d.date === date) if (dayIndex === -1) { throw new NotFoundError('Day not found') } const day = dayTodos[dayIndex] if (itemIndex >= day.items.length) { throw new NotFoundError('TODO item not found') } day.items.splice(itemIndex, 1) if (day.items.length === 0) { dayTodos.splice(dayIndex, 1) } await this.saveTodoFile(fullPath, dayTodos) return dayTodos } } export const createTodoService = (deps?: TodoServiceDependencies): TodoService => { return new TodoService(deps) }