Files
openclaw-skill/skills/daily-briefing/handler.ts
Selig 4c966a3ad2 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.
2026-03-13 10:58:30 +08:00

126 lines
4.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 },
};
}