cook-mcp-windy
Version:
HowToCook MCP Server - Intelligent Chinese recipe management and meal planning for AI assistants
227 lines • 8.72 kB
JavaScript
import { Recipe, ShoppingList, ShoppingItem, ShoppingSection, ShoppingCategory, ShoppingListRequest, WeeklyMealPlan } from '../types/index.js';
/**
* Generate shopping list from recipes
*/
export function generateShoppingListFromRecipes(recipes, request) {
const consolidatedItems = consolidateIngredients(recipes, request.numberOfPeople, request.consolidateItems);
const sections = groupItemsByCategory(consolidatedItems);
// Filter out excluded categories
const filteredSections = request.excludeCategories
? sections.filter(section => !request.excludeCategories.includes(section.category))
: sections;
const totalItems = filteredSections.reduce((sum, section) => sum + section.items.length, 0);
const totalEstimatedCost = request.includePriceEstimates
? calculateTotalCost(filteredSections)
: undefined;
return {
id: generateShoppingListId(),
name: `购物清单 - ${new Date().toLocaleDateString('zh-CN')}`,
description: `为 ${request.numberOfPeople} 人准备的食材清单`,
sections: filteredSections,
totalItems,
totalEstimatedCost,
createdAt: new Date().toISOString(),
lastModified: new Date().toISOString(),
notes: '请根据实际需要调整购买数量'
};
}
/**
* Generate shopping list from meal plan
*/
export function generateShoppingListFromMealPlan(mealPlan, request) {
const allRecipes = mealPlan.dailyPlans.flatMap(day => day.meals.map(meal => meal.recipe));
return generateShoppingListFromRecipes(allRecipes, {
...request,
mealPlanId: mealPlan.id
});
}
/**
* Consolidate ingredients from multiple recipes
*/
function consolidateIngredients(recipes, numberOfPeople, shouldConsolidate = true) {
const ingredientMap = new Map();
recipes.forEach(recipe => {
const scaleFactor = numberOfPeople / recipe.servings;
recipe.ingredients.forEach(ingredient => {
const scaledAmount = scaleIngredientAmount(ingredient.amount, scaleFactor);
const category = categorizeIngredient(ingredient.name);
const key = shouldConsolidate ? ingredient.name : `${ingredient.name}_${recipe.id}`;
if (ingredientMap.has(key)) {
const existingItem = ingredientMap.get(key);
existingItem.quantity = shouldConsolidate
? combineQuantities(existingItem.quantity, scaledAmount)
: scaledAmount;
existingItem.recipeIds.push(recipe.id);
existingItem.recipeNames.push(recipe.name);
}
else {
const shoppingItem = {
id: generateItemId(),
name: ingredient.name,
category,
quantity: scaledAmount,
estimatedPrice: estimatePrice(ingredient.name, category),
priority: determinePriority(ingredient.name, category),
notes: ingredient.notes,
recipeIds: [recipe.id],
recipeNames: [recipe.name],
isPurchased: false,
alternatives: getAlternatives(ingredient.name)
};
ingredientMap.set(key, shoppingItem);
}
});
});
return Array.from(ingredientMap.values());
}
/**
* Group shopping items by category
*/
function groupItemsByCategory(items) {
const categoryMap = new Map();
items.forEach(item => {
if (!categoryMap.has(item.category)) {
categoryMap.set(item.category, []);
}
categoryMap.get(item.category).push(item);
});
const sections = [];
categoryMap.forEach((items, category) => {
const totalEstimatedCost = items.reduce((sum, item) => sum + (item.estimatedPrice || 0), 0);
sections.push({
category,
items: items.sort((a, b) => a.name.localeCompare(b.name, 'zh-CN')),
totalEstimatedCost
});
});
// Sort sections by category priority
const categoryOrder = [
'蔬菜', '水果', '肉类', '海鲜', '蛋奶', '主食', '调料', '冷冻食品', '罐头', '饮品', '零食', '其他'
];
return sections.sort((a, b) => {
const aIndex = categoryOrder.indexOf(a.category);
const bIndex = categoryOrder.indexOf(b.category);
return aIndex - bIndex;
});
}
/**
* Categorize ingredient by name
*/
function categorizeIngredient(ingredientName) {
const categoryKeywords = {
'蔬菜': ['白菜', '萝卜', '土豆', '番茄', '黄瓜', '茄子', '豆角', '菠菜', '韭菜', '芹菜', '洋葱', '大蒜', '生姜'],
'水果': ['苹果', '香蕉', '橙子', '梨', '葡萄', '草莓', '西瓜', '柠檬'],
'肉类': ['猪肉', '牛肉', '羊肉', '鸡肉', '鸭肉', '排骨', '肉丝', '肉片'],
'海鲜': ['鱼', '虾', '蟹', '贝', '鱿鱼', '带鱼', '黄鱼', '鲈鱼'],
'蛋奶': ['鸡蛋', '鸭蛋', '牛奶', '酸奶', '奶酪', '黄油'],
'主食': ['大米', '面粉', '面条', '馒头', '面包', '土豆'],
'调料': ['盐', '糖', '醋', '酱油', '料酒', '胡椒', '八角', '桂皮', '花椒', '辣椒'],
'冷冻食品': ['冷冻', '速冻'],
'罐头': ['罐装', '罐头'],
'饮品': ['茶', '咖啡', '果汁', '汽水'],
'零食': ['饼干', '薯片', '坚果'],
'其他': []
};
for (const [category, keywords] of Object.entries(categoryKeywords)) {
if (keywords.some(keyword => ingredientName.includes(keyword))) {
return category;
}
}
return '其他';
}
/**
* Scale ingredient amount
*/
function scaleIngredientAmount(amount, scaleFactor) {
const numericMatch = amount.match(/^(\d+(?:\.\d+)?)\s*(.*)$/);
if (numericMatch && numericMatch[1]) {
const [, numStr, unit] = numericMatch;
const originalNum = parseFloat(numStr);
const scaledNum = originalNum * scaleFactor;
const roundedNum = Math.round(scaledNum * 100) / 100;
return `${roundedNum}${unit}`;
}
return amount;
}
/**
* Combine quantities (simplified - assumes same units)
*/
function combineQuantities(qty1, qty2) {
const num1Match = qty1.match(/^(\d+(?:\.\d+)?)\s*(.*)$/);
const num2Match = qty2.match(/^(\d+(?:\.\d+)?)\s*(.*)$/);
if (num1Match && num2Match && num1Match[1] && num2Match[1] && num1Match[2] === num2Match[2]) {
const sum = parseFloat(num1Match[1]) + parseFloat(num2Match[1]);
return `${Math.round(sum * 100) / 100}${num1Match[2]}`;
}
return `${qty1} + ${qty2}`;
}
/**
* Estimate price for ingredient
*/
function estimatePrice(_ingredientName, category) {
// Simplified price estimation - in real implementation, this could use a price database
const priceRanges = {
'蔬菜': [2, 8],
'水果': [5, 15],
'肉类': [15, 40],
'海鲜': [20, 60],
'蛋奶': [3, 12],
'主食': [2, 10],
'调料': [1, 8],
'冷冻食品': [8, 25],
'罐头': [5, 15],
'饮品': [3, 12],
'零食': [5, 20],
'其他': [2, 15]
};
const [min, max] = priceRanges[category];
return Math.round((min + max) / 2);
}
/**
* Determine shopping priority
*/
function determinePriority(ingredientName, category) {
const highPriorityItems = ['盐', '油', '米', '面', '蛋', '奶'];
if (highPriorityItems.some(item => ingredientName.includes(item))) {
return '高';
}
if (category === '零食' || category === '饮品') {
return '低';
}
return '中';
}
/**
* Get alternative ingredients
*/
function getAlternatives(ingredientName) {
const alternatives = {
'猪肉': ['牛肉', '鸡肉'],
'牛肉': ['猪肉', '羊肉'],
'鸡肉': ['鸭肉', '猪肉'],
'白菜': ['菠菜', '小白菜'],
'土豆': ['红薯', '山药'],
'番茄': ['西红柿'],
'生抽': ['老抽', '酱油'],
'料酒': ['黄酒', '白酒']
};
return alternatives[ingredientName] || [];
}
/**
* Calculate total estimated cost
*/
function calculateTotalCost(sections) {
return sections.reduce((total, section) => total + (section.totalEstimatedCost || 0), 0);
}
/**
* Generate shopping list ID
*/
function generateShoppingListId() {
return `shopping_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Generate shopping item ID
*/
function generateItemId() {
return `item_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
//# sourceMappingURL=shoppingListUtils.js.map