/** * task-capture skill * 快速記錄待辦事項到 TODO.md */ import { readFileSync, writeFileSync, existsSync } from 'fs'; import { join } from 'path'; function getDateStr(): string { const now = new Date(); return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; } function detectPriority(text: string): { emoji: string; label: string } { const lower = text.toLowerCase(); if (lower.includes('緊急') || lower.includes('urgent') || lower.includes('asap') || lower.includes('立刻')) { return { emoji: '🔴', label: '緊急' }; } if (lower.includes('之後') || lower.includes('有空') || lower.includes('低優先') || lower.includes('不急')) { return { emoji: '🟢', label: '低優先' }; } return { emoji: '🟡', label: '一般' }; } function detectDueDate(text: string): string | null { // 明天 if (text.includes('明天')) { const tomorrow = new Date(); tomorrow.setDate(tomorrow.getDate() + 1); return `due:${tomorrow.getFullYear()}-${String(tomorrow.getMonth() + 1).padStart(2, '0')}-${String(tomorrow.getDate()).padStart(2, '0')}`; } // 下週 if (text.includes('下週') || text.includes('下周')) { const next = new Date(); next.setDate(next.getDate() + 7); return `due:${next.getFullYear()}-${String(next.getMonth() + 1).padStart(2, '0')}-${String(next.getDate()).padStart(2, '0')}`; } // 明確日期 YYYY-MM-DD const match = text.match(/(\d{4}-\d{2}-\d{2})/); if (match) return `due:${match[1]}`; return null; } function detectTags(text: string): string[] { const tags: string[] = []; if (text.match(/專案|project|開發|dev|code|PR|review/i)) tags.push('#工作'); if (text.match(/個人|私人|自己|家/)) tags.push('#個人'); if (text.match(/學習|看|讀|研究|study/i)) tags.push('#學習'); // 保留原有 #tag const existing = text.match(/#[\w\u4e00-\u9fa5]+/g) || []; tags.push(...existing); return [...new Set(tags)]; } function cleanTaskText(text: string): string { return text .replace(/^(記住|記一下|待辦|todo|提醒我|別忘了|加到清單)[:::]?\s*/i, '') .replace(/(明天|下週|下周|緊急|urgent|asap)/gi, '') .replace(/#[\w\u4e00-\u9fa5]+/g, '') .replace(/\s+/g, ' ') .trim(); } export async function handler(ctx: any) { const workspace = ctx.env?.OPENCLAW_WORKSPACE || process.env.HOME + '/.openclaw/workspace'; const message = ctx.message?.text || ctx.message?.content || ''; const todoPath = join(workspace, 'TODO.md'); if (!message) { return { reply: '❌ 請告訴我要記錄什麼。' }; } const taskText = cleanTaskText(message); if (!taskText) { return { reply: '❌ 無法識別待辦內容,請再說清楚一點。' }; } const priority = detectPriority(message); const dueDate = detectDueDate(message); const tags = detectTags(message); // 組裝 TODO 項目 const parts = [`- [ ] ${priority.emoji} ${taskText}`]; if (dueDate) parts.push(dueDate); if (tags.length > 0) parts.push(tags.join(' ')); const todoLine = parts.join(' '); // 讀取或建立 TODO.md let content = ''; if (existsSync(todoPath)) { content = readFileSync(todoPath, 'utf-8'); } else { content = `# TODO\n\n## 🔴 緊急\n\n## 🟡 進行中\n\n## 🟢 稍後\n\n## ✅ 已完成\n`; } // 插入到對應優先級區段 const sectionMap: Record = { '緊急': '## 🔴 緊急', '一般': '## 🟡 進行中', '低優先': '## 🟢 稍後', }; const section = sectionMap[priority.label]; if (content.includes(section)) { content = content.replace(section, `${section}\n${todoLine}`); } else { content += `\n${todoLine}\n`; } writeFileSync(todoPath, content, 'utf-8'); const dueDateDisplay = dueDate ? `(截止:${dueDate.replace('due:', '')})` : ''; return { reply: `✅ 已記錄 ${priority.emoji} **${taskText}**${dueDateDisplay} ${tags.length > 0 ? `標籤:${tags.join(' ')}` : ''} 輸入「顯示我的待辦」查看完整清單。`, metadata: { taskText, priority: priority.label, dueDate, tags }, }; }