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:
2026-03-13 10:58:30 +08:00
commit 4c966a3ad2
884 changed files with 140761 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
/**
* daily-briefing skill
* 生成每日早安簡報
*/
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
function getWeekday(date: Date): string {
const days = ['日', '一', '二', '三', '四', '五', '六'];
return `${days[date.getDay()]}`;
}
function formatDate(date: Date): string {
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, '0');
const d = String(date.getDate()).padStart(2, '0');
return `${y}-${m}-${d}`;
}
async function fetchWeather(city: string, apiKey?: string): Promise<string> {
if (!apiKey) {
// 無 API key使用 web_search 取得天氣資訊
return '(天氣資訊需設定 OpenWeatherMap API Key或由 agent 透過 web_search 查詢)';
}
try {
const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}&units=metric&lang=zh_tw`;
const res = await fetch(url);
const data = await res.json() as any;
if (data.main) {
const temp = `${Math.round(data.main.temp_min)}-${Math.round(data.main.temp_max)}°C`;
const desc = data.weather?.[0]?.description || '';
return `氣溫 ${temp}${desc}`;
}
} catch {
// ignore
}
return '天氣資訊暫時無法取得';
}
function readWorkspaceFile(workspace: string, filename: string): string {
const path = join(workspace, filename);
if (existsSync(path)) {
return readFileSync(path, 'utf-8');
}
return '';
}
function parseTodos(todoContent: string): string[] {
if (!todoContent) return [];
return todoContent
.split('\n')
.filter(line => line.match(/^[-*]\s*\[\s*\]/))
.map(line => line.replace(/^[-*]\s*/, ''))
.slice(0, 10);
}
function parseTodaySchedule(scheduleContent: string, dateStr: string): string[] {
if (!scheduleContent) return [];
const lines = scheduleContent.split('\n');
const results: string[] = [];
let inToday = false;
for (const line of lines) {
if (line.includes(dateStr)) { inToday = true; continue; }
if (inToday && line.match(/^#{1,3}\s/)) break;
if (inToday && line.trim() && line.match(/^\d{2}:\d{2}/)) {
results.push(line.trim());
}
}
return results;
}
export async function handler(ctx: any) {
const workspace = ctx.env?.OPENCLAW_WORKSPACE || process.env.HOME + '/.openclaw/workspace';
const now = new Date();
const dateStr = formatDate(now);
const weekday = getWeekday(now);
// 讀取設定
const userMd = readWorkspaceFile(workspace, 'USER.md');
const cityMatch = userMd.match(/城市[:]\s*(.+)/);
const city = cityMatch?.[1]?.trim() || '台北';
const apiKeyMatch = userMd.match(/天氣 API Key[:]\s*(.+)/);
const weatherApiKey = apiKeyMatch?.[1]?.trim();
// 讀取待辦與行程
const todoContent = readWorkspaceFile(workspace, 'TODO.md');
const scheduleContent = readWorkspaceFile(workspace, 'SCHEDULE.md');
const memoryContent = readWorkspaceFile(workspace, `memory/${dateStr}.md`);
const todos = parseTodos(todoContent);
const schedule = parseTodaySchedule(scheduleContent, dateStr);
const weatherInfo = await fetchWeather(city, weatherApiKey);
// 組裝簡報
const sections: string[] = [];
sections.push(`☀️ **早安!${dateStr} ${weekday}**\n`);
sections.push(`🌤️ **今日天氣(${city}**\n${weatherInfo}`);
if (schedule.length > 0) {
sections.push(`📅 **今日行程**\n${schedule.map(s => `${s}`).join('\n')}`);
} else {
sections.push(`📅 **今日行程**\n• 暫無排程`);
}
if (todos.length > 0) {
sections.push(`✅ **待辦事項(${todos.length} 項)**\n${todos.map(t => `${t}`).join('\n')}`);
} else {
sections.push(`✅ **待辦事項**\n• 今日無待辦,保持輕鬆!`);
}
if (memoryContent) {
sections.push(`📝 **昨日記錄**\n${memoryContent.slice(0, 200)}...`);
}
sections.push(`\n有什麼想先處理的嗎`);
return {
reply: sections.join('\n\n'),
metadata: { date: dateStr, city, todoCount: todos.length, scheduleCount: schedule.length },
};
}