forked from Selig/openclaw-skill
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>
193 lines
6.8 KiB
TypeScript
193 lines
6.8 KiB
TypeScript
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) },
|
||
};
|
||
}
|
||
}
|