forked from Selig/openclaw-skill
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:
85
skills/daily-briefing/SKILL.md
Normal file
85
skills/daily-briefing/SKILL.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: daily-briefing
|
||||
description: 每日早安簡報:整合今日行程、天氣、待辦事項,以繁體中文發送 Telegram 摘要。
|
||||
triggers:
|
||||
- "今天行程"
|
||||
- "早安簡報"
|
||||
- "今日摘要"
|
||||
- "daily briefing"
|
||||
tools:
|
||||
- web_fetch
|
||||
- web_search
|
||||
- exec
|
||||
- memory
|
||||
---
|
||||
|
||||
# Daily Briefing Skill
|
||||
|
||||
## 功能說明
|
||||
|
||||
每日自動(或手動觸發)生成早安簡報,包含:
|
||||
1. 🌤️ 今日天氣(台灣地區)
|
||||
2. 📅 今日行程(來自 workspace/SCHEDULE.md 或 Google Calendar)
|
||||
3. ✅ 待辦事項(來自 workspace/TODO.md)
|
||||
4. 💡 今日重點提醒
|
||||
5. 📊 昨日工作回顧(選配)
|
||||
|
||||
## 設定
|
||||
|
||||
在 `workspace/USER.md` 設定:
|
||||
```markdown
|
||||
## 個人設定
|
||||
- 城市:台北
|
||||
- 時區:Asia/Taipei
|
||||
- 簡報語言:繁體中文
|
||||
- 天氣 API Key:<openweathermap-api-key>(選配)
|
||||
```
|
||||
|
||||
## 輸出格式範例
|
||||
|
||||
```
|
||||
☀️ **早安!2026-02-20 週五**
|
||||
|
||||
🌤️ **今日天氣(台北)**
|
||||
氣溫 16-22°C,多雲偶晴,東北風 2-3 級
|
||||
穿著建議:可帶薄外套
|
||||
|
||||
📅 **今日行程**
|
||||
• 09:00 - 週會(視訊)
|
||||
• 14:00 - 客戶簡報
|
||||
• 16:30 - Code Review
|
||||
|
||||
✅ **待辦事項(3 項)**
|
||||
• [ ] 完成 API 文件
|
||||
• [ ] 回覆客戶 email
|
||||
• [ ] 更新 deploy 腳本
|
||||
|
||||
💡 **今日提醒**
|
||||
• SSL 憑證 90 天後到期(2026-05-20)
|
||||
• 本週 sprint 截止日:2026-02-21
|
||||
|
||||
有什麼想先處理的嗎?
|
||||
```
|
||||
|
||||
## Cron 設定
|
||||
|
||||
```bash
|
||||
# 每日 08:00 自動觸發
|
||||
sudo openclaw cron add \
|
||||
--name "daily-briefing" \
|
||||
--cron "0 8 * * *" \
|
||||
--timezone "Asia/Taipei" \
|
||||
--session main \
|
||||
--system-event "請執行 daily-briefing skill,生成今日早安簡報並發送到 Telegram"
|
||||
```
|
||||
|
||||
## 擴充:Google Calendar 整合
|
||||
|
||||
若要連接 Google Calendar,在 workspace/TOOLS.md 記錄:
|
||||
```
|
||||
Google Calendar API:
|
||||
- Service Account: <path-to-credentials.json>
|
||||
- Calendar ID: primary
|
||||
```
|
||||
|
||||
然後 agent 可透過 Google Calendar API 抓取今日事件。
|
||||
125
skills/daily-briefing/handler.ts
Normal file
125
skills/daily-briefing/handler.ts
Normal 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 },
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user