码桶

发现社区成员的开源项目

奶狗 /

ng-webot

公开
main
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;
  },
};