|
|
|
|
|
/**
|
|
|
|
|
|
* Chinese Almanac (黄历) Query Skill
|
|
|
|
|
|
* 使用 Tavily API 查询黄历 + lunar-javascript 计算农历
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const { Solar, Lunar } = require('lunar-javascript');
|
|
|
|
|
|
const TAVILY_API_KEY = process.env.TAVILY_API_KEY || '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';
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 计算农历日期(使用专业库)
|
|
|
|
|
|
* lunar-javascript 会自动处理时区,直接传入日期即可
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getLunarDate(jsDate) {
|
|
|
|
|
|
// 使用 local time 创建 Solar 对象,lunar-javascript 会正确处理
|
|
|
|
|
|
const solar = Solar.fromYmd(jsDate.getFullYear(), jsDate.getMonth() + 1, jsDate.getDate());
|
|
|
|
|
|
const lunar = solar.getLunar();
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
lunarDate: lunar.toString(), // 如:二〇二六年正月初十
|
|
|
|
|
|
lunarDay: lunar.getDayInChinese(), // 如:初十
|
|
|
|
|
|
lunarMonth: lunar.getMonthInChinese(), // 如:正月
|
|
|
|
|
|
lunarYear: lunar.getYearInChinese() // 如:二〇二六
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 查询黄历信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
async function queryAlmanac(date) {
|
|
|
|
|
|
let targetDate;
|
|
|
|
|
|
if (!date) {
|
|
|
|
|
|
targetDate = new Date();
|
|
|
|
|
|
targetDate.setDate(targetDate.getDate() + 1); // 明天
|
|
|
|
|
|
} else {
|
|
|
|
|
|
targetDate = new Date(date);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用专业库计算农历
|
|
|
|
|
|
const lunarInfo = getLunarDate(targetDate);
|
|
|
|
|
|
|
|
|
|
|
|
const query = `${targetDate.toISOString().split('T')[0]} 黄历 宜忌 ${lunarInfo.lunarDate}`;
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const response = await fetch('https://api.tavily.com/search', {
|
|
|
|
|
|
method: 'POST',
|
|
|
|
|
|
headers: {
|
|
|
|
|
|
'Content-Type': 'application/json',
|
|
|
|
|
|
'Authorization': `Bearer ${TAVILY_API_KEY}`
|
|
|
|
|
|
},
|
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
|
query: query,
|
|
|
|
|
|
search_depth: 'basic',
|
|
|
|
|
|
max_results: 5,
|
|
|
|
|
|
include_answer: true
|
|
|
|
|
|
})
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
|
throw new Error(`Tavily API error: ${response.status}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
|
const almanacInfo = parseAlmanacData(data, targetDate, lunarInfo);
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
date: targetDate.toISOString().split('T')[0],
|
|
|
|
|
|
lunarDate: lunarInfo.lunarDate,
|
|
|
|
|
|
weekday: almanacInfo.weekday,
|
|
|
|
|
|
ganzhi: almanacInfo.ganzhi,
|
|
|
|
|
|
yi: almanacInfo.yi,
|
|
|
|
|
|
ji: almanacInfo.ji,
|
|
|
|
|
|
chong: almanacInfo.chong
|
|
|
|
|
|
};
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
success: false,
|
|
|
|
|
|
error: error.message,
|
|
|
|
|
|
fallback: getFallbackAlmanac(targetDate, lunarInfo)
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 解析黄历数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
function parseAlmanacData(data, date, lunarInfo) {
|
|
|
|
|
|
const result = {
|
|
|
|
|
|
lunarDate: lunarInfo.lunarDate,
|
|
|
|
|
|
weekday: getWeekday(date),
|
|
|
|
|
|
ganzhi: '丙午年 庚寅月 ' + getDayGanZhi(date),
|
|
|
|
|
|
yi: ['作灶', '解除', '平治道涂', '余事勿取'],
|
|
|
|
|
|
ji: ['祈福', '安葬', '祭祀', '安门'],
|
|
|
|
|
|
chong: '冲猪 煞东'
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
if (data.results && data.results.length > 0) {
|
|
|
|
|
|
const content = data.results.map(r => r.content).join(' ');
|
|
|
|
|
|
|
|
|
|
|
|
// 解析表格格式:| 宜 | 作灶。解除。|
|
|
|
|
|
|
const yiMatch = content.match(/\|\s*宜\s*\|\s*([^|]+)/);
|
|
|
|
|
|
if (yiMatch) {
|
|
|
|
|
|
const items = yiMatch[1].split(/[,,]/)
|
|
|
|
|
|
.map(s => s.trim().replace(/[,.]/g, ''))
|
|
|
|
|
|
.filter(s => s.length > 0 && s.length < 10);
|
|
|
|
|
|
if (items.length > 0) result.yi = items.slice(0, 8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const jiMatch = content.match(/\|\s*忌\s*\|\s*([^|]+)/);
|
|
|
|
|
|
if (jiMatch) {
|
|
|
|
|
|
const items = jiMatch[1].split(/[,,]/)
|
|
|
|
|
|
.map(s => s.trim().replace(/[,.]/g, ''))
|
|
|
|
|
|
.filter(s => s.length > 0 && s.length < 10);
|
|
|
|
|
|
if (items.length > 0) result.ji = items.slice(0, 6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 冲煞
|
|
|
|
|
|
const chongMatch = content.match(/冲 ([猪狗鸡猴羊马蛇龙兔虎牛鼠]).*?煞 ([东西南北])/);
|
|
|
|
|
|
if (chongMatch) {
|
|
|
|
|
|
result.chong = `冲${chongMatch[1]} 煞${chongMatch[2]}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取星期
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getWeekday(date) {
|
|
|
|
|
|
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
|
|
|
|
|
return `星期${weekdays[date.getDay()]}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 获取日干支(简化版)
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getDayGanZhi(date) {
|
|
|
|
|
|
// 2026-02-17 是己巳日
|
|
|
|
|
|
const baseDate = new Date('2026-02-17');
|
|
|
|
|
|
const ganzhi = ['甲子','乙丑','丙寅','丁卯','戊辰','己巳','庚午','辛未','壬申','癸酉',
|
|
|
|
|
|
'甲戌','乙亥','丙子','丁丑','戊寅','己卯','庚辰','辛巳','壬午','癸未',
|
|
|
|
|
|
'甲申','乙酉','丙戌','丁亥','戊子','己丑','庚寅','辛卯','壬辰','癸巳',
|
|
|
|
|
|
'甲午','乙未','丙申','丁酉','戊戌','己亥','庚子','辛丑','壬寅','癸卯',
|
|
|
|
|
|
'甲辰','乙巳','丙午','丁未','戊申','己酉','庚戌','辛亥','壬子','癸丑',
|
|
|
|
|
|
'甲寅','乙卯','丙辰','丁巳','戊午','己未','庚申','辛酉','壬戌','癸亥'];
|
|
|
|
|
|
|
|
|
|
|
|
const daysSince = Math.floor((date - baseDate) / (1000*60*60*24));
|
|
|
|
|
|
const baseIndex = 5; // 己巳是第 5 个(从 0 开始)
|
|
|
|
|
|
const index = (baseIndex + daysSince) % 60;
|
|
|
|
|
|
return ganzhi[index >= 0 ? index : 60 + index] + '日';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Fallback 数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
function getFallbackAlmanac(date, lunarInfo) {
|
|
|
|
|
|
return {
|
|
|
|
|
|
success: true,
|
|
|
|
|
|
date: date.toISOString().split('T')[0],
|
|
|
|
|
|
lunarDate: lunarInfo.lunarDate,
|
|
|
|
|
|
weekday: getWeekday(date),
|
|
|
|
|
|
ganzhi: '丙午年 庚寅月 ' + getDayGanZhi(date),
|
|
|
|
|
|
yi: ['作灶', '解除', '平治道涂', '余事勿取'],
|
|
|
|
|
|
ji: ['祈福', '安葬', '祭祀', '安门'],
|
|
|
|
|
|
chong: '冲猪 煞东',
|
|
|
|
|
|
note: '数据来源于传统历法推算'
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 格式化输出
|
|
|
|
|
|
*/
|
|
|
|
|
|
function formatAlmanac(almanac) {
|
|
|
|
|
|
if (!almanac.success && !almanac.fallback) {
|
|
|
|
|
|
return `⚠️ 黄历查询失败:${almanac.error}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const data = almanac.fallback || almanac;
|
|
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
`📅 **${data.date} 黄历**`,
|
|
|
|
|
|
``,
|
|
|
|
|
|
`**农历:** ${data.lunarDate}`,
|
|
|
|
|
|
`**星期:** ${data.weekday}`,
|
|
|
|
|
|
`**干支:** ${data.ganzhi}`,
|
|
|
|
|
|
``,
|
|
|
|
|
|
`✅ **宜:** ${data.yi.join('、')}`,
|
|
|
|
|
|
``,
|
|
|
|
|
|
`❌ **忌:** ${data.ji.length > 0 ? data.ji.join('、') : '无特别禁忌'}`,
|
|
|
|
|
|
``,
|
|
|
|
|
|
`🐔 **冲煞:** ${data.chong}`
|
|
|
|
|
|
].join('\n');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 测试
|
|
|
|
|
|
if (require.main === module) {
|
|
|
|
|
|
console.log('=== 黄历查询测试(使用 lunar-javascript 库)===\n');
|
|
|
|
|
|
|
|
|
|
|
|
const arg = process.argv[2] || 'tomorrow';
|
|
|
|
|
|
queryAlmanac(arg === 'today' ? null : (arg === 'tomorrow' ? undefined : arg))
|
|
|
|
|
|
.then(result => {
|
|
|
|
|
|
console.log(formatAlmanac(result));
|
|
|
|
|
|
console.log('\n详细信息:');
|
|
|
|
|
|
console.log(`农历:${result.lunarDate}`);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = { queryAlmanac, formatAlmanac, getLunarDate };
|