Files
openclaw-skill/skills/task-capture/handler.ts
Selig 4c966a3ad2 Initial commit: OpenClaw Skill Collection
6 custom skills (assign-task, dispatch-webhook, daily-briefing,
task-capture, qmd-brain, tts-voice) with technical documentation.
Compatible with Claude Code, OpenClaw, Codex CLI, and OpenCode.
2026-03-13 10:58:30 +08:00

124 lines
4.1 KiB
TypeScript
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.
/**
* 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<string, string> = {
'緊急': '## 🔴 緊急',
'一般': '## 🟡 進行中',
'低優先': '## 🟢 稍後',
};
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 },
};
}