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.
126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
/**
|
||
* 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 },
|
||
};
|
||
}
|