码桶
发现社区成员的开源项目
index.js14.4 KB
/**
* 插件:星座运势
* --------------------------------------------------
* 1) 手动查询:发送「星座 水瓶座」「水瓶座运势」「狮子座 本周」「aries today」。
* 2) 智能助手:声明 aiTools=get_horoscope,AI 可主动调用。
* 3) 定时订阅:星座定时 水瓶座 每天8点 / 每周一 9点 / 每月1号 9点
* —— 写入 reminders 表,复用定时提醒调度器,到点把运势推送给用户。
*
* API: https://v2.xxapi.cn/api/horoscope?type=aquarius&time=today
* 返回: { code:200, data:{ title, type, time, shortcomment, fortune:{...}, fortunetext:{...}, index:{...}, luckycolor, luckynumber, luckyconstellation, todo:{yi,ji}, name } }
*/
const axios = require('axios');
const API_BASE = 'https://v2.xxapi.cn/api/horoscope';
// 十二星座:中文名 -> 接口英文 type
const ZODIAC = {
'白羊座': 'aries', '金牛座': 'taurus', '双子座': 'gemini', '巨蟹座': 'cancer',
'狮子座': 'leo', '处女座': 'virgo', '天秤座': 'libra', '天蝎座': 'scorpio',
'射手座': 'sagittarius', '摩羯座': 'capricorn', '水瓶座': 'aquarius', '双鱼座': 'pisces',
};
const ZODIAC_EN = ['aries', 'taurus', 'gemini', 'cancer', 'leo', 'virgo', 'libra', 'scorpio', 'sagittarius', 'capricorn', 'aquarius', 'pisces'];
// 时间描述 -> 接口 time
const TIME_MAP = {
'今日': 'today', '今天': 'today', '今日运势': 'today',
'本周': 'week', '这周': 'week', '这星期': 'week',
'本月': 'month', '这个月': 'month',
'本年': 'year', '今年': 'year',
};
const TIME_LABEL = { today: '今日', week: '本周', month: '本月', year: '本年' };
// 五个维度
const DIMENSIONS = [
['all', '综合', '💡'],
['health', '健康', '💊'],
['love', '爱情', '❤️'],
['money', '财富', '💰'],
['work', '事业', '🚀'],
];
/** 是否由本插件处理该消息(普通查询) */
function shouldHandle(text) {
if (!text) return false;
if (text.includes('星座')) return true;
// 含「星座」或能解析出 星座+时间 即处理(支持中文名 / 英文名)
return parseArgs(text) !== null;
}
/** 解析 星座 + 时间 */
function parseArgs(text) {
let sign = null;
let signName = null;
for (const k of Object.keys(ZODIAC)) {
if (text.includes(k)) { sign = ZODIAC[k]; signName = k; break; }
}
if (!sign) {
const m = text.toLowerCase().match(/\b(aries|taurus|gemini|cancer|leo|virgo|libra|scorpio|sagittarius|capricorn|aquarius|pisces)\b/);
if (m) { sign = m[1]; signName = m[1]; }
}
if (!sign) return null;
let time = 'today';
for (const k of Object.keys(TIME_MAP)) {
if (text.includes(k)) { time = TIME_MAP[k]; break; }
}
return { sign, signName, time };
}
/** 把 1~5 的分值渲染成星级 */
function stars(n) {
n = parseInt(n, 10);
if (isNaN(n) || n < 1) n = 1;
if (n > 5) n = 5;
return '★'.repeat(n) + '☆'.repeat(5 - n);
}
function buildReply(d) {
const title = d.title || d.name || '星座';
const type = d.type || (TIME_LABEL[d.time] || '今日') + '运势';
const timeStr = d.time || '';
const lines = [];
lines.push(`【${title}·${type}】${timeStr ? ' ' + timeStr : ''}`);
if (d.shortcomment) lines.push(`短评:${d.shortcomment}`);
lines.push('');
// 指数 + 星级
if (d.fortune) {
for (const [key, label, icon] of DIMENSIONS) {
const val = d.fortune[key];
if (val == null) continue;
const idx = d.index && d.index[key] ? d.index[key] : '';
lines.push(`${icon} ${label}:${stars(val)}${idx ? ' ' + idx : ''}`);
}
lines.push('');
}
// 幸运信息
const luck = [];
if (d.luckycolor) luck.push('幸运色:' + d.luckycolor);
if (d.luckynumber) luck.push('幸运数字:' + d.luckynumber);
if (d.luckyconstellation) luck.push('幸运星座:' + d.luckyconstellation);
if (luck.length) { lines.push(luck.join(' | ')); lines.push(''); }
// 宜忌
if (d.todo) {
if (d.todo.yi) lines.push('📌 宜:' + d.todo.yi);
if (d.todo.ji) lines.push('🚫 忌:' + d.todo.ji);
lines.push('');
}
// 详细运势文字(整体/健康/爱情/财富/事业)
if (d.fortunetext) {
for (const [key, label, icon] of DIMENSIONS) {
const t = d.fortunetext[key];
if (!t) continue;
const short = t.length > 90 ? t.slice(0, 90) + '…' : t;
lines.push(`${icon} ${label}:${short}`);
}
}
return lines.join('\n');
}
async function fetchHoroscope(sign, time) {
try {
const { data: resp } = await axios.get(API_BASE, {
params: { type: sign, time },
timeout: 10000,
headers: { 'User-Agent': 'Mozilla/5.0 (compatible; HoroscopeBot/1.0)' },
});
if (resp && resp.code === 200 && resp.data) {
return { ok: true, data: resp.data };
}
return { ok: false, msg: (resp && resp.msg) || '接口返回异常' };
} catch (err) {
return { ok: false, msg: err.message };
}
}
/** 解析出 星座英文 type(供 AI 工具 / 订阅复用) */
function resolveSign(arg) {
if (!arg) return null;
const s = String(arg);
for (const k of Object.keys(ZODIAC)) {
if (s.includes(k)) return { sign: ZODIAC[k], name: k };
}
const m = s.toLowerCase().match(/\b(aries|taurus|gemini|cancer|leo|virgo|libra|scorpio|sagittarius|capricorn|aquarius|pisces)\b/);
if (m) return { sign: m[1], name: m[1] };
return null;
}
/** 解析出 接口 time(供 AI 工具 / 订阅复用) */
function resolveTime(arg) {
const s = String(arg || 'today').trim().toLowerCase();
if (['today', 'week', 'month', 'year'].includes(s)) return s;
for (const k of Object.keys(TIME_MAP)) {
if (String(arg).includes(k)) return TIME_MAP[k];
}
return 'today';
}
// ==================== 定时订阅(复用 reminders 表) ====================
const REPEAT_LABEL = { daily: '每天', weekly: '每周', monthly: '每月', once: '单次', interval: '间隔', cron: '定时' };
async function handleSchedule(msg, ctx, botId, peerId, sched) {
const db = require('../../lib/db');
const cancelId = sched[2];
const rest = (sched[3] || '').trim();
// ── 取消订阅:星座定时 取消 <id> ──
if (cancelId) {
const id = parseInt(cancelId, 10);
const r = await db.row(
"SELECT id, content FROM reminders WHERE id=? AND peer_id=? AND content LIKE ?",
[id, peerId, '星座 %']
);
if (!r) {
await ctx.sendText('【星座定时】未找到该订阅(或不属于当前会话)。');
return true;
}
await db.exec('DELETE FROM reminders WHERE id=?', [id]);
await ctx.sendText('【星座定时】已取消订阅:' + r.content);
return true;
}
// ── 列表:星座定时 ──
if (!rest) {
const items = await db.rows(
"SELECT id, content, remind_at, repeat_type, repeat_rule FROM reminders WHERE peer_id=? AND content LIKE ? AND fired=0 ORDER BY remind_at ASC",
[peerId, '星座 %']
);
if (!items.length) {
await ctx.sendText(
'【星座定时】你还没有星座订阅。\n设置:星座定时 水瓶座 每天8点 / 每周一 9点 / 每月1号 9点'
);
return true;
}
let reply = '【星座定时】你的订阅:\n';
items.forEach((r) => {
const dt = new Date(r.remind_at * 1000);
const ds = dt.toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' });
const tag = REPEAT_LABEL[r.repeat_type] || '';
reply += `${r.id}. ${tag} ${ds} — ${r.content}\n`;
});
reply += '\n取消:星座定时 取消 <id>';
await ctx.sendText(reply);
return true;
}
// ── 新建:rest = "<星座> <时间描述>" ──
const signInfo = resolveSign(rest);
if (!signInfo) {
await ctx.sendText('【星座定时】未识别星座。示例:星座定时 水瓶座 每天8点');
return true;
}
const timeStr = rest.replace(signInfo.name, '').trim();
if (!timeStr) {
await ctx.sendText('【星座定时】请指定时间。示例:星座定时 水瓶座 每天8点 / 每周一 9点 / 每月1号 9点');
return true;
}
const reminderMod = require('../../plugins/reminder');
const parsed = reminderMod.parseTime(timeStr);
if (!parsed) {
await ctx.sendText('【星座定时】无法识别时间:' + timeStr + '\n支持:每天8点 / 每周一 9点 / 每月1号 9点 / 30分钟后');
return true;
}
if (parsed.target <= Math.floor(Date.now() / 1000)) {
await ctx.sendText('【星座定时】时间已过,请设置未来时间。');
return true;
}
// 根据重复类型决定运势粒度(每日=今日 / 每周=本周 / 每月=本月)
let suffix = '';
let label = REPEAT_LABEL[parsed.repeatType] || '单次';
if (parsed.repeatType === 'monthly') suffix = ' 本月';
else if (parsed.repeatType === 'weekly') suffix = ' 本周';
const content = '星座 ' + signInfo.name + suffix;
await db.exec(
'INSERT INTO reminders (bot_id, peer_id, context_token, content, remind_at, repeat_type, repeat_rule, action, email_to, created_at, fired) VALUES (?,?,?,?,?,?,?,?,?,?,0)',
[botId, peerId, null, content, parsed.target, parsed.repeatType, parsed.repeatRule || '', 'message', '', Math.floor(Date.now() / 1000)]
);
const dt = new Date(parsed.target * 1000);
const ds = dt.toLocaleString('zh-CN', { month: 'numeric', day: 'numeric', hour: '2-digit', minute: '2-digit' });
await ctx.sendText(`【星座定时】已设定:${label} ${ds}\n到点自动发送「${signInfo.name.replace('座', '')}座」${TIME_LABEL[resolveTimeFromRepeat(parsed.repeatType)]}运势`);
return true;
}
// 把 repeat 类型映射回展示用 time 文案
function resolveTimeFromRepeat(rt) {
if (rt === 'weekly') return 'week';
if (rt === 'monthly') return 'month';
return 'today';
}
// 从用户已记住的事实(user_facts)里回忆星座,实现「记住星座后免重复询问」
async function recallSignFromMemory(botId, peerId) {
try {
const db = require('../../lib/db');
const rows = await db.rows(
"SELECT fvalue FROM user_facts WHERE bot_id=? AND peer_id=? AND fkey LIKE ? ORDER BY updated_at DESC LIMIT 1",
[botId, peerId, '%星座%']
);
if (rows.length) {
const info = resolveSign(rows[0].fvalue);
if (info) return info;
}
} catch (e) { /* 忽略,记忆不可用时按未识别处理 */ }
return null;
}
module.exports = {
meta: {
id: 'horoscope',
name: '星座运势',
version: '1.1.0',
author: '奶狗',
category: '信息获取',
description: '查询十二星座今日/本周/本月/本年运势,含综合/健康/爱情/财富/事业指数与幸运信息;支持智能助手调用与定时订阅(每日/每周/每月自动推送)。',
usage: '发送「星座 水瓶座」「水瓶座运势」「狮子座 本周」;定时:星座定时 水瓶座 每天8点 / 每周一 9点 / 每月1号 9点',
entry: 'horoscope/index.js',
configurable: false,
// 智能助手 function calling 工具声明
aiTools: [
{
function: {
name: 'get_horoscope',
description: '查询十二星座的今日/本周/本月/本年运势,包含综合、健康、爱情、财富、事业指数与幸运色、幸运数字、宜忌等。当用户询问星座运势、想了解某个星座(如白羊座、水瓶座、aquarius)的运气、幸运色、爱情或事业运时使用。',
parameters: {
type: 'object',
properties: {
sign: {
type: 'string',
description: '星座名称,支持中文(如 水瓶座)或英文(如 aquarius)',
enum: ['白羊座', '金牛座', '双子座', '巨蟹座', '狮子座', '处女座', '天秤座', '天蝎座', '射手座', '摩羯座', '双鱼座', '水瓶座', 'aries', 'taurus', 'gemini', 'cancer', 'leo', 'virgo', 'libra', 'scorpio', 'sagittarius', 'capricorn', 'aquarius', 'pisces'],
},
time: {
type: 'string',
description: '时间范围:今日/本周/本月/本年',
enum: ['今日', '本周', '本月', '本年', 'today', 'week', 'month', 'year'],
default: 'today',
},
},
required: ['sign'],
},
},
},
],
},
// 智能助手工具执行:返回运势文本(由 AI 转发给用户)
async handleAiTool(name, args, ctx) {
if (name !== 'get_horoscope') return '';
let signInfo = resolveSign(args && args.sign);
// 未显式给星座时,尝试从已记住的用户事实里取(用户此前透露过星座)
if (!signInfo) {
const botId = ctx && ctx.bot && ctx.bot.id;
const peerId = ctx && ctx.msg && ctx.msg.peer_id;
if (botId && peerId) signInfo = await recallSignFromMemory(botId, peerId);
}
if (!signInfo) return '未识别到星座。你可以直接说「白羊座运势」,或先告诉我你的星座(比如「我是白羊座」),我以后就记住啦。';
const time = resolveTime(args && args.time);
const res = await fetchHoroscope(signInfo.sign, time);
if (!res.ok) return '星座运势查询失败:' + res.msg;
return buildReply(Object.assign({ time }, res.data));
},
async onMessage(msg, ctx) {
const text = (msg.content || '').trim();
const botId = (ctx.bot && ctx.bot.id) || msg.bot_id || msg.botId;
const peerId = msg.peer_id;
// 优先匹配「星座定时」订阅管理指令
const sched = text.match(/^星座定时(\s+取消\s+(\d+))?\s*([\s\S]*)$/);
if (sched) {
return await handleSchedule(msg, ctx, botId, peerId, sched);
}
if (!shouldHandle(text)) return false;
const args = parseArgs(text);
if (!args) {
await ctx.sendText(
'【星座运势】未识别到星座。\n支持:白羊/金牛/双子/巨蟹/狮子/处女/天秤/天蝎/射手/摩羯/水瓶/双鱼 座。\n示例:星座 水瓶座 今日'
);
return true;
}
const res = await fetchHoroscope(args.sign, args.time);
if (!res.ok) {
await ctx.sendText('【星座运势】查询失败:' + res.msg + ',请稍后再试。');
return true;
}
const d = Object.assign({ time: args.time }, res.data);
await ctx.sendText(buildReply(d));
return true;
},
};