Files
openclaw-skill/skills/gooddays-calendar/handler.ts
Selig 8bacc868bd 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>
2026-03-14 20:36:30 +08:00

193 lines
6.8 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.
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) },
};
}
}