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,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 抓取今日事件。

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 },
};
}