aiService.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539
  1. import axios from 'axios'
  2. import type { Recipe, CuisineType, NutritionAnalysis, WinePairing } from '@/types'
  3. // AI服务配置 - 智谱AI
  4. const AI_CONFIG = {
  5. // baseURL: 'https://open.bigmodel.cn/api/paas/v4/',
  6. // apiKey: 'a835b9f6866d48ec956d341418df8a50.NuhlKYn58EkCb5iP',
  7. // model: 'glm-4-flash-250414',
  8. // temperature: 0.7,
  9. // timeout: 30000
  10. baseURL: 'https://api.deepseek.com/v1/',
  11. apiKey: 'sk-78d4fed678fa4a5ebc5f7beac54b1a78',
  12. model: 'deepseek-chat',
  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. "nutritionAnalysis": {
  59. "nutrition": {
  60. "calories": 350,
  61. "protein": 25,
  62. "carbs": 45,
  63. "fat": 12,
  64. "fiber": 8,
  65. "sodium": 800,
  66. "sugar": 6,
  67. "vitaminC": 30,
  68. "calcium": 150,
  69. "iron": 3
  70. },
  71. "healthScore": 8,
  72. "balanceAdvice": ["建议搭配蔬菜沙拉增加维生素", "可适量减少盐分"],
  73. "dietaryTags": ["高蛋白", "低脂"],
  74. "servingSize": "1人份"
  75. },
  76. "winePairing": {
  77. "name": "推荐酒水名称",
  78. "type": "red_wine",
  79. "reason": "搭配理由说明",
  80. "servingTemperature": "16-18°C",
  81. "glassType": "波尔多杯",
  82. "alcoholContent": "13.5%",
  83. "flavor": "风味描述",
  84. "origin": "产地"
  85. }
  86. }`
  87. // 调用智谱AI接口
  88. const response = await aiClient.post('/chat/completions', {
  89. model: AI_CONFIG.model,
  90. messages: [
  91. {
  92. role: 'system',
  93. content: '你是一位专业的厨师,请根据用户提供的食材和菜系要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
  94. },
  95. {
  96. role: 'user',
  97. content: prompt
  98. }
  99. ],
  100. temperature: AI_CONFIG.temperature,
  101. stream: false
  102. })
  103. // 解析AI响应
  104. const aiResponse = response.data.choices[0].message.content
  105. // 清理响应内容,提取JSON部分
  106. let cleanResponse = aiResponse.trim()
  107. if (cleanResponse.startsWith('```json')) {
  108. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  109. } else if (cleanResponse.startsWith('```')) {
  110. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  111. }
  112. const recipeData = JSON.parse(cleanResponse)
  113. // 构建完整的Recipe对象
  114. const recipe: Recipe = {
  115. id: `recipe-${cuisine.id}-${Date.now()}`,
  116. name: recipeData.name || `${cuisine.name}推荐菜品`,
  117. cuisine: cuisine.name,
  118. ingredients: recipeData.ingredients || ingredients,
  119. steps: recipeData.steps || [
  120. { step: 1, description: '准备所有食材', time: 5 },
  121. { step: 2, description: '按照传统做法烹饪', time: 20 }
  122. ],
  123. cookingTime: recipeData.cookingTime || 25,
  124. difficulty: recipeData.difficulty || 'medium',
  125. tips: recipeData.tips || ['注意火候控制', '调味要适中'],
  126. nutritionAnalysis: recipeData.nutritionAnalysis || generateFallbackNutrition(ingredients),
  127. winePairing: recipeData.winePairing || generateFallbackWinePairing(cuisine, ingredients)
  128. }
  129. return recipe
  130. } catch (error) {
  131. console.error(`生成${cuisine.name}菜谱失败:`, error)
  132. // 抛出错误,让上层处理
  133. throw new Error(`AI生成${cuisine.name}菜谱失败,请稍后重试`)
  134. }
  135. }
  136. /**
  137. * 批量生成多个菜系的菜谱
  138. * @param ingredients 食材列表
  139. * @param cuisines 菜系列表
  140. * @param customPrompt 自定义提示词(可选)
  141. * @returns Promise<Recipe[]>
  142. */
  143. export const generateMultipleRecipes = async (ingredients: string[], cuisines: CuisineType[], customPrompt?: string): Promise<Recipe[]> => {
  144. try {
  145. // 并发调用多个AI接口
  146. const promises = cuisines.map(cuisine => generateRecipe(ingredients, cuisine, customPrompt))
  147. const recipes = await Promise.all(promises)
  148. return recipes
  149. } catch (error) {
  150. console.error('批量生成菜谱失败:', error)
  151. throw new Error('批量生成菜谱失败')
  152. }
  153. }
  154. /**
  155. * 流式生成多个菜系的菜谱,每完成一个就通过回调返回
  156. * @param ingredients 食材列表
  157. * @param cuisines 菜系列表
  158. * @param onRecipeGenerated 每生成一个菜谱时的回调函数
  159. * @param customPrompt 自定义提示词(可选)
  160. * @returns Promise<void>
  161. */
  162. export const generateMultipleRecipesStream = async (
  163. ingredients: string[],
  164. cuisines: CuisineType[],
  165. onRecipeGenerated: (recipe: Recipe, index: number, total: number) => void,
  166. customPrompt?: string
  167. ): Promise<void> => {
  168. const total = cuisines.length
  169. let completedCount = 0
  170. // 创建所有的Promise,但不等待全部完成
  171. const promises = cuisines.map(async (cuisine, index) => {
  172. try {
  173. const recipe = await generateRecipe(ingredients, cuisine, customPrompt)
  174. completedCount++
  175. // 每完成一个就立即回调
  176. onRecipeGenerated(recipe, index, total)
  177. return { success: true, recipe, index }
  178. } catch (error) {
  179. console.error(`生成${cuisine.name}菜谱失败:`, error)
  180. return { success: false, error, index, cuisine: cuisine.name }
  181. }
  182. })
  183. // 等待所有Promise完成(无论成功还是失败)
  184. const results = await Promise.allSettled(promises)
  185. // 检查是否有失败的情况
  186. const failedResults = results.filter(result => result.status === 'rejected' || (result.status === 'fulfilled' && !result.value.success))
  187. // 如果所有菜谱都失败了,抛出错误
  188. if (completedCount === 0 && failedResults.length > 0) {
  189. throw new Error('所有菜系生成都失败了,请稍后重试')
  190. }
  191. // 如果部分失败,在控制台记录但不抛出错误
  192. if (failedResults.length > 0) {
  193. console.warn(`${failedResults.length}个菜系生成失败,但已成功生成${completedCount}个菜谱`)
  194. }
  195. }
  196. /**
  197. * 更新AI配置
  198. * @param config 新的配置
  199. */
  200. export const updateAIConfig = (config: Partial<typeof AI_CONFIG>) => {
  201. Object.assign(AI_CONFIG, config)
  202. // 更新axios实例配置
  203. aiClient.defaults.baseURL = AI_CONFIG.baseURL
  204. aiClient.defaults.headers['Authorization'] = `Bearer ${AI_CONFIG.apiKey}`
  205. }
  206. /**
  207. * 使用自定义提示词生成菜谱
  208. * @param ingredients 食材列表
  209. * @param customPrompt 自定义提示词
  210. * @returns Promise<Recipe>
  211. */
  212. export const generateCustomRecipe = async (ingredients: string[], customPrompt: string): Promise<Recipe> => {
  213. try {
  214. // 构建自定义提示词
  215. const prompt = `你是一位专业的厨师,请根据用户提供的食材和特殊要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。
  216. 用户提供的食材:${ingredients.join('、')}
  217. 用户的特殊要求:${customPrompt}
  218. 请按照以下JSON格式返回菜谱,包含营养分析和酒水搭配:
  219. {
  220. "name": "菜品名称",
  221. "ingredients": ["食材1", "食材2"],
  222. "steps": [
  223. {
  224. "step": 1,
  225. "description": "步骤描述",
  226. "time": 5,
  227. "temperature": "中火"
  228. }
  229. ],
  230. "cookingTime": 30,
  231. "difficulty": "medium",
  232. "tips": ["技巧1", "技巧2"],
  233. "nutritionAnalysis": {
  234. "nutrition": {
  235. "calories": 350,
  236. "protein": 25,
  237. "carbs": 45,
  238. "fat": 12,
  239. "fiber": 8,
  240. "sodium": 800,
  241. "sugar": 6,
  242. "vitaminC": 30,
  243. "calcium": 150,
  244. "iron": 3
  245. },
  246. "healthScore": 8,
  247. "balanceAdvice": ["建议搭配蔬菜沙拉增加维生素", "可适量减少盐分"],
  248. "dietaryTags": ["高蛋白", "低脂"],
  249. "servingSize": "1人份"
  250. },
  251. "winePairing": {
  252. "name": "推荐酒水名称",
  253. "type": "red_wine",
  254. "reason": "搭配理由说明",
  255. "servingTemperature": "16-18°C",
  256. "glassType": "波尔多杯",
  257. "alcoholContent": "13.5%",
  258. "flavor": "风味描述",
  259. "origin": "产地"
  260. }
  261. }`
  262. // 调用智谱AI接口
  263. const response = await aiClient.post('/chat/completions', {
  264. model: AI_CONFIG.model,
  265. messages: [
  266. {
  267. role: 'system',
  268. content: '你是一位专业的厨师,请根据用户提供的食材和特殊要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
  269. },
  270. {
  271. role: 'user',
  272. content: prompt
  273. }
  274. ],
  275. temperature: AI_CONFIG.temperature,
  276. max_tokens: 2000,
  277. stream: false
  278. })
  279. // 解析AI响应
  280. const aiResponse = response.data.choices[0].message.content
  281. // 清理响应内容,提取JSON部分
  282. let cleanResponse = aiResponse.trim()
  283. if (cleanResponse.startsWith('```json')) {
  284. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  285. } else if (cleanResponse.startsWith('```')) {
  286. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  287. }
  288. const recipeData = JSON.parse(cleanResponse)
  289. // 构建完整的Recipe对象
  290. const recipe: Recipe = {
  291. id: `recipe-custom-${Date.now()}`,
  292. name: recipeData.name || '自定义菜品',
  293. cuisine: '自定义',
  294. ingredients: recipeData.ingredients || ingredients,
  295. steps: recipeData.steps || [
  296. { step: 1, description: '准备所有食材', time: 5 },
  297. { step: 2, description: '按照要求烹饪', time: 20 }
  298. ],
  299. cookingTime: recipeData.cookingTime || 25,
  300. difficulty: recipeData.difficulty || 'medium',
  301. tips: recipeData.tips || ['根据个人口味调整', '注意火候控制'],
  302. nutritionAnalysis: recipeData.nutritionAnalysis || generateFallbackNutrition(ingredients),
  303. winePairing: recipeData.winePairing || generateFallbackWinePairing({ id: 'custom', name: '自定义' } as any, ingredients)
  304. }
  305. return recipe
  306. } catch (error) {
  307. console.error('生成自定义菜谱失败:', error)
  308. // 抛出错误,让上层处理
  309. throw new Error('AI生成自定义菜谱失败,请稍后重试')
  310. }
  311. }
  312. /**
  313. * 生成后备营养分析数据
  314. * @param ingredients 食材列表
  315. * @returns NutritionAnalysis
  316. */
  317. const generateFallbackNutrition = (ingredients: string[]): NutritionAnalysis => {
  318. // 基于食材数量和类型估算营养成分
  319. const baseCalories = ingredients.length * 50 + Math.floor(Math.random() * 100) + 200
  320. const hasVegetables = ingredients.some(ing => ['菜', '瓜', '豆', '萝卜', '白菜', '菠菜', '西红柿', '黄瓜', '茄子', '土豆'].some(veg => ing.includes(veg)))
  321. const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
  322. const hasGrains = ingredients.some(ing => ['米', '面', '粉', '饭', '面条', '馒头'].some(grain => ing.includes(grain)))
  323. // 生成饮食标签
  324. const dietaryTags: string[] = []
  325. if (hasVegetables && !hasMeat) dietaryTags.push('素食')
  326. if (hasMeat) dietaryTags.push('高蛋白')
  327. if (hasVegetables) dietaryTags.push('富含维生素')
  328. if (!hasGrains) dietaryTags.push('低碳水')
  329. if (baseCalories < 300) dietaryTags.push('低卡路里')
  330. // 生成营养建议
  331. const balanceAdvice: string[] = []
  332. if (!hasVegetables) balanceAdvice.push('建议搭配新鲜蔬菜增加维生素和膳食纤维')
  333. if (!hasMeat && !ingredients.some(ing => ['豆', '蛋', '奶'].some(protein => ing.includes(protein)))) {
  334. balanceAdvice.push('建议增加蛋白质来源,如豆类或蛋类')
  335. }
  336. if (hasGrains && hasMeat) balanceAdvice.push('营养搭配均衡,适合日常食用')
  337. if (ingredients.length > 5) balanceAdvice.push('食材丰富,营养全面')
  338. return {
  339. nutrition: {
  340. calories: baseCalories,
  341. protein: hasMeat ? 20 + Math.floor(Math.random() * 15) : 8 + Math.floor(Math.random() * 8),
  342. carbs: hasGrains ? 35 + Math.floor(Math.random() * 20) : 15 + Math.floor(Math.random() * 10),
  343. fat: hasMeat ? 12 + Math.floor(Math.random() * 8) : 5 + Math.floor(Math.random() * 5),
  344. fiber: hasVegetables ? 6 + Math.floor(Math.random() * 4) : 2 + Math.floor(Math.random() * 2),
  345. sodium: 600 + Math.floor(Math.random() * 400),
  346. sugar: 3 + Math.floor(Math.random() * 5),
  347. vitaminC: hasVegetables ? 20 + Math.floor(Math.random() * 30) : undefined,
  348. calcium: hasMeat || ingredients.some(ing => ['奶', '豆'].some(ca => ing.includes(ca))) ? 100 + Math.floor(Math.random() * 100) : undefined,
  349. iron: hasMeat ? 2 + Math.floor(Math.random() * 3) : undefined
  350. },
  351. healthScore: Math.floor(Math.random() * 3) + (hasVegetables ? 6 : 4) + (hasMeat ? 1 : 0),
  352. balanceAdvice: balanceAdvice.length > 0 ? balanceAdvice : ['营养搭配合理,可以放心享用'],
  353. dietaryTags: dietaryTags.length > 0 ? dietaryTags : ['家常菜'],
  354. servingSize: '1人份'
  355. }
  356. }
  357. /**
  358. * 生成后备酒水搭配数据
  359. * @param cuisine 菜系信息
  360. * @param ingredients 食材列表
  361. * @returns WinePairing
  362. */
  363. const generateFallbackWinePairing = (cuisine: CuisineType, ingredients: string[]): WinePairing => {
  364. const hasSpicy = ingredients.some(ing => ['辣椒', '花椒', '胡椒', '姜', '蒜', '洋葱'].some(spice => ing.includes(spice)))
  365. const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
  366. const hasSeafood = ingredients.some(ing => ['鱼', '虾', '蟹', '贝', '海带', '紫菜'].some(seafood => ing.includes(seafood)))
  367. const isLight = ingredients.some(ing => ['菜', '瓜', '豆腐', '蛋'].some(light => ing.includes(light)))
  368. // 根据菜系推荐酒水
  369. const cuisineWineMap: Record<string, Partial<WinePairing>> = {
  370. 川菜大师: {
  371. name: '德国雷司令',
  372. type: 'white_wine',
  373. reason: '雷司令的甜度和酸度能很好地平衡川菜的麻辣,清洁口腔',
  374. servingTemperature: '8-10°C',
  375. glassType: '雷司令杯',
  376. alcoholContent: '11-12%',
  377. flavor: '清新果香,微甜带酸',
  378. origin: '德国莱茵河谷'
  379. },
  380. 粤菜大师: {
  381. name: '香槟',
  382. type: 'white_wine',
  383. reason: '香槟的气泡和酸度与粤菜的清淡鲜美完美搭配',
  384. servingTemperature: '6-8°C',
  385. glassType: '香槟杯',
  386. alcoholContent: '12%',
  387. flavor: '清爽气泡,柑橘香气',
  388. origin: '法国香槟区'
  389. },
  390. 日式料理大师: {
  391. name: '清酒',
  392. type: 'sake',
  393. reason: '清酒的清淡甘甜与日式料理的鲜美本味相得益彰',
  394. servingTemperature: '10-15°C',
  395. glassType: '清酒杯',
  396. alcoholContent: '15-16%',
  397. flavor: '清香甘甜,口感顺滑',
  398. origin: '日本'
  399. },
  400. 法式料理大师: {
  401. name: '勃艮第红酒',
  402. type: 'red_wine',
  403. reason: '勃艮第红酒的优雅单宁与法式料理的精致风味完美融合',
  404. servingTemperature: '16-18°C',
  405. glassType: '勃艮第杯',
  406. alcoholContent: '13-14%',
  407. flavor: '优雅果香,单宁柔顺',
  408. origin: '法国勃艮第'
  409. },
  410. 意式料理大师: {
  411. name: '基安帝红酒',
  412. type: 'red_wine',
  413. reason: '基安帝的酸度和果香与意式料理的番茄和香草完美搭配',
  414. servingTemperature: '16-18°C',
  415. glassType: '波尔多杯',
  416. alcoholContent: '12-13%',
  417. flavor: '樱桃果香,酸度适中',
  418. origin: '意大利托斯卡纳'
  419. },
  420. 印度料理大师: {
  421. name: '拉西酸奶饮',
  422. type: 'non_alcoholic',
  423. reason: '拉西的奶香和清凉感能很好地缓解印度香料的辛辣',
  424. servingTemperature: '4-6°C',
  425. glassType: '高脚杯',
  426. alcoholContent: '0%',
  427. flavor: '奶香浓郁,清凉甘甜',
  428. origin: '印度'
  429. }
  430. }
  431. // 获取菜系推荐,如果没有则根据食材特点推荐
  432. let winePairing = cuisineWineMap[cuisine.name] || {}
  433. // 如果没有菜系特定推荐,根据食材特点推荐
  434. if (!winePairing.name) {
  435. if (hasSpicy) {
  436. winePairing = {
  437. name: '德国雷司令',
  438. type: 'white_wine',
  439. reason: '甜白酒能很好地平衡辛辣食物,清洁口腔',
  440. servingTemperature: '8-10°C',
  441. glassType: '白酒杯',
  442. alcoholContent: '11-12%',
  443. flavor: '果香浓郁,微甜清新',
  444. origin: '德国'
  445. }
  446. } else if (hasSeafood) {
  447. winePairing = {
  448. name: '长相思白酒',
  449. type: 'white_wine',
  450. reason: '白酒的酸度和矿物质感与海鲜的鲜味完美搭配',
  451. servingTemperature: '8-10°C',
  452. glassType: '白酒杯',
  453. alcoholContent: '12-13%',
  454. flavor: '清新草本,柑橘香气',
  455. origin: '新西兰'
  456. }
  457. } else if (hasMeat && !isLight) {
  458. winePairing = {
  459. name: '赤霞珠红酒',
  460. type: 'red_wine',
  461. reason: '红酒的单宁与肉类的蛋白质结合,提升整体风味',
  462. servingTemperature: '16-18°C',
  463. glassType: '波尔多杯',
  464. alcoholContent: '13-14%',
  465. flavor: '浓郁果香,单宁丰富',
  466. origin: '法国波尔多'
  467. }
  468. } else {
  469. winePairing = {
  470. name: '绿茶',
  471. type: 'tea',
  472. reason: '绿茶的清香淡雅与清淡菜品相得益彰,有助消化',
  473. servingTemperature: '70-80°C',
  474. glassType: '茶杯',
  475. alcoholContent: '0%',
  476. flavor: '清香淡雅,回甘甜美',
  477. origin: '中国'
  478. }
  479. }
  480. }
  481. return {
  482. name: winePairing.name || '绿茶',
  483. type: winePairing.type || 'tea',
  484. reason: winePairing.reason || '经典搭配,有助消化',
  485. servingTemperature: winePairing.servingTemperature || '常温',
  486. glassType: winePairing.glassType,
  487. alcoholContent: winePairing.alcoholContent,
  488. flavor: winePairing.flavor || '清香怡人',
  489. origin: winePairing.origin
  490. }
  491. }
  492. // 导出配置更新函数,供外部使用
  493. export { AI_CONFIG }