aiService.ts 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823
  1. import axios from 'axios'
  2. import type { Recipe, CuisineType, NutritionAnalysis, WinePairing } from '@/types'
  3. // AI服务配置 - 从环境变量读取
  4. const AI_CONFIG = {
  5. // baseURL: 'https://api.deepseek.com/v1/',
  6. // apiKey: import.meta.env.VITE_TEXT_GENERATION_API_KEY,
  7. // model: 'deepseek-chat',
  8. // temperature: 0.7,
  9. // timeout: 300000
  10. baseURL: 'https://open.bigmodel.cn/api/paas/v4/',
  11. apiKey: import.meta.env.VITE_IMAGE_GENERATION_API_KEY,
  12. model: 'glm-4-flash-250414',
  13. temperature: 0.7,
  14. timeout: 300000
  15. }
  16. // 创建axios实例
  17. const aiClient = axios.create({
  18. baseURL: AI_CONFIG.baseURL,
  19. timeout: AI_CONFIG.timeout,
  20. headers: {
  21. 'Content-Type': 'application/json',
  22. Authorization: `Bearer ${AI_CONFIG.apiKey}`
  23. }
  24. })
  25. /**
  26. * 调用AI接口生成菜谱
  27. * @param ingredients 食材列表
  28. * @param cuisine 菜系信息
  29. * @param customPrompt 自定义提示词(可选)
  30. * @returns Promise<Recipe>
  31. */
  32. export const generateRecipe = async (ingredients: string[], cuisine: CuisineType, customPrompt?: string): Promise<Recipe> => {
  33. try {
  34. // 构建提示词
  35. let prompt = `${cuisine.prompt}
  36. 用户提供的食材:${ingredients.join('、')}`
  37. // 如果有自定义要求,添加到提示词中
  38. if (customPrompt) {
  39. prompt += `
  40. 用户的特殊要求:${customPrompt}`
  41. }
  42. prompt += `
  43. 请按照以下JSON格式返回菜谱,不包含营养分析和酒水搭配:
  44. {
  45. "name": "菜品名称",
  46. "ingredients": ["食材1", "食材2"],
  47. "steps": [
  48. {
  49. "step": 1,
  50. "description": "步骤描述",
  51. "time": 5,
  52. "temperature": "中火"
  53. }
  54. ],
  55. "cookingTime": 30,
  56. "difficulty": "medium",
  57. "tips": ["技巧1", "技巧2"]
  58. }`
  59. // 调用智谱AI接口
  60. const response = await aiClient.post('/chat/completions', {
  61. model: AI_CONFIG.model,
  62. messages: [
  63. {
  64. role: 'system',
  65. content: '你是一位专业的厨师,请根据用户提供的食材和菜系要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
  66. },
  67. {
  68. role: 'user',
  69. content: prompt
  70. }
  71. ],
  72. temperature: AI_CONFIG.temperature,
  73. stream: false
  74. })
  75. // 解析AI响应
  76. const aiResponse = response.data.choices[0].message.content
  77. // 清理响应内容,提取JSON部分
  78. let cleanResponse = aiResponse.trim()
  79. if (cleanResponse.startsWith('```json')) {
  80. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  81. } else if (cleanResponse.startsWith('```')) {
  82. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  83. }
  84. const recipeData = JSON.parse(cleanResponse)
  85. // 构建完整的Recipe对象
  86. const recipe: Recipe = {
  87. id: `recipe-${cuisine.id}-${Date.now()}`,
  88. name: recipeData.name || `${cuisine.name}推荐菜品`,
  89. cuisine: cuisine.name,
  90. ingredients: recipeData.ingredients || ingredients,
  91. steps: recipeData.steps || [
  92. { step: 1, description: '准备所有食材', time: 5 },
  93. { step: 2, description: '按照传统做法烹饪', time: 20 }
  94. ],
  95. cookingTime: recipeData.cookingTime || 25,
  96. difficulty: recipeData.difficulty || 'medium',
  97. tips: recipeData.tips || ['注意火候控制', '调味要适中'],
  98. nutritionAnalysis: undefined,
  99. winePairing: undefined
  100. }
  101. return recipe
  102. } catch (error) {
  103. console.error(`生成${cuisine.name}菜谱失败:`, error)
  104. // 抛出错误,让上层处理
  105. throw new Error(`AI生成${cuisine.name}菜谱失败,请稍后重试`)
  106. }
  107. }
  108. /**
  109. * 生成一桌菜的菜单
  110. * @param config 一桌菜配置
  111. * @returns Promise<DishInfo[]>
  112. */
  113. export const generateTableMenu = async (config: {
  114. dishCount: number
  115. flexibleCount: boolean
  116. tastes: string[]
  117. cuisineStyle: string
  118. diningScene: string
  119. nutritionFocus: string
  120. customRequirement: string
  121. customDishes: string[]
  122. }): Promise<
  123. Array<{
  124. name: string
  125. description: string
  126. category: string
  127. tags: string[]
  128. }>
  129. > => {
  130. try {
  131. // 构建提示词
  132. const tasteText = config.tastes.length > 0 ? config.tastes.join('、') : '适中'
  133. const sceneMap = {
  134. family: '家庭聚餐',
  135. friends: '朋友聚会',
  136. romantic: '浪漫晚餐',
  137. business: '商务宴请',
  138. festival: '节日庆祝',
  139. casual: '日常用餐'
  140. }
  141. const nutritionMap = {
  142. balanced: '营养均衡',
  143. protein: '高蛋白',
  144. vegetarian: '素食为主',
  145. low_fat: '低脂健康',
  146. comfort: '滋补养生'
  147. }
  148. const styleMap = {
  149. mixed: '混合菜系',
  150. chinese: '中式为主',
  151. western: '西式为主',
  152. japanese: '日式为主'
  153. }
  154. let prompt = `请为我设计一桌菜,要求如下:
  155. - ${config.flexibleCount ? `参考菜品数量:${config.dishCount}道菜(可以根据实际情况智能调整,重点是搭配合理)` : `菜品数量:${config.dishCount}道菜(请严格按照这个数量生成)`}
  156. - 口味偏好:${tasteText}
  157. - 菜系风格:${styleMap[config.cuisineStyle] || '混合菜系'}
  158. - 用餐场景:${sceneMap[config.diningScene] || '家庭聚餐'}
  159. - 营养搭配:${nutritionMap[config.nutritionFocus] || '营养均衡'}`
  160. if (config.customDishes.length > 0) {
  161. prompt += `\n- ${config.flexibleCount ? '优先考虑的菜品' : '必须包含的菜品'}:${config.customDishes.join('、')}${
  162. config.flexibleCount ? '(可以作为参考,根据搭配需要决定是否全部包含)' : '(请确保这些菜品都包含在菜单中)'
  163. }`
  164. }
  165. if (config.customRequirement) {
  166. prompt += `\n- 特殊要求:${config.customRequirement}`
  167. }
  168. if (config.flexibleCount) {
  169. prompt += `
  170. 智能搭配原则:
  171. 1. 菜品数量可以灵活调整(建议在${Math.max(2, config.dishCount - 2)}-${config.dishCount + 2}道之间),重点是搭配合理、营养均衡
  172. 2. 每道菜应该有独特的特色,避免食材和口味重复过多
  173. 3. 如果指定的菜品较少或重复度高,可以适当减少总菜品数量,确保每道菜都有特色
  174. 4. 优先考虑菜品的多样性和营养搭配,而不是强制凑够指定数量
  175. 5. 合理搭配不同类型的菜品:主菜、素菜、汤品、凉菜、主食等
  176. 6. 避免出现重复搭配,每道菜都应该有独特价值`
  177. } else {
  178. prompt += `
  179. 固定数量原则:
  180. 1. 严格按照${config.dishCount}道菜的数量生成菜单
  181. 2. 确保菜品搭配合理,营养均衡
  182. 3. 每道菜都有独特特色,尽量避免食材重复
  183. 4. 合理分配不同类型的菜品:主菜、素菜、汤品、凉菜、主食等`
  184. }
  185. prompt += `
  186. 请按照以下JSON格式返回菜单:
  187. {
  188. "dishes": [
  189. {
  190. "name": "菜品名称",
  191. "description": "菜品简介和特色描述",
  192. "category": "主菜/素菜/汤品/凉菜/主食/甜品",
  193. "tags": ["标签1", "标签2", "标签3"]
  194. }
  195. ]
  196. }`
  197. const response = await aiClient.post('/chat/completions', {
  198. model: AI_CONFIG.model,
  199. messages: [
  200. {
  201. role: 'system',
  202. content:
  203. '你是一位专业的菜单设计师,擅长根据不同场景和需求搭配合理的菜品组合。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答,包括菜名也要翻译成中文'
  204. },
  205. {
  206. role: 'user',
  207. content: prompt
  208. }
  209. ],
  210. temperature: 0.8,
  211. stream: false
  212. })
  213. // 解析AI响应
  214. const aiResponse = response.data.choices[0].message.content
  215. let cleanResponse = aiResponse.trim()
  216. if (cleanResponse.startsWith('```json')) {
  217. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  218. } else if (cleanResponse.startsWith('```')) {
  219. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  220. }
  221. const menuData = JSON.parse(cleanResponse)
  222. return menuData.dishes || []
  223. } catch (error) {
  224. console.error('生成一桌菜菜单失败:', error)
  225. throw new Error('AI生成菜单失败,请稍后重试')
  226. }
  227. }
  228. /**
  229. * 为一桌菜中的单个菜品生成详细菜谱
  230. * @param dishName 菜品名称
  231. * @param dishDescription 菜品描述
  232. * @param category 菜品分类
  233. * @returns Promise<Recipe>
  234. */
  235. export const generateDishRecipe = async (dishName: string, dishDescription: string, category: string): Promise<Recipe> => {
  236. try {
  237. const prompt = `请为以下菜品生成详细的菜谱:
  238. 菜品名称:${dishName}
  239. 菜品描述:${dishDescription}
  240. 菜品分类:${category}
  241. 请按照以下JSON格式返回菜谱:
  242. {
  243. "name": "菜品名称",
  244. "ingredients": ["食材1", "食材2"],
  245. "steps": [
  246. {
  247. "step": 1,
  248. "description": "步骤描述",
  249. "time": 5,
  250. "temperature": "中火"
  251. }
  252. ],
  253. "cookingTime": 30,
  254. "difficulty": "easy/medium/hard",
  255. "tips": ["技巧1", "技巧2"]
  256. }`
  257. const response = await aiClient.post('/chat/completions', {
  258. model: AI_CONFIG.model,
  259. messages: [
  260. {
  261. role: 'system',
  262. content: '你是一位专业的厨师,请根据菜品信息生成详细的制作菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
  263. },
  264. {
  265. role: 'user',
  266. content: prompt
  267. }
  268. ],
  269. temperature: 0.7,
  270. stream: false
  271. })
  272. // 解析AI响应
  273. const aiResponse = response.data.choices[0].message.content
  274. let cleanResponse = aiResponse.trim()
  275. if (cleanResponse.startsWith('```json')) {
  276. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  277. } else if (cleanResponse.startsWith('```')) {
  278. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  279. }
  280. const recipeData = JSON.parse(cleanResponse)
  281. const recipe: Recipe = {
  282. id: `dish-recipe-${Date.now()}`,
  283. name: recipeData.name || dishName,
  284. cuisine: category,
  285. ingredients: recipeData.ingredients || ['主要食材', '调料'],
  286. steps: recipeData.steps || [
  287. { step: 1, description: '准备食材', time: 5 },
  288. { step: 2, description: '开始制作', time: 15 }
  289. ],
  290. cookingTime: recipeData.cookingTime || 20,
  291. difficulty: recipeData.difficulty || 'medium',
  292. tips: recipeData.tips || ['注意火候', '调味适中']
  293. }
  294. return recipe
  295. } catch (error) {
  296. console.error('生成菜品菜谱失败:', error)
  297. throw new Error('AI生成菜谱失败,请稍后重试')
  298. }
  299. }
  300. // 使用自定义提示词生成菜谱
  301. export const generateCustomRecipe = async (ingredients: string[], customPrompt: string): Promise<Recipe> => {
  302. try {
  303. const prompt = `你是一位专业的厨师,请根据用户提供的食材和特殊要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。
  304. 用户提供的食材:${ingredients.join('、')}
  305. 用户的特殊要求:${customPrompt}
  306. 请按照以下JSON格式返回菜谱,不包含营养分析和酒水搭配:
  307. {
  308. "name": "菜品名称",
  309. "ingredients": ["食材1", "食材2"],
  310. "steps": [
  311. {
  312. "step": 1,
  313. "description": "步骤描述",
  314. "time": 5,
  315. "temperature": "中火"
  316. }
  317. ],
  318. "cookingTime": 30,
  319. "difficulty": "medium",
  320. "tips": ["技巧1", "技巧2"]
  321. }`
  322. const response = await aiClient.post('/chat/completions', {
  323. model: AI_CONFIG.model,
  324. messages: [
  325. {
  326. role: 'system',
  327. content: '你是一位专业的厨师,请根据用户提供的食材和特殊要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
  328. },
  329. {
  330. role: 'user',
  331. content: prompt
  332. }
  333. ],
  334. temperature: AI_CONFIG.temperature,
  335. max_tokens: 2000,
  336. stream: false
  337. })
  338. const aiResponse = response.data.choices[0].message.content
  339. let cleanResponse = aiResponse.trim()
  340. if (cleanResponse.startsWith('```json')) {
  341. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  342. } else if (cleanResponse.startsWith('```')) {
  343. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  344. }
  345. const recipeData = JSON.parse(cleanResponse)
  346. const recipe: Recipe = {
  347. id: `recipe-custom-${Date.now()}`,
  348. name: recipeData.name || '自定义菜品',
  349. cuisine: '自定义',
  350. ingredients: recipeData.ingredients || ingredients,
  351. steps: recipeData.steps || [
  352. { step: 1, description: '准备所有食材', time: 5 },
  353. { step: 2, description: '按照要求烹饪', time: 20 }
  354. ],
  355. cookingTime: recipeData.cookingTime || 25,
  356. difficulty: recipeData.difficulty || 'medium',
  357. tips: recipeData.tips || ['根据个人口味调整', '注意火候控制'],
  358. nutritionAnalysis: undefined,
  359. winePairing: undefined
  360. }
  361. return recipe
  362. } catch (error) {
  363. console.error('生成自定义菜谱失败:', error)
  364. throw new Error('AI生成自定义菜谱失败,请稍后重试')
  365. }
  366. }
  367. // 流式生成多个菜系的菜谱
  368. export const generateMultipleRecipesStream = async (
  369. ingredients: string[],
  370. cuisines: CuisineType[],
  371. onRecipeGenerated: (recipe: Recipe, index: number, total: number) => void,
  372. customPrompt?: string
  373. ): Promise<void> => {
  374. const total = cuisines.length
  375. let completedCount = 0
  376. // 为了更好的用户体验,我们不并行生成,而是依次生成
  377. // 这样用户可以看到一个个菜谱依次完成的效果
  378. for (let index = 0; index < cuisines.length; index++) {
  379. const cuisine = cuisines[index]
  380. try {
  381. // 添加一些随机延迟,让生成过程更自然
  382. const delay = 1000 + Math.random() * 2000 // 1-3秒的随机延迟
  383. await new Promise(resolve => setTimeout(resolve, delay))
  384. const recipe = await generateRecipe(ingredients, cuisine, customPrompt)
  385. completedCount++
  386. onRecipeGenerated(recipe, index, total)
  387. } catch (error) {
  388. console.error(`生成${cuisine.name}菜谱失败:`, error)
  389. // 即使某个菜系失败,也继续生成其他菜系
  390. continue
  391. }
  392. }
  393. if (completedCount === 0) {
  394. throw new Error('所有菜系生成都失败了,请稍后重试')
  395. }
  396. if (completedCount < total) {
  397. console.warn(`${total - completedCount}个菜系生成失败,但已成功生成${completedCount}个菜谱`)
  398. }
  399. }
  400. /**
  401. * 获取菜谱的营养分析
  402. * @param recipe 菜谱信息
  403. * @returns Promise<NutritionAnalysis>
  404. */
  405. export const getNutritionAnalysis = async (recipe: Recipe): Promise<NutritionAnalysis> => {
  406. try {
  407. const prompt = `请为以下菜谱生成详细的营养分析:
  408. 菜名:${recipe.name}
  409. 食材:${recipe.ingredients.join('、')}
  410. 烹饪方法:${recipe.steps.map(step => step.description).join(',')}
  411. 请按照以下JSON格式返回营养分析:
  412. {
  413. "nutrition": {
  414. "calories": 350,
  415. "protein": 25,
  416. "carbs": 45,
  417. "fat": 12,
  418. "fiber": 8,
  419. "sodium": 800,
  420. "sugar": 6,
  421. "vitaminC": 30,
  422. "calcium": 150,
  423. "iron": 3
  424. },
  425. "healthScore": 8,
  426. "balanceAdvice": ["建议搭配蔬菜沙拉增加维生素", "可适量减少盐分"],
  427. "dietaryTags": ["高蛋白", "低脂"],
  428. "servingSize": "1人份"
  429. }`
  430. const response = await aiClient.post('/chat/completions', {
  431. model: AI_CONFIG.model,
  432. messages: [
  433. {
  434. role: 'system',
  435. content: '你是一位专业的营养师,请根据菜谱信息生成详细的营养分析。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答,包括菜名也要翻译成中文'
  436. },
  437. {
  438. role: 'user',
  439. content: prompt
  440. }
  441. ],
  442. temperature: 0.5, // 使用更低的temperature以获得更准确的分析
  443. stream: false
  444. })
  445. // 解析AI响应
  446. const aiResponse = response.data.choices[0].message.content
  447. let cleanResponse = aiResponse.trim()
  448. if (cleanResponse.startsWith('```json')) {
  449. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  450. } else if (cleanResponse.startsWith('```')) {
  451. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  452. }
  453. const nutritionData = JSON.parse(cleanResponse)
  454. return nutritionData
  455. } catch (error) {
  456. console.error('获取营养分析失败:', error)
  457. return generateFallbackNutrition(recipe.ingredients)
  458. }
  459. }
  460. /**
  461. * 获取菜谱的酒水搭配建议
  462. * @param recipe 菜谱信息
  463. * @returns Promise<WinePairing>
  464. */
  465. export const getWinePairing = async (recipe: Recipe): Promise<WinePairing> => {
  466. try {
  467. const prompt = `请为以下菜谱推荐合适的酒水搭配:
  468. 菜名:${recipe.name}
  469. 菜系:${recipe.cuisine}
  470. 食材:${recipe.ingredients.join('、')}
  471. 请按照以下JSON格式返回酒水搭配建议:
  472. {
  473. "name": "推荐酒水名称",
  474. "type": "red_wine",
  475. "reason": "搭配理由说明",
  476. "servingTemperature": "16-18°C",
  477. "glassType": "波尔多杯",
  478. "alcoholContent": "13.5%",
  479. "flavor": "风味描述",
  480. "origin": "产地"
  481. }`
  482. const response = await aiClient.post('/chat/completions', {
  483. model: AI_CONFIG.model,
  484. messages: [
  485. {
  486. role: 'system',
  487. content: '你是一位专业的侍酒师,请根据菜谱信息推荐合适的酒水搭配。请严格按照JSON格式返回,不要包含任何其他文字。'
  488. },
  489. {
  490. role: 'user',
  491. content: prompt
  492. }
  493. ],
  494. temperature: 0.7,
  495. stream: false
  496. })
  497. // 解析AI响应
  498. const aiResponse = response.data.choices[0].message.content
  499. let cleanResponse = aiResponse.trim()
  500. if (cleanResponse.startsWith('```json')) {
  501. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  502. } else if (cleanResponse.startsWith('```')) {
  503. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  504. }
  505. const wineData = JSON.parse(cleanResponse)
  506. return wineData
  507. } catch (error) {
  508. console.error('获取酒水搭配失败:', error)
  509. return generateFallbackWinePairing({ id: 'custom', name: recipe.cuisine } as CuisineType, recipe.ingredients)
  510. }
  511. }
  512. // 生成后备营养分析数据
  513. const generateFallbackNutrition = (ingredients: string[]): NutritionAnalysis => {
  514. const baseCalories = ingredients.length * 50 + Math.floor(Math.random() * 100) + 200
  515. const hasVegetables = ingredients.some(ing => ['菜', '瓜', '豆', '萝卜', '白菜', '菠菜', '西红柿', '黄瓜', '茄子', '土豆'].some(veg => ing.includes(veg)))
  516. const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
  517. const hasGrains = ingredients.some(ing => ['米', '面', '粉', '饭', '面条', '馒头'].some(grain => ing.includes(grain)))
  518. const dietaryTags: string[] = []
  519. if (hasVegetables && !hasMeat) dietaryTags.push('素食')
  520. if (hasMeat) dietaryTags.push('高蛋白')
  521. if (hasVegetables) dietaryTags.push('富含维生素')
  522. if (!hasGrains) dietaryTags.push('低碳水')
  523. if (baseCalories < 300) dietaryTags.push('低卡路里')
  524. const balanceAdvice: string[] = []
  525. if (!hasVegetables) balanceAdvice.push('建议搭配新鲜蔬菜增加维生素和膳食纤维')
  526. if (!hasMeat && !ingredients.some(ing => ['豆', '蛋', '奶'].some(protein => ing.includes(protein)))) {
  527. balanceAdvice.push('建议增加蛋白质来源,如豆类或蛋类')
  528. }
  529. if (hasGrains && hasMeat) balanceAdvice.push('营养搭配均衡,适合日常食用')
  530. if (ingredients.length > 5) balanceAdvice.push('食材丰富,营养全面')
  531. return {
  532. nutrition: {
  533. calories: baseCalories,
  534. protein: hasMeat ? 20 + Math.floor(Math.random() * 15) : 8 + Math.floor(Math.random() * 8),
  535. carbs: hasGrains ? 35 + Math.floor(Math.random() * 20) : 15 + Math.floor(Math.random() * 10),
  536. fat: hasMeat ? 12 + Math.floor(Math.random() * 8) : 5 + Math.floor(Math.random() * 5),
  537. fiber: hasVegetables ? 6 + Math.floor(Math.random() * 4) : 2 + Math.floor(Math.random() * 2),
  538. sodium: 600 + Math.floor(Math.random() * 400),
  539. sugar: 3 + Math.floor(Math.random() * 5),
  540. vitaminC: hasVegetables ? 20 + Math.floor(Math.random() * 30) : undefined,
  541. calcium: hasMeat || ingredients.some(ing => ['奶', '豆'].some(ca => ing.includes(ca))) ? 100 + Math.floor(Math.random() * 100) : undefined,
  542. iron: hasMeat ? 2 + Math.floor(Math.random() * 3) : undefined
  543. },
  544. healthScore: Math.floor(Math.random() * 3) + (hasVegetables ? 6 : 4) + (hasMeat ? 1 : 0),
  545. balanceAdvice: balanceAdvice.length > 0 ? balanceAdvice : ['营养搭配合理,可以放心享用'],
  546. dietaryTags: dietaryTags.length > 0 ? dietaryTags : ['家常菜'],
  547. servingSize: '1人份'
  548. }
  549. }
  550. // 生成后备酒水搭配数据
  551. const generateFallbackWinePairing = (cuisine: CuisineType, ingredients: string[]): WinePairing => {
  552. const hasSpicy = ingredients.some(ing => ['辣椒', '花椒', '胡椒', '姜', '蒜', '洋葱'].some(spice => ing.includes(spice)))
  553. const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
  554. const hasSeafood = ingredients.some(ing => ['鱼', '虾', '蟹', '贝', '海带', '紫菜'].some(seafood => ing.includes(seafood)))
  555. const isLight = ingredients.some(ing => ['菜', '瓜', '豆腐', '蛋'].some(light => ing.includes(light)))
  556. const cuisineWineMap: Record<string, Partial<WinePairing>> = {
  557. 川菜大师: {
  558. name: '德国雷司令',
  559. type: 'white_wine',
  560. reason: '雷司令的甜度和酸度能很好地平衡川菜的麻辣,清洁口腔',
  561. servingTemperature: '8-10°C',
  562. glassType: '雷司令杯',
  563. alcoholContent: '11-12%',
  564. flavor: '清新果香,微甜带酸',
  565. origin: '德国莱茵河谷'
  566. },
  567. 粤菜大师: {
  568. name: '香槟',
  569. type: 'white_wine',
  570. reason: '香槟的气泡和酸度与粤菜的清淡鲜美完美搭配',
  571. servingTemperature: '6-8°C',
  572. glassType: '香槟杯',
  573. alcoholContent: '12%',
  574. flavor: '清爽气泡,柑橘香气',
  575. origin: '法国香槟区'
  576. },
  577. 日式料理大师: {
  578. name: '清酒',
  579. type: 'sake',
  580. reason: '清酒的清淡甘甜与日式料理的鲜美本味相得益彰',
  581. servingTemperature: '10-15°C',
  582. glassType: '清酒杯',
  583. alcoholContent: '15-16%',
  584. flavor: '清香甘甜,口感顺滑',
  585. origin: '日本'
  586. }
  587. }
  588. let winePairing = cuisineWineMap[cuisine.name] || {}
  589. if (!winePairing.name) {
  590. if (hasSpicy) {
  591. winePairing = {
  592. name: '德国雷司令',
  593. type: 'white_wine',
  594. reason: '甜白酒能很好地平衡辛辣食物,清洁口腔',
  595. servingTemperature: '8-10°C',
  596. glassType: '白酒杯',
  597. alcoholContent: '11-12%',
  598. flavor: '果香浓郁,微甜清新',
  599. origin: '德国'
  600. }
  601. } else if (hasSeafood) {
  602. winePairing = {
  603. name: '长相思白酒',
  604. type: 'white_wine',
  605. reason: '白酒的酸度和矿物质感与海鲜的鲜味完美搭配',
  606. servingTemperature: '8-10°C',
  607. glassType: '白酒杯',
  608. alcoholContent: '12-13%',
  609. flavor: '清新草本,柑橘香气',
  610. origin: '新西兰'
  611. }
  612. } else if (hasMeat && !isLight) {
  613. winePairing = {
  614. name: '赤霞珠红酒',
  615. type: 'red_wine',
  616. reason: '红酒的单宁与肉类的蛋白质结合,提升整体风味',
  617. servingTemperature: '16-18°C',
  618. glassType: '波尔多杯',
  619. alcoholContent: '13-14%',
  620. flavor: '浓郁果香,单宁丰富',
  621. origin: '法国波尔多'
  622. }
  623. } else {
  624. winePairing = {
  625. name: '绿茶',
  626. type: 'tea',
  627. reason: '绿茶的清香淡雅与清淡菜品相得益彰,有助消化',
  628. servingTemperature: '70-80°C',
  629. glassType: '茶杯',
  630. alcoholContent: '0%',
  631. flavor: '清香淡雅,回甘甜美',
  632. origin: '中国'
  633. }
  634. }
  635. }
  636. return {
  637. name: winePairing.name || '绿茶',
  638. type: winePairing.type || 'tea',
  639. reason: winePairing.reason || '经典搭配,有助消化',
  640. servingTemperature: winePairing.servingTemperature || '常温',
  641. glassType: winePairing.glassType,
  642. alcoholContent: winePairing.alcoholContent,
  643. flavor: winePairing.flavor || '清香怡人',
  644. origin: winePairing.origin
  645. }
  646. }
  647. /**
  648. * 测试AI服务连接
  649. * @returns Promise<boolean>
  650. */
  651. export const testAIConnection = async (): Promise<boolean> => {
  652. try {
  653. const response = await aiClient.post('/chat/completions', {
  654. model: AI_CONFIG.model,
  655. messages: [
  656. {
  657. role: 'user',
  658. content: '你好'
  659. }
  660. ],
  661. max_tokens: 10
  662. })
  663. return response.status === 200
  664. } catch (error) {
  665. console.error('AI服务连接测试失败:', error)
  666. return false
  667. }
  668. }
  669. /**
  670. * 根据菜名生成详细菜谱
  671. * @param dishName 菜品名称
  672. * @returns Promise<Recipe>
  673. */
  674. export const generateDishRecipeByName = async (dishName: string): Promise<Recipe> => {
  675. try {
  676. const prompt = `请为"${dishName}"这道菜生成详细的制作教程。
  677. 要求:
  678. 1. 提供完整的食材清单(包括主料和调料)
  679. 2. 详细的制作步骤,每个步骤要包含具体的时间和火候
  680. 3. 实用的烹饪技巧和注意事项
  681. 4. 如果是地方菜,请说明其特色和来源
  682. 请按照以下JSON格式返回菜谱:
  683. {
  684. "name": "菜品名称",
  685. "ingredients": ["主料1 200g", "调料1 适量", "调料2 1勺"],
  686. "steps": [
  687. {
  688. "step": 1,
  689. "description": "详细的步骤描述,包含具体操作方法",
  690. "time": 5,
  691. "temperature": "中火/大火/小火"
  692. }
  693. ],
  694. "cookingTime": 30,
  695. "difficulty": "easy/medium/hard",
  696. "tips": ["实用技巧1", "注意事项2", "口感调节3"]
  697. }`
  698. const response = await aiClient.post('/chat/completions', {
  699. model: AI_CONFIG.model,
  700. messages: [
  701. {
  702. role: 'system',
  703. content: '你是一位经验丰富的中华料理大师,精通各种菜系的制作方法。请根据用户提供的菜名,生成详细、实用的制作教程。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  704. },
  705. {
  706. role: 'user',
  707. content: prompt
  708. }
  709. ],
  710. temperature: 0.7,
  711. stream: false
  712. })
  713. // 解析AI响应
  714. const aiResponse = response.data.choices[0].message.content
  715. let cleanResponse = aiResponse.trim()
  716. if (cleanResponse.startsWith('```json')) {
  717. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  718. } else if (cleanResponse.startsWith('```')) {
  719. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  720. }
  721. const recipeData = JSON.parse(cleanResponse)
  722. // 构建完整的Recipe对象
  723. const recipe: Recipe = {
  724. id: `dish-search-${Date.now()}`,
  725. name: recipeData.name || dishName,
  726. cuisine: '传统菜谱',
  727. ingredients: recipeData.ingredients || ['主要食材', '调料'],
  728. steps: recipeData.steps || [
  729. { step: 1, description: '准备所有食材', time: 5 },
  730. { step: 2, description: '按照传统方法制作', time: 20 }
  731. ],
  732. cookingTime: recipeData.cookingTime || 25,
  733. difficulty: recipeData.difficulty || 'medium',
  734. tips: recipeData.tips || ['注意火候控制', '调味要适中'],
  735. nutritionAnalysis: undefined,
  736. winePairing: undefined
  737. }
  738. return recipe
  739. } catch (error) {
  740. console.error(`生成"${dishName}"菜谱失败:`, error)
  741. throw new Error(`AI生成"${dishName}"菜谱失败,请稍后重试`)
  742. }
  743. }
  744. // 导出配置更新函数,供外部使用
  745. export { AI_CONFIG }