Add 5 missing skills to repo for sync coverage

github-repo-search, gooddays-calendar, luxtts,
openclaw-tavily-search, skill-vetter — previously only
in workspace, now tracked in Gitea for full sync.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-14 20:36:22 +08:00
parent 6451d73732
commit 8bacc868bd
12 changed files with 1043 additions and 0 deletions

View File

@@ -0,0 +1,46 @@
---
name: gooddays-calendar
description: 讀寫 GoodDays 行程與今日吉時資訊。支援登入取得 JWT、查詢 `/api/unified-events`,以及呼叫 `/api/mystical/daily` 取得今日吉時/神祕學資料。
---
# gooddays-calendar
此 skill 用於整合 GoodDays API讓 agent 可以直接:
1. 登入 GoodDays 取得 JWT
2. 查詢未來事件(`/api/unified-events`
3. 查詢今日吉時/神祕學資訊(`/api/mystical/daily`
4. 用自然語言判斷是要查「吉時」還是「行程」
## API 重點
- Base URL`GOODDAYS_BASE_URL`
- Login`POST /auth/login`
- Mystical daily`POST /api/mystical/daily`
- Events`/api/unified-events`
## Mystical daily 實測格式
必填欄位:
- `year`
- `month`
- `day`
選填欄位:
- `hour`
- `userId`
範例:
```json
{"year":2026,"month":3,"day":13,"hour":9}
```
## 設定來源
從 workspace `.env` 讀取:
- `GOODDAYS_BASE_URL`
- `GOODDAYS_EMAIL`
- `GOODDAYS_PASSWORD`
- `GOODDAYS_USER_ID`
## 後續可擴充
- 新增事件建立/更新/刪除
- 將今日吉時整理成 daily-briefing 可直接引用的格式
-`life-planner` / `daily-briefing` skill 串接

View File

@@ -0,0 +1,192 @@
import { readFileSync, existsSync } from 'fs';
type EnvMap = Record<string, string>;
function loadDotEnv(path: string): EnvMap {
const out: EnvMap = {};
if (!existsSync(path)) return out;
const text = readFileSync(path, 'utf-8');
for (const line of text.split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const idx = trimmed.indexOf('=');
if (idx === -1) continue;
const key = trimmed.slice(0, idx).trim();
const value = trimmed.slice(idx + 1).trim();
out[key] = value;
}
return out;
}
async function login(baseUrl: string, email: string, password: string): Promise<string> {
const res = await fetch(`${baseUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json() as any;
if (!res.ok || !data?.data?.token) {
throw new Error(data?.error || 'GoodDays login failed');
}
return data.data.token;
}
async function getMysticalDaily(baseUrl: string, token: string, payload: any) {
const res = await fetch(`${baseUrl}/api/mystical/daily`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(payload),
});
const data = await res.json() as any;
if (!res.ok || data?.success === false) {
throw new Error(data?.error || 'GoodDays mystical daily failed');
}
return data;
}
async function getUnifiedEvents(baseUrl: string, token: string, userId: string, startDate: string, endDate: string) {
const url = new URL(`${baseUrl}/api/unified-events`);
url.searchParams.set('userId', userId);
url.searchParams.set('startDate', startDate);
url.searchParams.set('endDate', endDate);
const res = await fetch(url.toString(), {
method: 'GET',
headers: token ? { 'Authorization': `Bearer ${token}` } : {},
});
const data = await res.json() as any;
if (!res.ok || data?.success === false) {
throw new Error(data?.error || 'GoodDays unified-events failed');
}
return data;
}
function parseDateFromMessage(message: string): { year: number; month: number; day: number; hour?: number } {
const now = new Date();
const dateMatch = message.match(/(\d{4})-(\d{1,2})-(\d{1,2})/);
const hourMatch = message.match(/(?:hour|小時|時|點)\s*[:]?\s*(\d{1,2})/i);
if (dateMatch) {
return {
year: Number(dateMatch[1]),
month: Number(dateMatch[2]),
day: Number(dateMatch[3]),
hour: hourMatch ? Number(hourMatch[1]) : undefined,
};
}
return {
year: now.getFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
hour: hourMatch ? Number(hourMatch[1]) : undefined,
};
}
function formatYmd(year: number, month: number, day: number): string {
return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
}
function addDays(year: number, month: number, day: number, offset: number): { year: number; month: number; day: number } {
const d = new Date(year, month - 1, day);
d.setDate(d.getDate() + offset);
return { year: d.getFullYear(), month: d.getMonth() + 1, day: d.getDate() };
}
function detectIntent(message: string): 'events' | 'mystical' {
const m = message.toLowerCase();
if (/(行程|事件|日程|schedule|calendar|待會|今天有什麼安排|未來48小時)/i.test(m)) return 'events';
return 'mystical';
}
function summarizeEvents(events: any[]): string {
if (!Array.isArray(events) || events.length === 0) return '• 目前沒有查到符合條件的事件';
return events.slice(0, 20).map((evt: any, idx: number) => {
const title = evt?.title || evt?.name || evt?.summary || `事件 ${idx + 1}`;
const start = evt?.startDate || evt?.start || evt?.start_time || evt?.date || '未知時間';
const end = evt?.endDate || evt?.end || evt?.end_time || '';
return `${title}${start ? `${start}` : ''}${end ? `${end}` : ''}`;
}).join('\n');
}
export async function handler(ctx: any) {
const workspace = ctx.env?.OPENCLAW_WORKSPACE || `${process.env.HOME}/.openclaw/workspace`;
const env = {
...loadDotEnv(`${workspace}/.env`),
...process.env,
} as EnvMap;
const baseUrl = env.GOODDAYS_BASE_URL;
const email = env.GOODDAYS_EMAIL;
const password = env.GOODDAYS_PASSWORD;
const userId = env.GOODDAYS_USER_ID;
const message = ctx.message?.text || ctx.message?.content || '';
if (!baseUrl || !email || !password) {
return { reply: '缺少 GoodDays 設定,請先檢查 workspace/.env。' };
}
try {
const token = await login(baseUrl, email, password);
const datePayload = parseDateFromMessage(message);
const intent = detectIntent(message);
if (intent === 'events') {
const startDate = formatYmd(datePayload.year, datePayload.month, datePayload.day);
const plusOne = addDays(datePayload.year, datePayload.month, datePayload.day, 1);
const endDate = formatYmd(plusOne.year, plusOne.month, plusOne.day);
const result = await getUnifiedEvents(baseUrl, token, userId, startDate, endDate);
const events = result?.data || [];
return {
reply:
`📅 GoodDays 行程查詢\n\n` +
`區間:${startDate} ~ ${endDate}\n` +
`${summarizeEvents(events)}`,
metadata: {
engine: 'gooddays-calendar',
endpoint: '/api/unified-events',
startDate,
endDate,
count: Array.isArray(events) ? events.length : 0,
result,
},
};
}
const payload = { ...datePayload, userId };
if (payload.hour == null) delete (payload as any).hour;
const result = await getMysticalDaily(baseUrl, token, payload);
const d = result?.data || {};
const goodHours = d?.good_hours?.good_hours_display || '未提供';
const isGoodNow = d?.good_hours?.is_good_hour;
const ganzhi = d?.ganzhi?.day || '未知';
const lunar = d?.lunar?.full_date || '未知';
const dongong = d?.dongong?.note || '未提供';
const twelve = d?.twelve_star?.description || '未提供';
return {
reply:
`📅 GoodDays 今日資訊\n\n` +
`日期:${payload.year}-${String(payload.month).padStart(2, '0')}-${String(payload.day).padStart(2, '0')}` +
`${payload.hour != null ? ` ${payload.hour}:00` : ''}` +
`\n干支${ganzhi}` +
`\n農曆${lunar}` +
`\n吉時${goodHours}` +
`\n此刻是否吉時${isGoodNow === true ? '是' : isGoodNow === false ? '否' : '未知'}` +
`\n董公${dongong}` +
`\n十二建星${twelve}`,
metadata: {
engine: 'gooddays-calendar',
endpoint: '/api/mystical/daily',
payload,
result,
},
};
} catch (error: any) {
return {
reply: `❌ GoodDays 查詢失敗:${error?.message || String(error)}`,
metadata: { error: error?.message || String(error) },
};
}
}