server.go 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903
  1. package howtocook
  2. import (
  3. "context"
  4. "errors"
  5. "fmt"
  6. "math"
  7. "math/rand"
  8. "slices"
  9. "strings"
  10. "github.com/bytedance/sonic"
  11. mcpservers "github.com/labring/aiproxy/mcp-servers"
  12. "github.com/mark3labs/mcp-go/mcp"
  13. "github.com/mark3labs/mcp-go/server"
  14. )
  15. const (
  16. RecipesURL = "https://weilei.site/all_recipes.json"
  17. Version = "0.0.6"
  18. )
  19. // Server represents the HowToCook MCP server
  20. type Server struct {
  21. *server.MCPServer
  22. recipes []Recipe
  23. categories []string
  24. }
  25. // NewServer creates a new HowToCook MCP server
  26. func NewServer(_, _ map[string]string) (mcpservers.Server, error) {
  27. // Create MCP server
  28. mcpServer := server.NewMCPServer("howtocook-mcp", Version)
  29. cookServer := &Server{
  30. MCPServer: mcpServer,
  31. }
  32. // Initialize recipes and categories
  33. if err := cookServer.initialize(context.Background()); err != nil {
  34. return nil, fmt.Errorf("failed to initialize server: %w", err)
  35. }
  36. // Add tools
  37. cookServer.addTools()
  38. return cookServer, nil
  39. }
  40. func ListTools(ctx context.Context) ([]mcp.Tool, error) {
  41. cookServer := &Server{
  42. MCPServer: server.NewMCPServer("howtocook-mcp", Version),
  43. }
  44. cookServer.addTools()
  45. return mcpservers.ListServerTools(ctx, cookServer)
  46. }
  47. // initialize loads recipe data and categories
  48. func (s *Server) initialize(ctx context.Context) error {
  49. recipes, err := s.fetchRecipes(ctx)
  50. if err != nil {
  51. return fmt.Errorf("failed to fetch recipes: %w", err)
  52. }
  53. if len(recipes) == 0 {
  54. return errors.New("no recipes found")
  55. }
  56. s.recipes = recipes
  57. s.categories = s.getAllCategories()
  58. return nil
  59. }
  60. // addTools adds all tools to the server
  61. func (s *Server) addTools() {
  62. s.addGetAllRecipesTool()
  63. s.addGetRecipesByCategoryTool()
  64. s.addRecommendMealsTool()
  65. s.addWhatToEatTool()
  66. }
  67. // addGetAllRecipesTool adds the get all recipes tool
  68. func (s *Server) addGetAllRecipesTool() {
  69. s.AddTool(
  70. mcp.Tool{
  71. Name: "mcp_howtocook_getAllRecipes",
  72. Description: "获取所有菜谱",
  73. InputSchema: mcp.ToolInputSchema{
  74. Type: "object",
  75. Properties: map[string]any{},
  76. Required: []string{},
  77. },
  78. },
  79. func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
  80. simplifiedRecipes := make([]NameOnlyRecipe, 0, len(s.recipes))
  81. for _, recipe := range s.recipes {
  82. simplifiedRecipes = append(simplifiedRecipes, NameOnlyRecipe{
  83. Name: recipe.Name,
  84. Description: recipe.Description,
  85. })
  86. }
  87. result, err := sonic.Marshal(simplifiedRecipes)
  88. if err != nil {
  89. return nil, fmt.Errorf("failed to marshal recipes: %w", err)
  90. }
  91. return mcp.NewToolResultText(string(result)), nil
  92. },
  93. )
  94. }
  95. // addGetRecipesByCategoryTool adds the get recipes by category tool
  96. func (s *Server) addGetRecipesByCategoryTool() {
  97. s.AddTool(
  98. mcp.Tool{
  99. Name: "mcp_howtocook_getRecipesByCategory",
  100. Description: "根据分类查询菜谱,可选分类有: " + strings.Join(s.categories, ", "),
  101. InputSchema: mcp.ToolInputSchema{
  102. Type: "object",
  103. Properties: map[string]any{
  104. "category": map[string]any{
  105. "type": "string",
  106. "description": "菜谱分类名称,如水产、早餐、荤菜、主食等",
  107. "enum": s.categories,
  108. },
  109. },
  110. Required: []string{"category"},
  111. },
  112. },
  113. s.handleGetRecipesByCategory,
  114. )
  115. }
  116. // addRecommendMealsTool adds the recommend meals tool
  117. func (s *Server) addRecommendMealsTool() {
  118. s.AddTool(
  119. mcp.Tool{
  120. Name: "mcp_howtocook_recommendMeals",
  121. Description: "根据用户的忌口、过敏原、人数智能推荐菜谱,创建一周的膳食计划以及大致的购物清单",
  122. InputSchema: mcp.ToolInputSchema{
  123. Type: "object",
  124. Properties: map[string]any{
  125. "allergies": map[string]any{
  126. "type": "array",
  127. "items": map[string]any{"type": "string"},
  128. "description": "过敏原列表,如[\"大蒜\", \"虾\"]",
  129. },
  130. "avoidItems": map[string]any{
  131. "type": "array",
  132. "items": map[string]any{"type": "string"},
  133. "description": "忌口食材列表,如[\"葱\", \"姜\"]",
  134. },
  135. "peopleCount": map[string]any{
  136. "type": "integer",
  137. "minimum": 1,
  138. "maximum": 10,
  139. "description": "用餐人数,1-10之间的整数",
  140. },
  141. },
  142. Required: []string{"peopleCount"},
  143. },
  144. },
  145. s.handleRecommendMeals,
  146. )
  147. }
  148. // addWhatToEatTool adds the what to eat tool
  149. func (s *Server) addWhatToEatTool() {
  150. s.AddTool(
  151. mcp.Tool{
  152. Name: "mcp_howtocook_whatToEat",
  153. Description: "不知道吃什么?根据人数直接推荐适合的菜品组合",
  154. InputSchema: mcp.ToolInputSchema{
  155. Type: "object",
  156. Properties: map[string]any{
  157. "peopleCount": map[string]any{
  158. "type": "integer",
  159. "minimum": 1,
  160. "maximum": 10,
  161. "description": "用餐人数,1-10之间的整数,会根据人数推荐合适数量的菜品",
  162. },
  163. },
  164. Required: []string{"peopleCount"},
  165. },
  166. },
  167. s.handleWhatToEat,
  168. )
  169. }
  170. // handleGetRecipesByCategory handles the get recipes by category request
  171. func (s *Server) handleGetRecipesByCategory(
  172. _ context.Context,
  173. request mcp.CallToolRequest,
  174. ) (*mcp.CallToolResult, error) {
  175. args := request.GetArguments()
  176. category, ok := args["category"].(string)
  177. if !ok || category == "" {
  178. return nil, errors.New("category is required")
  179. }
  180. var filteredRecipes []SimpleRecipe
  181. for _, recipe := range s.recipes {
  182. if recipe.Category == category {
  183. filteredRecipes = append(filteredRecipes, s.simplifyRecipe(recipe))
  184. }
  185. }
  186. result, err := sonic.Marshal(filteredRecipes)
  187. if err != nil {
  188. return nil, fmt.Errorf("failed to marshal recipes: %w", err)
  189. }
  190. return mcp.NewToolResultText(string(result)), nil
  191. }
  192. // handleRecommendMeals handles the recommend meals request
  193. func (s *Server) handleRecommendMeals(
  194. _ context.Context,
  195. request mcp.CallToolRequest,
  196. ) (*mcp.CallToolResult, error) {
  197. args := request.GetArguments()
  198. peopleCount, ok := args["peopleCount"].(float64)
  199. if !ok {
  200. return nil, errors.New("peopleCount is required")
  201. }
  202. var allergies, avoidItems []string
  203. if allergiesRaw, ok := args["allergies"].([]any); ok {
  204. for _, allergy := range allergiesRaw {
  205. if allergyStr, ok := allergy.(string); ok {
  206. allergies = append(allergies, allergyStr)
  207. }
  208. }
  209. }
  210. if avoidItemsRaw, ok := args["avoidItems"].([]any); ok {
  211. for _, item := range avoidItemsRaw {
  212. if itemStr, ok := item.(string); ok {
  213. avoidItems = append(avoidItems, itemStr)
  214. }
  215. }
  216. }
  217. mealPlan := s.generateMealPlan(int(peopleCount), allergies, avoidItems)
  218. result, err := sonic.Marshal(mealPlan)
  219. if err != nil {
  220. return nil, fmt.Errorf("failed to marshal meal plan: %w", err)
  221. }
  222. return mcp.NewToolResultText(string(result)), nil
  223. }
  224. // handleWhatToEat handles the what to eat request
  225. func (s *Server) handleWhatToEat(
  226. _ context.Context,
  227. request mcp.CallToolRequest,
  228. ) (*mcp.CallToolResult, error) {
  229. args := request.GetArguments()
  230. peopleCount, ok := args["peopleCount"].(float64)
  231. if !ok {
  232. return nil, errors.New("peopleCount is required")
  233. }
  234. recommendation := s.generateDishRecommendation(int(peopleCount))
  235. result, err := sonic.Marshal(recommendation)
  236. if err != nil {
  237. return nil, fmt.Errorf("failed to marshal recommendation: %w", err)
  238. }
  239. return mcp.NewToolResultText(string(result)), nil
  240. }
  241. // generateMealPlan generates a weekly meal plan
  242. func (s *Server) generateMealPlan(peopleCount int, allergies, avoidItems []string) MealPlan {
  243. // Filter recipes based on allergies and avoid items
  244. filteredRecipes := s.filterRecipes(allergies, avoidItems)
  245. // Group recipes by category
  246. recipesByCategory := s.groupRecipesByCategory(filteredRecipes)
  247. mealPlan := MealPlan{
  248. Weekdays: make([]DayPlan, 5),
  249. Weekend: make([]DayPlan, 2),
  250. GroceryList: GroceryList{
  251. Ingredients: []GroceryItem{},
  252. ShoppingPlan: ShoppingPlan{
  253. Fresh: []string{},
  254. Pantry: []string{},
  255. Spices: []string{},
  256. Others: []string{},
  257. },
  258. },
  259. }
  260. var selectedRecipes []Recipe
  261. // Generate weekday plans
  262. weekdays := []string{"周一", "周二", "周三", "周四", "周五"}
  263. for i := range 5 {
  264. dayPlan := DayPlan{
  265. Day: weekdays[i],
  266. Breakfast: []SimpleRecipe{},
  267. Lunch: []SimpleRecipe{},
  268. Dinner: []SimpleRecipe{},
  269. }
  270. // Breakfast
  271. breakfastCount := int(math.Max(1, math.Ceil(float64(peopleCount)/5)))
  272. dayPlan.Breakfast, selectedRecipes = s.selectMeals(
  273. recipesByCategory,
  274. "早餐",
  275. breakfastCount,
  276. selectedRecipes,
  277. )
  278. // Lunch and dinner
  279. mealCount := int(math.Max(2, math.Ceil(float64(peopleCount)/3)))
  280. dayPlan.Lunch, selectedRecipes = s.selectVariedMeals(
  281. recipesByCategory,
  282. mealCount,
  283. selectedRecipes,
  284. )
  285. dayPlan.Dinner, selectedRecipes = s.selectVariedMeals(
  286. recipesByCategory,
  287. mealCount,
  288. selectedRecipes,
  289. )
  290. mealPlan.Weekdays[i] = dayPlan
  291. }
  292. // Generate weekend plans
  293. weekendDays := []string{"周六", "周日"}
  294. for i := range 2 {
  295. dayPlan := DayPlan{
  296. Day: weekendDays[i],
  297. Breakfast: []SimpleRecipe{},
  298. Lunch: []SimpleRecipe{},
  299. Dinner: []SimpleRecipe{},
  300. }
  301. // Weekend breakfast
  302. breakfastCount := int(math.Max(2, math.Ceil(float64(peopleCount)/3)))
  303. dayPlan.Breakfast, selectedRecipes = s.selectMeals(
  304. recipesByCategory,
  305. "早餐",
  306. breakfastCount,
  307. selectedRecipes,
  308. )
  309. // Weekend meals (more dishes)
  310. weekdayMealCount := int(math.Max(2, math.Ceil(float64(peopleCount)/3)))
  311. var weekendAddition int
  312. if peopleCount <= 4 {
  313. weekendAddition = 1
  314. } else {
  315. weekendAddition = 2
  316. }
  317. mealCount := weekdayMealCount + weekendAddition
  318. dayPlan.Lunch, selectedRecipes = s.selectWeekendMeals(
  319. recipesByCategory,
  320. mealCount,
  321. selectedRecipes,
  322. )
  323. dayPlan.Dinner, selectedRecipes = s.selectWeekendMeals(
  324. recipesByCategory,
  325. mealCount,
  326. selectedRecipes,
  327. )
  328. mealPlan.Weekend[i] = dayPlan
  329. }
  330. // Generate grocery list
  331. mealPlan.GroceryList = s.generateGroceryList(selectedRecipes)
  332. return mealPlan
  333. }
  334. // generateDishRecommendation generates dish recommendations based on people count
  335. func (s *Server) generateDishRecommendation(peopleCount int) DishRecommendation {
  336. vegetableCount := (peopleCount + 1) / 2
  337. meatCount := int(math.Ceil(float64(peopleCount+1) / 2))
  338. var meatDishes []Recipe
  339. for _, recipe := range s.recipes {
  340. if recipe.Category == "荤菜" || recipe.Category == "水产" {
  341. meatDishes = append(meatDishes, recipe)
  342. }
  343. }
  344. var vegetableDishes []Recipe
  345. for _, recipe := range s.recipes {
  346. if recipe.Category != "荤菜" && recipe.Category != "水产" &&
  347. recipe.Category != "早餐" && recipe.Category != "主食" {
  348. vegetableDishes = append(vegetableDishes, recipe)
  349. }
  350. }
  351. var (
  352. recommendedDishes []Recipe
  353. fishDish *Recipe
  354. )
  355. // Add fish dish for large groups
  356. if peopleCount > 8 {
  357. var fishDishes []Recipe
  358. for _, recipe := range s.recipes {
  359. if recipe.Category == "水产" {
  360. fishDishes = append(fishDishes, recipe)
  361. }
  362. }
  363. if len(fishDishes) > 0 {
  364. selected := fishDishes[rand.Intn(len(fishDishes))]
  365. fishDish = &selected
  366. recommendedDishes = append(recommendedDishes, selected)
  367. }
  368. }
  369. // Select meat dishes
  370. remainingMeatCount := meatCount
  371. if fishDish != nil {
  372. remainingMeatCount--
  373. }
  374. selectedMeatDishes := s.selectMeatDishes(meatDishes, remainingMeatCount)
  375. recommendedDishes = append(recommendedDishes, selectedMeatDishes...)
  376. // Select vegetable dishes
  377. selectedVegetableDishes := s.selectRandomDishes(vegetableDishes, vegetableCount)
  378. recommendedDishes = append(recommendedDishes, selectedVegetableDishes...)
  379. // Convert to simple recipes
  380. simpleDishes := make([]SimpleRecipe, len(recommendedDishes))
  381. for _, dish := range recommendedDishes {
  382. simpleDishes = append(simpleDishes, s.simplifyRecipe(dish))
  383. }
  384. fishCount := 0
  385. if fishDish != nil {
  386. fishCount = 1
  387. }
  388. return DishRecommendation{
  389. PeopleCount: peopleCount,
  390. MeatDishCount: len(selectedMeatDishes) + fishCount,
  391. VegetableDishCount: len(selectedVegetableDishes),
  392. Dishes: simpleDishes,
  393. Message: fmt.Sprintf("为%d人推荐的菜品,包含%d个荤菜和%d个素菜。",
  394. peopleCount, len(selectedMeatDishes)+fishCount, len(selectedVegetableDishes)),
  395. }
  396. }
  397. // Helper methods would continue here...
  398. // Due to length constraints, I'll provide the key helper methods:
  399. // simplifyRecipe converts Recipe to SimpleRecipe
  400. func (s *Server) simplifyRecipe(recipe Recipe) SimpleRecipe {
  401. ingredients := make([]SimpleIngredient, len(recipe.Ingredients))
  402. for _, ing := range recipe.Ingredients {
  403. ingredients = append(ingredients, SimpleIngredient{
  404. Name: ing.Name,
  405. TextQuantity: ing.TextQuantity,
  406. })
  407. }
  408. return SimpleRecipe{
  409. ID: recipe.ID,
  410. Name: recipe.Name,
  411. Description: recipe.Description,
  412. Ingredients: ingredients,
  413. }
  414. }
  415. // filterRecipes filters recipes based on allergies and avoid items
  416. func (s *Server) filterRecipes(allergies, avoidItems []string) []Recipe {
  417. var filtered []Recipe
  418. for _, recipe := range s.recipes {
  419. hasAllergyOrAvoid := false
  420. for _, ingredient := range recipe.Ingredients {
  421. name := strings.ToLower(ingredient.Name)
  422. for _, allergy := range allergies {
  423. if strings.Contains(name, strings.ToLower(allergy)) {
  424. hasAllergyOrAvoid = true
  425. break
  426. }
  427. }
  428. if hasAllergyOrAvoid {
  429. break
  430. }
  431. for _, avoid := range avoidItems {
  432. if strings.Contains(name, strings.ToLower(avoid)) {
  433. hasAllergyOrAvoid = true
  434. break
  435. }
  436. }
  437. if hasAllergyOrAvoid {
  438. break
  439. }
  440. }
  441. if !hasAllergyOrAvoid {
  442. filtered = append(filtered, recipe)
  443. }
  444. }
  445. return filtered
  446. }
  447. func (s *Server) groupRecipesByCategory(recipes []Recipe) map[string][]Recipe {
  448. recipesByCategory := make(map[string][]Recipe)
  449. targetCategories := []string{"水产", "早餐", "荤菜", "主食", "素菜", "甜品", "汤羹"}
  450. for _, recipe := range recipes {
  451. for _, category := range targetCategories {
  452. if recipe.Category == category {
  453. if recipesByCategory[category] == nil {
  454. recipesByCategory[category] = []Recipe{}
  455. }
  456. recipesByCategory[category] = append(recipesByCategory[category], recipe)
  457. break
  458. }
  459. }
  460. }
  461. return recipesByCategory
  462. }
  463. // selectMeals selects meals from a specific category
  464. func (s *Server) selectMeals(
  465. recipesByCategory map[string][]Recipe,
  466. category string,
  467. count int,
  468. selectedRecipes []Recipe,
  469. ) ([]SimpleRecipe, []Recipe) {
  470. var meals []SimpleRecipe
  471. if recipes, exists := recipesByCategory[category]; exists && len(recipes) > 0 {
  472. for i := 0; i < count && len(recipes) > 0; i++ {
  473. index := rand.Intn(len(recipes))
  474. selectedRecipe := recipes[index]
  475. selectedRecipes = append(selectedRecipes, selectedRecipe)
  476. meals = append(meals, s.simplifyRecipe(selectedRecipe))
  477. // Remove selected recipe to avoid duplication
  478. recipes = append(recipes[:index], recipes[index+1:]...)
  479. recipesByCategory[category] = recipes
  480. }
  481. }
  482. return meals, selectedRecipes
  483. }
  484. // selectVariedMeals selects meals from various categories for lunch/dinner
  485. func (s *Server) selectVariedMeals(
  486. recipesByCategory map[string][]Recipe,
  487. count int,
  488. selectedRecipes []Recipe,
  489. ) ([]SimpleRecipe, []Recipe) {
  490. var meals []SimpleRecipe
  491. categories := []string{"主食", "水产", "荤菜", "素菜", "甜品"}
  492. for range count {
  493. selectedCategory := categories[rand.Intn(len(categories))]
  494. // Try to find a category with available recipes
  495. attempts := 0
  496. for attempts < len(categories) {
  497. if recipes, exists := recipesByCategory[selectedCategory]; exists && len(recipes) > 0 {
  498. index := rand.Intn(len(recipes))
  499. selectedRecipe := recipes[index]
  500. selectedRecipes = append(selectedRecipes, selectedRecipe)
  501. meals = append(meals, s.simplifyRecipe(selectedRecipe))
  502. // Remove selected recipe
  503. recipes = append(recipes[:index], recipes[index+1:]...)
  504. recipesByCategory[selectedCategory] = recipes
  505. break
  506. }
  507. // Try next category
  508. attempts++
  509. selectedCategory = categories[(rand.Intn(len(categories)))]
  510. }
  511. }
  512. return meals, selectedRecipes
  513. }
  514. // selectWeekendMeals selects meals for weekend with preference for meat and seafood
  515. func (s *Server) selectWeekendMeals(
  516. recipesByCategory map[string][]Recipe,
  517. count int,
  518. selectedRecipes []Recipe,
  519. ) ([]SimpleRecipe, []Recipe) {
  520. var meals []SimpleRecipe
  521. categories := []string{"荤菜", "水产"}
  522. for i := range count {
  523. category := categories[i%len(categories)]
  524. if recipes, exists := recipesByCategory[category]; exists && len(recipes) > 0 {
  525. index := rand.Intn(len(recipes))
  526. selectedRecipe := recipes[index]
  527. selectedRecipes = append(selectedRecipes, selectedRecipe)
  528. meals = append(meals, s.simplifyRecipe(selectedRecipe))
  529. // Remove selected recipe
  530. recipes = append(recipes[:index], recipes[index+1:]...)
  531. recipesByCategory[category] = recipes
  532. } else if recipes, exists := recipesByCategory["主食"]; exists && len(recipes) > 0 {
  533. // Fallback to 主食 if no meat/seafood available
  534. index := rand.Intn(len(recipes))
  535. selectedRecipe := recipes[index]
  536. selectedRecipes = append(selectedRecipes, selectedRecipe)
  537. meals = append(meals, s.simplifyRecipe(selectedRecipe))
  538. // Remove selected recipe
  539. recipes = append(recipes[:index], recipes[index+1:]...)
  540. recipesByCategory["主食"] = recipes
  541. }
  542. }
  543. return meals, selectedRecipes
  544. }
  545. // selectMeatDishes selects meat dishes with preference for different meat types
  546. func (s *Server) selectMeatDishes(meatDishes []Recipe, count int) []Recipe {
  547. //nolint:prealloc
  548. var selectedMeatDishes []Recipe
  549. meatTypes := []string{"猪肉", "鸡肉", "牛肉", "羊肉", "鸭肉", "鱼肉"}
  550. availableDishes := make([]Recipe, len(meatDishes))
  551. copy(availableDishes, meatDishes)
  552. // Try to select different meat types
  553. for _, meatType := range meatTypes {
  554. if len(selectedMeatDishes) >= count {
  555. break
  556. }
  557. var (
  558. meatTypeOptions []Recipe
  559. meatTypeIndices []int
  560. )
  561. for i, dish := range availableDishes {
  562. for _, ingredient := range dish.Ingredients {
  563. if strings.Contains(strings.ToLower(ingredient.Name), strings.ToLower(meatType)) {
  564. meatTypeOptions = append(meatTypeOptions, dish)
  565. meatTypeIndices = append(meatTypeIndices, i)
  566. break
  567. }
  568. }
  569. }
  570. if len(meatTypeOptions) > 0 {
  571. selectedIndex := rand.Intn(len(meatTypeOptions))
  572. selectedMeatDishes = append(selectedMeatDishes, meatTypeOptions[selectedIndex])
  573. // Remove selected dish from available dishes
  574. originalIndex := meatTypeIndices[selectedIndex]
  575. availableDishes = append(
  576. availableDishes[:originalIndex],
  577. availableDishes[originalIndex+1:]...)
  578. // Adjust indices for remaining items
  579. for j := range meatTypeIndices {
  580. if meatTypeIndices[j] > originalIndex {
  581. meatTypeIndices[j]--
  582. }
  583. }
  584. }
  585. }
  586. // Fill remaining slots with random meat dishes
  587. for len(selectedMeatDishes) < count && len(availableDishes) > 0 {
  588. index := rand.Intn(len(availableDishes))
  589. selectedMeatDishes = append(selectedMeatDishes, availableDishes[index])
  590. availableDishes = append(availableDishes[:index], availableDishes[index+1:]...)
  591. }
  592. return selectedMeatDishes
  593. }
  594. // selectRandomDishes selects random dishes from a list
  595. func (s *Server) selectRandomDishes(dishes []Recipe, count int) []Recipe {
  596. //nolint:prealloc
  597. var selectedDishes []Recipe
  598. availableDishes := make([]Recipe, len(dishes))
  599. copy(availableDishes, dishes)
  600. for len(selectedDishes) < count && len(availableDishes) > 0 {
  601. index := rand.Intn(len(availableDishes))
  602. selectedDishes = append(selectedDishes, availableDishes[index])
  603. availableDishes = append(availableDishes[:index], availableDishes[index+1:]...)
  604. }
  605. return selectedDishes
  606. }
  607. // generateGroceryList generates a grocery list from selected recipes
  608. func (s *Server) generateGroceryList(selectedRecipes []Recipe) GroceryList {
  609. ingredientMap := make(map[string]*GroceryItem)
  610. // Process all recipes
  611. for _, recipe := range selectedRecipes {
  612. s.processRecipeIngredients(recipe, ingredientMap)
  613. }
  614. // Convert map to slice
  615. ingredients := make([]GroceryItem, len(ingredientMap))
  616. for _, item := range ingredientMap {
  617. ingredients = append(ingredients, *item)
  618. }
  619. // Sort by usage frequency
  620. for i := range len(ingredients) - 1 {
  621. for j := range len(ingredients) - 1 - i {
  622. if ingredients[j].RecipeCount < ingredients[j+1].RecipeCount {
  623. ingredients[j], ingredients[j+1] = ingredients[j+1], ingredients[j]
  624. }
  625. }
  626. }
  627. // Generate shopping plan
  628. shoppingPlan := ShoppingPlan{
  629. Fresh: []string{},
  630. Pantry: []string{},
  631. Spices: []string{},
  632. Others: []string{},
  633. }
  634. s.categorizeIngredients(ingredients, &shoppingPlan)
  635. return GroceryList{
  636. Ingredients: ingredients,
  637. ShoppingPlan: shoppingPlan,
  638. }
  639. }
  640. // processRecipeIngredients processes ingredients from a recipe
  641. func (s *Server) processRecipeIngredients(recipe Recipe, ingredientMap map[string]*GroceryItem) {
  642. for _, ingredient := range recipe.Ingredients {
  643. key := strings.ToLower(ingredient.Name)
  644. if existingItem, exists := ingredientMap[key]; exists {
  645. // Update existing ingredient
  646. if existingItem.Unit != nil && ingredient.Unit != nil &&
  647. *existingItem.Unit == *ingredient.Unit &&
  648. existingItem.TotalQuantity != nil && ingredient.Quantity != nil {
  649. *existingItem.TotalQuantity += *ingredient.Quantity
  650. } else {
  651. // Set to nil if units don't match or quantities are uncertain
  652. existingItem.TotalQuantity = nil
  653. existingItem.Unit = nil
  654. }
  655. existingItem.RecipeCount++
  656. // Add recipe name if not already present
  657. found := slices.Contains(existingItem.Recipes, recipe.Name)
  658. if !found {
  659. existingItem.Recipes = append(existingItem.Recipes, recipe.Name)
  660. }
  661. } else {
  662. // Create new ingredient entry
  663. newItem := &GroceryItem{
  664. Name: ingredient.Name,
  665. TotalQuantity: ingredient.Quantity,
  666. Unit: ingredient.Unit,
  667. RecipeCount: 1,
  668. Recipes: []string{recipe.Name},
  669. }
  670. ingredientMap[key] = newItem
  671. }
  672. }
  673. }
  674. // categorizeIngredients categorizes ingredients into shopping plan categories
  675. func (s *Server) categorizeIngredients(ingredients []GroceryItem, shoppingPlan *ShoppingPlan) {
  676. spiceKeywords := []string{
  677. "盐",
  678. "糖",
  679. "酱油",
  680. "醋",
  681. "料酒",
  682. "香料",
  683. "胡椒",
  684. "孜然",
  685. "辣椒",
  686. "花椒",
  687. "姜",
  688. "蒜",
  689. "葱",
  690. "调味",
  691. }
  692. freshKeywords := []string{
  693. "肉",
  694. "鱼",
  695. "虾",
  696. "蛋",
  697. "奶",
  698. "菜",
  699. "菠菜",
  700. "白菜",
  701. "青菜",
  702. "豆腐",
  703. "生菜",
  704. "水产",
  705. "豆芽",
  706. "西红柿",
  707. "番茄",
  708. "水果",
  709. "香菇",
  710. "木耳",
  711. "蘑菇",
  712. }
  713. pantryKeywords := []string{
  714. "米",
  715. "面",
  716. "粉",
  717. "油",
  718. "酒",
  719. "醋",
  720. "糖",
  721. "盐",
  722. "酱",
  723. "豆",
  724. "干",
  725. "罐头",
  726. "方便面",
  727. "面条",
  728. "米饭",
  729. "意大利面",
  730. "燕麦",
  731. }
  732. for _, ingredient := range ingredients {
  733. name := strings.ToLower(ingredient.Name)
  734. categorized := false
  735. // Check spices
  736. for _, keyword := range spiceKeywords {
  737. if strings.Contains(name, keyword) {
  738. shoppingPlan.Spices = append(shoppingPlan.Spices, ingredient.Name)
  739. categorized = true
  740. break
  741. }
  742. }
  743. if !categorized {
  744. // Check fresh items
  745. for _, keyword := range freshKeywords {
  746. if strings.Contains(name, keyword) {
  747. shoppingPlan.Fresh = append(shoppingPlan.Fresh, ingredient.Name)
  748. categorized = true
  749. break
  750. }
  751. }
  752. }
  753. if !categorized {
  754. // Check pantry items
  755. for _, keyword := range pantryKeywords {
  756. if strings.Contains(name, keyword) {
  757. shoppingPlan.Pantry = append(shoppingPlan.Pantry, ingredient.Name)
  758. categorized = true
  759. break
  760. }
  761. }
  762. }
  763. if !categorized {
  764. // Default to others
  765. shoppingPlan.Others = append(shoppingPlan.Others, ingredient.Name)
  766. }
  767. }
  768. }