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.
This commit is contained in:
123
skills/task-capture/handler.ts
Normal file
123
skills/task-capture/handler.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* 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 },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user