forked from Selig/openclaw-skill
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:
192
skills/gooddays-calendar/handler.ts
Normal file
192
skills/gooddays-calendar/handler.ts
Normal 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) },
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user