forked from Selig/openclaw-skill
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.
124 lines
4.1 KiB
TypeScript
124 lines
4.1 KiB
TypeScript
/**
|
||
* 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 },
|
||
};
|
||
}
|