/** * 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 { 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 }, }; }