import { readFileSync, existsSync } from 'fs'; type EnvMap = Record; 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 { 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) }, }; } }