码桶
发现社区成员的开源项目
index.js11.5 KB
/**
* 菜谱查询插件(recipe)
*
* 功能:
* 1) 指令模式:发送「菜谱 <菜名>」查询做法与食材;「菜谱 随机」来一道随机菜;「菜谱 分类」看菜系。
* 2) 智能助手:声明 aiTools=get_recipe,AI 可主动调用(如「今天做什么菜」「红烧肉怎么做」)。
*
* 数据源:
* - 默认 TheMealDB(https://www.themealdb.com):完全免费、无需 Key、含食材/步骤/图片/视频。
* 内容偏英文,菜名请用英文(如 chicken / pasta),或用「菜谱 随机」。
* - 可选「天行数据 caipu」:中文菜谱,需在插件设置里填入 API Key,并在设置中将数据源切换为「天行数据」。
*
* 命令前缀已在 meta.commandPrefix 声明,smart 会自动跳过该前缀(不会把「菜谱 xxx」当成普通对话)。
*/
const path = require('path');
const fs = require('fs');
const axios = require('axios');
const db = require('../../lib/db');
const PLUGIN_ID = 'recipe';
const THEMEALDB_BASE = 'https://www.themealdb.com/api/json/v1/1';
// 读取本插件在某机器人上的配置(与后台「插件设置」打通)
async function getSettings(botId) {
try {
const rows = await db.rows(
'SELECT config_key, config_value FROM plugin_settings WHERE bot_id=? AND plugin_id=?',
[botId, PLUGIN_ID]
);
const cfg = {};
for (const r of rows) cfg[r.config_key] = r.config_value;
return cfg;
} catch (e) {
return {};
}
}
async function dataTmpDir() {
const dir = path.join(__dirname, '..', '..', 'data', 'tmp');
fs.mkdirSync(dir, { recursive: true });
return dir;
}
// ---------------- 数据源:TheMealDB(免费免 Key) ----------------
function normalizeTheMealDB(meal) {
const ingredients = [];
for (let i = 1; i <= 20; i++) {
const ing = meal['strIngredient' + i];
const mea = meal['strMeasure' + i];
if (ing && String(ing).trim()) {
ingredients.push({ name: String(ing).trim(), measure: mea ? String(mea).trim() : '' });
}
}
const instr = meal.strInstructions || '';
const steps = instr
.split(/\r?\n/)
.map((s) => s.trim())
.filter(Boolean);
return {
name: meal.strMeal || '未知菜名',
category: meal.strCategory || '',
area: meal.strArea || '',
image: meal.strMealThumb || '',
video: meal.strYoutube || '',
source: meal.strSource || '',
ingredients,
steps: steps.length ? steps : [instr].filter(Boolean),
};
}
async function searchTheMealDB(query) {
const url = `${THEMEALDB_BASE}/search.php?s=${encodeURIComponent(query)}`;
const resp = await axios.get(url, { timeout: 15000 });
const meals = resp.data && resp.data.meals;
if (!meals || !meals.length) return [];
return meals.map(normalizeTheMealDB).slice(0, 10);
}
async function randomTheMealDB() {
const resp = await axios.get(`${THEMEALDB_BASE}/random.php`, { timeout: 15000 });
const meals = resp.data && resp.data.meals;
if (!meals || !meals.length) return null;
return normalizeTheMealDB(meals[0]);
}
async function categoriesTheMealDB() {
const resp = await axios.get(`${THEMEALDB_BASE}/categories.php`, { timeout: 15000 });
const cats = resp.data && resp.data.categories;
return cats ? cats.map((c) => c.strCategory).filter(Boolean) : [];
}
// ---------------- 数据源:天行数据 caipu(中文,需 Key) ----------------
async function searchTianapi(key, query) {
const url =
`https://apis.tianapi.com/caipu/index?key=${encodeURIComponent(key)}` +
`&word=${encodeURIComponent(query)}&num=3&page=1`;
const resp = await axios.get(url, { timeout: 15000 });
if (!resp.data || resp.data.code !== 200) {
throw new Error('天行数据返回:' + (resp.data && resp.data.msg ? resp.data.msg : '未知错误'));
}
const list = (resp.data.result && resp.data.result.data) || [];
return list.map((d) => {
const steps = String(d.content || '')
.split(/\d+\./)
.map((s) => s.trim())
.filter(Boolean);
return {
name: d.title || '未知菜名',
category: d.tag || '',
area: '',
image: d.pic || '',
video: '',
source: '',
ingredients: [],
ingredientsText: d.materiale || '',
steps: steps.length ? steps : [d.content || ''].filter(Boolean),
};
});
}
// ---------------- 渲染 ----------------
function formatRecipe(r) {
const lines = [];
lines.push('🍳 ' + r.name);
const tags = [r.category, r.area].filter(Boolean).join(' · ');
if (tags) lines.push('【' + tags + '】');
if (r.ingredients && r.ingredients.length) {
lines.push('');
lines.push('🥬 食材:');
lines.push(r.ingredients.map((i) => (i.measure ? i.measure + ' ' : '') + i.name).join('、'));
} else if (r.ingredientsText) {
lines.push('');
lines.push('🥬 食材:');
lines.push(r.ingredientsText);
}
if (r.steps && r.steps.length) {
lines.push('');
lines.push('👩🍳 做法:');
r.steps.forEach((s, i) => lines.push(i + 1 + '. ' + s));
}
if (r.video) lines.push('', '📺 视频:' + r.video);
return lines.join('\n');
}
async function sendRecipe(ctx, r) {
// 先发成品图(若有),失败不影响文字
if (r.image) {
let tmp = null;
try {
tmp = path.join(await dataTmpDir(), `recipe_${Date.now()}.jpg`);
const resp = await axios.get(r.image, { responseType: 'arraybuffer', timeout: 20000 });
fs.writeFileSync(tmp, resp.data);
await ctx.sendMedia(tmp, 'image', (r.name || 'recipe') + '.jpg');
} catch (e) {
// 图片发送失败忽略
} finally {
if (tmp) {
try { fs.unlinkSync(tmp); } catch (_) {}
}
}
}
await ctx.sendText(formatRecipe(r));
}
async function resolveResults(cfg, query) {
if (cfg.provider === 'tianapi') {
const key = cfg.tianapi_key;
if (!key) {
throw new Error(
'使用天行数据需先配置 API Key:在插件设置里填入「天行数据 caipu」的 Key,或将数据源改回默认的 TheMealDB(免 Key)。'
);
}
return await searchTianapi(key, query);
}
return await searchTheMealDB(query);
}
async function searchAndSend(ctx, query) {
const cfg = await getSettings(ctx.bot.id);
const results = await resolveResults(cfg, query);
if (!results.length) {
return ctx.sendText('没找到「' + query + '」相关的菜谱,换个菜名试试?');
}
await sendRecipe(ctx, results[0]);
if (results.length > 1) {
const others = results
.slice(1, 6)
.map((r) => r.name)
.join('、');
await ctx.sendText('(还找到:' + others + ' 等共 ' + results.length + ' 道,可「菜谱 <具体菜名>」查看)');
}
}
const HELP =
'🍳 菜谱查询\n' +
'────────────\n' +
'• 菜谱 <菜名> :查询做法与食材(如「菜谱 pasta」「菜谱 红烧肉」)\n' +
'• 菜谱 随机 :来一道随机菜\n' +
'• 菜谱 分类 :查看菜系/分类\n' +
'• 菜谱 帮 :显示本帮助\n' +
'────────────\n' +
'数据源默认 TheMealDB(免费免 Key,内容偏英文,建议用英文菜名或「菜谱 随机」);\n' +
'如需中文菜谱,可在插件设置里填入「天行数据 caipu」Key 并切换数据源。';
// ---------------- 指令入口 ----------------
async function onMessage(msg, ctx) {
const text = (msg.content || '').trim();
if (!text) return;
const m = text.match(/^(?:菜谱|recipe)\s*(.*)$/i);
if (!m) return;
const arg = (m[1] || '').trim();
try {
if (!arg || ['帮', '帮助', 'help', '?', '菜单'].includes(arg.toLowerCase())) {
await ctx.sendText(HELP);
return true;
}
const cfg = await getSettings(ctx.bot.id);
const useTianapi = cfg.provider === 'tianapi';
if (arg === '随机' || arg === 'random') {
if (useTianapi) {
await ctx.sendText('当前数据源(天行数据)不支持「随机」,请直接发送「菜谱 <菜名>」');
return true;
}
const r = await randomTheMealDB();
if (!r) {
await ctx.sendText('暂时没找到随机菜谱,换一个试试~');
return true;
}
await sendRecipe(ctx, r);
return true;
}
if (arg === '分类' || arg === 'categories') {
if (useTianapi) {
await ctx.sendText('当前数据源(天行数据)不支持「分类」,请直接发送「菜谱 <菜名>」');
return true;
}
const cats = await categoriesTheMealDB();
if (!cats.length) {
await ctx.sendText('获取分类失败,稍后再试~');
return true;
}
await ctx.sendText('🍽 菜系 / 分类:\n' + cats.join('、'));
return true;
}
await searchAndSend(ctx, arg);
return true;
} catch (e) {
console.error('[recipe] 出错:', e.message);
await ctx.sendText('查询菜谱出错了:' + e.message);
return true;
}
}
// ---------------- 智能助手工具 ----------------
const aiTools = [
{
type: 'function',
function: {
name: 'get_recipe',
description:
'根据菜名查询菜谱,返回食材清单与做法步骤。当用户询问「XX 怎么做」「今天做什么菜」「推荐个菜谱」时调用。',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: '要查询的菜名,如「红烧肉」「pasta」' },
},
required: ['query'],
},
},
},
];
async function handleAiTool(name, args, ctx) {
if (name !== 'get_recipe') return null;
const query = args && args.query;
if (!query) return '未提供菜名';
try {
const cfg = await getSettings(ctx.bot.id);
const results = await resolveResults(cfg, query);
if (!results.length) return '未找到「' + query + '」的菜谱';
let out = formatRecipe(results[0]);
if (results.length > 1) {
const others = results
.slice(1, 4)
.map((r) => r.name)
.join('、');
out += '\n(还找到:' + others + ')';
}
return out;
} catch (e) {
return '查询菜谱失败:' + e.message;
}
}
const meta = {
id: PLUGIN_ID,
name: '菜谱查询',
description:
'发送「菜谱 <菜名>」查询做法与食材,「菜谱 随机」来一道随机菜,「菜谱 分类」看菜系。默认数据源 TheMealDB(免费免 Key,偏英文);可在插件设置里切换为天行数据(中文,需 Key)。',
category: '娱乐',
commandPrefix: ['菜谱', 'recipe'],
version: '1.0.0',
configurable: true,
settingsSchema: [
{
key: 'provider',
label: '数据源',
type: 'select',
options: [
{ label: 'TheMealDB(免费免 Key,内容偏英文)', value: 'themealdb' },
{ label: '天行数据(中文,需填 Key)', value: 'tianapi' },
],
help: '默认用 TheMealDB 即可开箱即用;想要中文菜谱请选「天行数据」并填 Key。',
},
{
key: 'tianapi_key',
label: '天行数据 API Key',
type: 'password',
placeholder: '在天行数据「菜谱 caipu」申请的 key',
help: '仅当数据源选「天行数据」时需要。',
},
],
aiTools,
};
module.exports = { meta, onMessage, handleAiTool, aiTools };