aiService.ts 59 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622
  1. import axios from 'axios'
  2. import type {
  3. Recipe,
  4. CuisineType,
  5. NutritionAnalysis,
  6. WinePairing,
  7. SauceRecipe,
  8. SaucePreference,
  9. CustomSauceRequest,
  10. FortuneResult,
  11. DailyFortuneParams,
  12. MoodFortuneParams,
  13. CoupleFortuneParams,
  14. NumberFortuneParams
  15. } from '@/types'
  16. // AI服务配置 - 从环境变量读取(菜谱生成模型配置)
  17. const AI_CONFIG = {
  18. baseURL: import.meta.env.VITE_TEXT_GENERATION_BASE_URL || 'https://api.lingyiwanwu.com/v1/',
  19. apiKey: import.meta.env.VITE_TEXT_GENERATION_API_KEY,
  20. model: import.meta.env.VITE_TEXT_GENERATION_MODEL || 'yi-lightning',
  21. temperature: Number(import.meta.env.VITE_TEXT_GENERATION_TEMPERATURE) || 0.7,
  22. timeout: Number(import.meta.env.VITE_TEXT_GENERATION_TIMEOUT) || 300000
  23. }
  24. // 创建axios实例
  25. const aiClient = axios.create({
  26. baseURL: AI_CONFIG.baseURL,
  27. timeout: AI_CONFIG.timeout,
  28. headers: {
  29. 'Content-Type': 'application/json',
  30. Authorization: `Bearer ${AI_CONFIG.apiKey}`
  31. }
  32. })
  33. /**
  34. * 调用AI接口生成菜谱
  35. * @param ingredients 食材列表
  36. * @param cuisine 菜系信息
  37. * @param customPrompt 自定义提示词(可选)
  38. * @returns Promise<Recipe>
  39. */
  40. export const generateRecipe = async (ingredients: string[], cuisine: CuisineType, customPrompt?: string): Promise<Recipe> => {
  41. try {
  42. // 构建提示词
  43. let prompt = `${cuisine.prompt}
  44. 用户提供的食材:${ingredients.join('、')}`
  45. // 如果有自定义要求,添加到提示词中
  46. if (customPrompt) {
  47. prompt += `
  48. 用户的特殊要求:${customPrompt}`
  49. }
  50. prompt += `
  51. 请按照以下JSON格式返回菜谱,不包含营养分析和酒水搭配:
  52. {
  53. "name": "菜品名称",
  54. "ingredients": ["食材1", "食材2"],
  55. "steps": [
  56. {
  57. "step": 1,
  58. "description": "步骤描述",
  59. "time": 5,
  60. "temperature": "中火"
  61. }
  62. ],
  63. "cookingTime": 30,
  64. "difficulty": "medium",
  65. "tips": ["技巧1", "技巧2"]
  66. }`
  67. // 调用智谱AI接口
  68. const response = await aiClient.post('/chat/completions', {
  69. model: AI_CONFIG.model,
  70. messages: [
  71. {
  72. role: 'system',
  73. content: '你是一位专业的厨师,请根据用户提供的食材和菜系要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
  74. },
  75. {
  76. role: 'user',
  77. content: prompt
  78. }
  79. ],
  80. temperature: AI_CONFIG.temperature,
  81. stream: false
  82. })
  83. // 解析AI响应
  84. const aiResponse = response.data.choices[0].message.content
  85. // 清理响应内容,提取JSON部分
  86. let cleanResponse = aiResponse.trim()
  87. if (cleanResponse.startsWith('```json')) {
  88. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  89. } else if (cleanResponse.startsWith('```')) {
  90. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  91. }
  92. const recipeData = JSON.parse(cleanResponse)
  93. // 构建完整的Recipe对象
  94. const recipe: Recipe = {
  95. id: `recipe-${cuisine.id}-${Date.now()}`,
  96. name: recipeData.name || `${cuisine.name}推荐菜品`,
  97. cuisine: cuisine.name,
  98. ingredients: recipeData.ingredients || ingredients,
  99. steps: recipeData.steps || [
  100. { step: 1, description: '准备所有食材', time: 5 },
  101. { step: 2, description: '按照传统做法烹饪', time: 20 }
  102. ],
  103. cookingTime: recipeData.cookingTime || 25,
  104. difficulty: recipeData.difficulty || 'medium',
  105. tips: recipeData.tips || ['注意火候控制', '调味要适中'],
  106. nutritionAnalysis: undefined,
  107. winePairing: undefined
  108. }
  109. return recipe
  110. } catch (error) {
  111. console.error(`生成${cuisine.name}菜谱失败:`, error)
  112. // 检查是否是400错误或其他特定错误
  113. if (error && typeof error === 'object' && 'response' in error) {
  114. const axiosError = error as any
  115. if (axiosError.response?.status === 400) {
  116. throw new Error(`${cuisine.name}表示这个食材搭配太有挑战性了`)
  117. }
  118. }
  119. // 抛出友好的错误信息
  120. throw new Error(`${cuisine.name}暂时学不会这道菜`)
  121. }
  122. }
  123. /**
  124. * 生成一桌菜的菜单
  125. * @param config 一桌菜配置
  126. * @returns Promise<DishInfo[]>
  127. */
  128. export const generateTableMenu = async (config: {
  129. dishCount: number
  130. flexibleCount: boolean
  131. tastes: string[]
  132. cuisineStyle: string
  133. diningScene: string
  134. nutritionFocus: string
  135. customRequirement: string
  136. customDishes: string[]
  137. }): Promise<
  138. Array<{
  139. name: string
  140. description: string
  141. category: string
  142. tags: string[]
  143. }>
  144. > => {
  145. try {
  146. // 构建提示词
  147. const tasteText = config.tastes.length > 0 ? config.tastes.join('、') : '适中'
  148. const sceneMap = {
  149. family: '家庭聚餐',
  150. friends: '朋友聚会',
  151. romantic: '浪漫晚餐',
  152. business: '商务宴请',
  153. festival: '节日庆祝',
  154. casual: '日常用餐'
  155. }
  156. const nutritionMap = {
  157. balanced: '营养均衡',
  158. protein: '高蛋白',
  159. vegetarian: '素食为主',
  160. low_fat: '低脂健康',
  161. comfort: '滋补养生'
  162. }
  163. const styleMap = {
  164. mixed: '混合菜系',
  165. chinese: '中式为主',
  166. western: '西式为主',
  167. japanese: '日式为主'
  168. }
  169. const styleText = (styleMap as Record<string, string>)[config.cuisineStyle] || '混合菜系'
  170. const sceneText = (sceneMap as Record<string, string>)[config.diningScene] || '家庭聚餐'
  171. const nutritionText = (nutritionMap as Record<string, string>)[config.nutritionFocus] || '营养均衡'
  172. let prompt = `请为我设计一桌菜,要求如下:
  173. - ${config.flexibleCount ? `参考菜品数量:${config.dishCount}道菜(可以根据实际情况智能调整,重点是搭配合理)` : `菜品数量:${config.dishCount}道菜(请严格按照这个数量生成)`}
  174. - 口味偏好:${tasteText}
  175. - 菜系风格:${styleText}
  176. - 用餐场景:${sceneText}
  177. - 营养搭配:${nutritionText}`
  178. if (config.customDishes.length > 0) {
  179. prompt += `\n- ${config.flexibleCount ? '优先考虑的菜品' : '必须包含的菜品'}:${config.customDishes.join('、')}${
  180. config.flexibleCount ? '(可以作为参考,根据搭配需要决定是否全部包含)' : '(请确保这些菜品都包含在菜单中)'
  181. }`
  182. }
  183. if (config.customRequirement) {
  184. prompt += `\n- 特殊要求:${config.customRequirement}`
  185. }
  186. if (config.flexibleCount) {
  187. prompt += `
  188. 智能搭配原则:
  189. 1. 菜品数量可以灵活调整(建议在${Math.max(2, config.dishCount - 2)}-${config.dishCount + 2}道之间),重点是搭配合理、营养均衡
  190. 2. 每道菜应该有独特的特色,避免食材和口味重复过多
  191. 3. 如果指定的菜品较少或重复度高,可以适当减少总菜品数量,确保每道菜都有特色
  192. 4. 优先考虑菜品的多样性和营养搭配,而不是强制凑够指定数量
  193. 5. 合理搭配不同类型的菜品:主菜、素菜、汤品、凉菜、主食等
  194. 6. 避免出现重复搭配,每道菜都应该有独特价值`
  195. } else {
  196. prompt += `
  197. 固定数量原则:
  198. 1. 严格按照${config.dishCount}道菜的数量生成菜单
  199. 2. 确保菜品搭配合理,营养均衡
  200. 3. 每道菜都有独特特色,尽量避免食材重复
  201. 4. 合理分配不同类型的菜品:主菜、素菜、汤品、凉菜、主食等`
  202. }
  203. prompt += `
  204. 请按照以下JSON格式返回菜单:
  205. {
  206. "dishes": [
  207. {
  208. "name": "菜品名称",
  209. "description": "菜品简介和特色描述",
  210. "category": "主菜/素菜/汤品/凉菜/主食/甜品",
  211. "tags": ["标签1", "标签2", "标签3"]
  212. }
  213. ]
  214. }`
  215. const response = await aiClient.post('/chat/completions', {
  216. model: AI_CONFIG.model,
  217. messages: [
  218. {
  219. role: 'system',
  220. content:
  221. '你是一位专业的菜单设计师,擅长根据不同场景和需求搭配合理的菜品组合。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答,包括菜名也要翻译成中文'
  222. },
  223. {
  224. role: 'user',
  225. content: prompt
  226. }
  227. ],
  228. temperature: 0.8,
  229. stream: false
  230. })
  231. // 解析AI响应
  232. const aiResponse = response.data.choices[0].message.content
  233. let cleanResponse = aiResponse.trim()
  234. if (cleanResponse.startsWith('```json')) {
  235. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  236. } else if (cleanResponse.startsWith('```')) {
  237. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  238. }
  239. const menuData = JSON.parse(cleanResponse)
  240. return menuData.dishes || []
  241. } catch (error) {
  242. console.error('生成一桌菜菜单失败:', error)
  243. throw new Error('大厨表示这个菜单搭配太有挑战性了,请稍后重试')
  244. }
  245. }
  246. /**
  247. * 为一桌菜中的单个菜品生成详细菜谱
  248. * @param dishName 菜品名称
  249. * @param dishDescription 菜品描述
  250. * @param category 菜品分类
  251. * @returns Promise<Recipe>
  252. */
  253. export const generateDishRecipe = async (dishName: string, dishDescription: string, category: string): Promise<Recipe> => {
  254. try {
  255. const prompt = `请为以下菜品生成详细的菜谱:
  256. 菜品名称:${dishName}
  257. 菜品描述:${dishDescription}
  258. 菜品分类:${category}
  259. 请按照以下JSON格式返回菜谱:
  260. {
  261. "name": "菜品名称",
  262. "ingredients": ["食材1", "食材2"],
  263. "steps": [
  264. {
  265. "step": 1,
  266. "description": "步骤描述",
  267. "time": 5,
  268. "temperature": "中火"
  269. }
  270. ],
  271. "cookingTime": 30,
  272. "difficulty": "easy/medium/hard",
  273. "tips": ["技巧1", "技巧2"]
  274. }`
  275. const response = await aiClient.post('/chat/completions', {
  276. model: AI_CONFIG.model,
  277. messages: [
  278. {
  279. role: 'system',
  280. content: '你是一位专业的厨师,请根据菜品信息生成详细的制作菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
  281. },
  282. {
  283. role: 'user',
  284. content: prompt
  285. }
  286. ],
  287. temperature: 0.7,
  288. stream: false
  289. })
  290. // 解析AI响应
  291. const aiResponse = response.data.choices[0].message.content
  292. let cleanResponse = aiResponse.trim()
  293. if (cleanResponse.startsWith('```json')) {
  294. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  295. } else if (cleanResponse.startsWith('```')) {
  296. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  297. }
  298. const recipeData = JSON.parse(cleanResponse)
  299. const recipe: Recipe = {
  300. id: `dish-recipe-${Date.now()}`,
  301. name: recipeData.name || dishName,
  302. cuisine: category,
  303. ingredients: recipeData.ingredients || ['主要食材', '调料'],
  304. steps: recipeData.steps || [
  305. { step: 1, description: '准备食材', time: 5 },
  306. { step: 2, description: '开始制作', time: 15 }
  307. ],
  308. cookingTime: recipeData.cookingTime || 20,
  309. difficulty: recipeData.difficulty || 'medium',
  310. tips: recipeData.tips || ['注意火候', '调味适中']
  311. }
  312. return recipe
  313. } catch (error) {
  314. console.error('生成菜品菜谱失败:', error)
  315. throw new Error('大厨表示这道菜太有挑战性了,请稍后重试')
  316. }
  317. }
  318. // 使用自定义提示词生成菜谱
  319. export const generateCustomRecipe = async (ingredients: string[], customPrompt: string): Promise<Recipe> => {
  320. try {
  321. const prompt = `你是一位专业的厨师,请根据用户提供的食材和特殊要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。
  322. 用户提供的食材:${ingredients.join('、')}
  323. 用户的特殊要求:${customPrompt}
  324. 请按照以下JSON格式返回菜谱,不包含营养分析和酒水搭配:
  325. {
  326. "name": "菜品名称",
  327. "ingredients": ["食材1", "食材2"],
  328. "steps": [
  329. {
  330. "step": 1,
  331. "description": "步骤描述",
  332. "time": 5,
  333. "temperature": "中火"
  334. }
  335. ],
  336. "cookingTime": 30,
  337. "difficulty": "medium",
  338. "tips": ["技巧1", "技巧2"]
  339. }`
  340. const response = await aiClient.post('/chat/completions', {
  341. model: AI_CONFIG.model,
  342. messages: [
  343. {
  344. role: 'system',
  345. content: '你是一位专业的厨师,请根据用户提供的食材和特殊要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
  346. },
  347. {
  348. role: 'user',
  349. content: prompt
  350. }
  351. ],
  352. temperature: AI_CONFIG.temperature,
  353. max_tokens: 2000,
  354. stream: false
  355. })
  356. const aiResponse = response.data.choices[0].message.content
  357. let cleanResponse = aiResponse.trim()
  358. if (cleanResponse.startsWith('```json')) {
  359. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  360. } else if (cleanResponse.startsWith('```')) {
  361. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  362. }
  363. const recipeData = JSON.parse(cleanResponse)
  364. const recipe: Recipe = {
  365. id: `recipe-custom-${Date.now()}`,
  366. name: recipeData.name || '自定义菜品',
  367. cuisine: '自定义',
  368. ingredients: recipeData.ingredients || ingredients,
  369. steps: recipeData.steps || [
  370. { step: 1, description: '准备所有食材', time: 5 },
  371. { step: 2, description: '按照要求烹饪', time: 20 }
  372. ],
  373. cookingTime: recipeData.cookingTime || 25,
  374. difficulty: recipeData.difficulty || 'medium',
  375. tips: recipeData.tips || ['根据个人口味调整', '注意火候控制'],
  376. nutritionAnalysis: undefined,
  377. winePairing: undefined
  378. }
  379. return recipe
  380. } catch (error) {
  381. console.error('生成自定义菜谱失败:', error)
  382. throw new Error('大厨表示这个自定义要求太有挑战性了,请稍后重试')
  383. }
  384. }
  385. // 流式生成多个菜系的菜谱
  386. export const generateMultipleRecipesStream = async (
  387. ingredients: string[],
  388. cuisines: CuisineType[],
  389. onRecipeGenerated: (recipe: Recipe, index: number, total: number) => void,
  390. onRecipeError?: (error: Error, index: number, cuisine: CuisineType, total: number) => void,
  391. customPrompt?: string
  392. ): Promise<void> => {
  393. const total = cuisines.length
  394. let completedCount = 0
  395. // 为了更好的用户体验,我们不并行生成,而是依次生成
  396. // 这样用户可以看到一个个菜谱依次完成的效果
  397. for (let index = 0; index < cuisines.length; index++) {
  398. const cuisine = cuisines[index]
  399. try {
  400. // 添加一些随机延迟,让生成过程更自然
  401. const delay = 1000 + Math.random() * 2000 // 1-3秒的随机延迟
  402. await new Promise(resolve => setTimeout(resolve, delay))
  403. const recipe = await generateRecipe(ingredients, cuisine, customPrompt)
  404. completedCount++
  405. onRecipeGenerated(recipe, index, total)
  406. } catch (error) {
  407. console.error(`生成${cuisine.name}菜谱失败:`, error)
  408. // 调用错误回调,让前端可以显示友好的错误信息
  409. if (onRecipeError) {
  410. const friendlyError = new Error(`${cuisine.name}不会这道菜,哈哈`)
  411. onRecipeError(friendlyError, index, cuisine, total)
  412. }
  413. // 即使某个菜系失败,也继续生成其他菜系
  414. continue
  415. }
  416. }
  417. if (completedCount === 0) {
  418. throw new Error('所有菜系生成都失败了,请稍后重试')
  419. }
  420. if (completedCount < total) {
  421. console.warn(`${total - completedCount}个菜系生成失败,但已成功生成${completedCount}个菜谱`)
  422. }
  423. }
  424. /**
  425. * 获取菜谱的营养分析
  426. * @param recipe 菜谱信息
  427. * @returns Promise<NutritionAnalysis>
  428. */
  429. export const getNutritionAnalysis = async (recipe: Recipe): Promise<NutritionAnalysis> => {
  430. try {
  431. const prompt = `请为以下菜谱生成详细的营养分析:
  432. 菜名:${recipe.name}
  433. 食材:${recipe.ingredients.join('、')}
  434. 烹饪方法:${recipe.steps.map(step => step.description).join(',')}
  435. 请按照以下JSON格式返回营养分析:
  436. {
  437. "nutrition": {
  438. "calories": 350,
  439. "protein": 25,
  440. "carbs": 45,
  441. "fat": 12,
  442. "fiber": 8,
  443. "sodium": 800,
  444. "sugar": 6,
  445. "vitaminC": 30,
  446. "calcium": 150,
  447. "iron": 3
  448. },
  449. "healthScore": 8,
  450. "balanceAdvice": ["建议搭配蔬菜沙拉增加维生素", "可适量减少盐分"],
  451. "dietaryTags": ["高蛋白", "低脂"],
  452. "servingSize": "1人份"
  453. }`
  454. const response = await aiClient.post('/chat/completions', {
  455. model: AI_CONFIG.model,
  456. messages: [
  457. {
  458. role: 'system',
  459. content: '你是一位专业的营养师,请根据菜谱信息生成详细的营养分析。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答,包括菜名也要翻译成中文'
  460. },
  461. {
  462. role: 'user',
  463. content: prompt
  464. }
  465. ],
  466. temperature: 0.5, // 使用更低的temperature以获得更准确的分析
  467. stream: false
  468. })
  469. // 解析AI响应
  470. const aiResponse = response.data.choices[0].message.content
  471. let cleanResponse = aiResponse.trim()
  472. if (cleanResponse.startsWith('```json')) {
  473. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  474. } else if (cleanResponse.startsWith('```')) {
  475. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  476. }
  477. const nutritionData = JSON.parse(cleanResponse)
  478. return nutritionData
  479. } catch (error) {
  480. console.error('获取营养分析失败:', error)
  481. return generateFallbackNutrition(recipe.ingredients)
  482. }
  483. }
  484. /**
  485. * 获取菜谱的酒水搭配建议
  486. * @param recipe 菜谱信息
  487. * @returns Promise<WinePairing>
  488. */
  489. export const getWinePairing = async (recipe: Recipe): Promise<WinePairing> => {
  490. try {
  491. const prompt = `请为以下菜谱推荐合适的酒水搭配:
  492. 菜名:${recipe.name}
  493. 菜系:${recipe.cuisine}
  494. 食材:${recipe.ingredients.join('、')}
  495. 请按照以下JSON格式返回酒水搭配建议:
  496. {
  497. "name": "推荐酒水名称",
  498. "type": "red_wine",
  499. "reason": "搭配理由说明",
  500. "servingTemperature": "16-18°C",
  501. "glassType": "波尔多杯",
  502. "alcoholContent": "13.5%",
  503. "flavor": "风味描述",
  504. "origin": "产地"
  505. }`
  506. const response = await aiClient.post('/chat/completions', {
  507. model: AI_CONFIG.model,
  508. messages: [
  509. {
  510. role: 'system',
  511. content: '你是一位专业的侍酒师,请根据菜谱信息推荐合适的酒水搭配。请严格按照JSON格式返回,不要包含任何其他文字。'
  512. },
  513. {
  514. role: 'user',
  515. content: prompt
  516. }
  517. ],
  518. temperature: 0.7,
  519. stream: false
  520. })
  521. // 解析AI响应
  522. const aiResponse = response.data.choices[0].message.content
  523. let cleanResponse = aiResponse.trim()
  524. if (cleanResponse.startsWith('```json')) {
  525. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  526. } else if (cleanResponse.startsWith('```')) {
  527. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  528. }
  529. const wineData = JSON.parse(cleanResponse)
  530. return wineData
  531. } catch (error) {
  532. console.error('获取酒水搭配失败:', error)
  533. return generateFallbackWinePairing({ id: 'custom', name: recipe.cuisine } as CuisineType, recipe.ingredients)
  534. }
  535. }
  536. // 生成后备营养分析数据
  537. const generateFallbackNutrition = (ingredients: string[]): NutritionAnalysis => {
  538. const baseCalories = ingredients.length * 50 + Math.floor(Math.random() * 100) + 200
  539. const hasVegetables = ingredients.some(ing => ['菜', '瓜', '豆', '萝卜', '白菜', '菠菜', '西红柿', '黄瓜', '茄子', '土豆'].some(veg => ing.includes(veg)))
  540. const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
  541. const hasGrains = ingredients.some(ing => ['米', '面', '粉', '饭', '面条', '馒头'].some(grain => ing.includes(grain)))
  542. const dietaryTags: string[] = []
  543. if (hasVegetables && !hasMeat) dietaryTags.push('素食')
  544. if (hasMeat) dietaryTags.push('高蛋白')
  545. if (hasVegetables) dietaryTags.push('富含维生素')
  546. if (!hasGrains) dietaryTags.push('低碳水')
  547. if (baseCalories < 300) dietaryTags.push('低卡路里')
  548. const balanceAdvice: string[] = []
  549. if (!hasVegetables) balanceAdvice.push('建议搭配新鲜蔬菜增加维生素和膳食纤维')
  550. if (!hasMeat && !ingredients.some(ing => ['豆', '蛋', '奶'].some(protein => ing.includes(protein)))) {
  551. balanceAdvice.push('建议增加蛋白质来源,如豆类或蛋类')
  552. }
  553. if (hasGrains && hasMeat) balanceAdvice.push('营养搭配均衡,适合日常食用')
  554. if (ingredients.length > 5) balanceAdvice.push('食材丰富,营养全面')
  555. return {
  556. nutrition: {
  557. calories: baseCalories,
  558. protein: hasMeat ? 20 + Math.floor(Math.random() * 15) : 8 + Math.floor(Math.random() * 8),
  559. carbs: hasGrains ? 35 + Math.floor(Math.random() * 20) : 15 + Math.floor(Math.random() * 10),
  560. fat: hasMeat ? 12 + Math.floor(Math.random() * 8) : 5 + Math.floor(Math.random() * 5),
  561. fiber: hasVegetables ? 6 + Math.floor(Math.random() * 4) : 2 + Math.floor(Math.random() * 2),
  562. sodium: 600 + Math.floor(Math.random() * 400),
  563. sugar: 3 + Math.floor(Math.random() * 5),
  564. vitaminC: hasVegetables ? 20 + Math.floor(Math.random() * 30) : undefined,
  565. calcium: hasMeat || ingredients.some(ing => ['奶', '豆'].some(ca => ing.includes(ca))) ? 100 + Math.floor(Math.random() * 100) : undefined,
  566. iron: hasMeat ? 2 + Math.floor(Math.random() * 3) : undefined
  567. },
  568. healthScore: Math.floor(Math.random() * 3) + (hasVegetables ? 6 : 4) + (hasMeat ? 1 : 0),
  569. balanceAdvice: balanceAdvice.length > 0 ? balanceAdvice : ['营养搭配合理,可以放心享用'],
  570. dietaryTags: dietaryTags.length > 0 ? dietaryTags : ['家常菜'],
  571. servingSize: '1人份'
  572. }
  573. }
  574. // 生成后备酒水搭配数据
  575. const generateFallbackWinePairing = (cuisine: CuisineType, ingredients: string[]): WinePairing => {
  576. const hasSpicy = ingredients.some(ing => ['辣椒', '花椒', '胡椒', '姜', '蒜', '洋葱'].some(spice => ing.includes(spice)))
  577. const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
  578. const hasSeafood = ingredients.some(ing => ['鱼', '虾', '蟹', '贝', '海带', '紫菜'].some(seafood => ing.includes(seafood)))
  579. const isLight = ingredients.some(ing => ['菜', '瓜', '豆腐', '蛋'].some(light => ing.includes(light)))
  580. const cuisineWineMap: Record<string, Partial<WinePairing>> = {
  581. 川菜大师: {
  582. name: '德国雷司令',
  583. type: 'white_wine',
  584. reason: '雷司令的甜度和酸度能很好地平衡川菜的麻辣,清洁口腔',
  585. servingTemperature: '8-10°C',
  586. glassType: '雷司令杯',
  587. alcoholContent: '11-12%',
  588. flavor: '清新果香,微甜带酸',
  589. origin: '德国莱茵河谷'
  590. },
  591. 粤菜大师: {
  592. name: '香槟',
  593. type: 'white_wine',
  594. reason: '香槟的气泡和酸度与粤菜的清淡鲜美完美搭配',
  595. servingTemperature: '6-8°C',
  596. glassType: '香槟杯',
  597. alcoholContent: '12%',
  598. flavor: '清爽气泡,柑橘香气',
  599. origin: '法国香槟区'
  600. },
  601. 日式料理大师: {
  602. name: '清酒',
  603. type: 'sake',
  604. reason: '清酒的清淡甘甜与日式料理的鲜美本味相得益彰',
  605. servingTemperature: '10-15°C',
  606. glassType: '清酒杯',
  607. alcoholContent: '15-16%',
  608. flavor: '清香甘甜,口感顺滑',
  609. origin: '日本'
  610. }
  611. }
  612. let winePairing = cuisineWineMap[cuisine.name] || {}
  613. if (!winePairing.name) {
  614. if (hasSpicy) {
  615. winePairing = {
  616. name: '德国雷司令',
  617. type: 'white_wine',
  618. reason: '甜白酒能很好地平衡辛辣食物,清洁口腔',
  619. servingTemperature: '8-10°C',
  620. glassType: '白酒杯',
  621. alcoholContent: '11-12%',
  622. flavor: '果香浓郁,微甜清新',
  623. origin: '德国'
  624. }
  625. } else if (hasSeafood) {
  626. winePairing = {
  627. name: '长相思白酒',
  628. type: 'white_wine',
  629. reason: '白酒的酸度和矿物质感与海鲜的鲜味完美搭配',
  630. servingTemperature: '8-10°C',
  631. glassType: '白酒杯',
  632. alcoholContent: '12-13%',
  633. flavor: '清新草本,柑橘香气',
  634. origin: '新西兰'
  635. }
  636. } else if (hasMeat && !isLight) {
  637. winePairing = {
  638. name: '赤霞珠红酒',
  639. type: 'red_wine',
  640. reason: '红酒的单宁与肉类的蛋白质结合,提升整体风味',
  641. servingTemperature: '16-18°C',
  642. glassType: '波尔多杯',
  643. alcoholContent: '13-14%',
  644. flavor: '浓郁果香,单宁丰富',
  645. origin: '法国波尔多'
  646. }
  647. } else {
  648. winePairing = {
  649. name: '绿茶',
  650. type: 'tea',
  651. reason: '绿茶的清香淡雅与清淡菜品相得益彰,有助消化',
  652. servingTemperature: '70-80°C',
  653. glassType: '茶杯',
  654. alcoholContent: '0%',
  655. flavor: '清香淡雅,回甘甜美',
  656. origin: '中国'
  657. }
  658. }
  659. }
  660. return {
  661. name: winePairing.name || '绿茶',
  662. type: winePairing.type || 'tea',
  663. reason: winePairing.reason || '经典搭配,有助消化',
  664. servingTemperature: winePairing.servingTemperature || '常温',
  665. glassType: winePairing.glassType,
  666. alcoholContent: winePairing.alcoholContent,
  667. flavor: winePairing.flavor || '清香怡人',
  668. origin: winePairing.origin
  669. }
  670. }
  671. /**
  672. * 测试AI服务连接
  673. * @returns Promise<boolean>
  674. */
  675. export const testAIConnection = async (): Promise<boolean> => {
  676. try {
  677. const response = await aiClient.post('/chat/completions', {
  678. model: AI_CONFIG.model,
  679. messages: [
  680. {
  681. role: 'user',
  682. content: '你好'
  683. }
  684. ],
  685. max_tokens: 10
  686. })
  687. return response.status === 200
  688. } catch (error) {
  689. console.error('AI服务连接测试失败:', error)
  690. return false
  691. }
  692. }
  693. /**
  694. * 根据菜名生成详细菜谱
  695. * @param dishName 菜品名称
  696. * @returns Promise<Recipe>
  697. */
  698. export const generateDishRecipeByName = async (dishName: string): Promise<Recipe> => {
  699. try {
  700. const prompt = `请为"${dishName}"这道菜生成详细的制作教程。
  701. 要求:
  702. 1. 提供完整的食材清单(包括主料和调料)
  703. 2. 详细的制作步骤,每个步骤要包含具体的时间和火候
  704. 3. 实用的烹饪技巧和注意事项
  705. 4. 如果是地方菜,请说明其特色和来源
  706. 请按照以下JSON格式返回菜谱:
  707. {
  708. "name": "菜品名称",
  709. "ingredients": ["主料1 200g", "调料1 适量", "调料2 1勺"],
  710. "steps": [
  711. {
  712. "step": 1,
  713. "description": "详细的步骤描述,包含具体操作方法",
  714. "time": 5,
  715. "temperature": "中火/大火/小火"
  716. }
  717. ],
  718. "cookingTime": 30,
  719. "difficulty": "easy/medium/hard",
  720. "tips": ["实用技巧1", "注意事项2", "口感调节3"]
  721. }`
  722. const response = await aiClient.post('/chat/completions', {
  723. model: AI_CONFIG.model,
  724. messages: [
  725. {
  726. role: 'system',
  727. content:
  728. '你是一位经验丰富的中华料理大师,精通各种菜系的制作方法。请根据用户提供的菜名,生成详细、实用的制作教程。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  729. },
  730. {
  731. role: 'user',
  732. content: prompt
  733. }
  734. ],
  735. temperature: 0.7,
  736. stream: false
  737. })
  738. // 解析AI响应
  739. const aiResponse = response.data.choices[0].message.content
  740. let cleanResponse = aiResponse.trim()
  741. if (cleanResponse.startsWith('```json')) {
  742. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  743. } else if (cleanResponse.startsWith('```')) {
  744. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  745. }
  746. const recipeData = JSON.parse(cleanResponse)
  747. // 构建完整的Recipe对象
  748. const recipe: Recipe = {
  749. id: `dish-search-${Date.now()}`,
  750. name: recipeData.name || dishName,
  751. cuisine: '传统菜谱',
  752. ingredients: recipeData.ingredients || ['主要食材', '调料'],
  753. steps: recipeData.steps || [
  754. { step: 1, description: '准备所有食材', time: 5 },
  755. { step: 2, description: '按照传统方法制作', time: 20 }
  756. ],
  757. cookingTime: recipeData.cookingTime || 25,
  758. difficulty: recipeData.difficulty || 'medium',
  759. tips: recipeData.tips || ['注意火候控制', '调味要适中'],
  760. nutritionAnalysis: undefined,
  761. winePairing: undefined
  762. }
  763. return recipe
  764. } catch (error) {
  765. console.error(`生成"${dishName}"菜谱失败:`, error)
  766. throw new Error(`大厨表示"${dishName}"这道菜太有挑战性了,请稍后重试`)
  767. }
  768. }
  769. /**
  770. * 根据酱料名称生成详细制作方法
  771. * @param sauceName 酱料名称
  772. * @returns Promise<SauceRecipe>
  773. */
  774. export const generateSauceRecipe = async (sauceName: string): Promise<SauceRecipe> => {
  775. try {
  776. const prompt = `请为"${sauceName}"这种酱料生成详细的制作教程。
  777. 要求:
  778. 1. 提供完整的食材清单(主料、辅料、调料)
  779. 2. 详细的制作步骤,包含具体操作方法、时间、温度控制
  780. 3. 实用的制作技巧和注意事项
  781. 4. 保存方法和保质期信息
  782. 5. 推荐的搭配菜品
  783. 6. 口味特点描述
  784. 请按照以下JSON格式返回酱料配方:
  785. {
  786. "name": "酱料名称",
  787. "category": "spicy/garlic/sweet/complex/regional/fusion",
  788. "ingredients": ["主料1 200g", "调料1 适量", "调料2 1勺"],
  789. "steps": [
  790. {
  791. "step": 1,
  792. "description": "详细的步骤描述",
  793. "time": 5,
  794. "temperature": "中火",
  795. "technique": "炒制"
  796. }
  797. ],
  798. "makingTime": 30,
  799. "difficulty": "easy/medium/hard",
  800. "tips": ["制作技巧1", "注意事项2"],
  801. "storage": {
  802. "method": "密封冷藏",
  803. "duration": "1个月",
  804. "temperature": "4°C"
  805. },
  806. "pairings": ["搭配菜品1", "搭配菜品2"],
  807. "tags": ["标签1", "标签2"],
  808. "spiceLevel": 3,
  809. "sweetLevel": 1,
  810. "saltLevel": 4,
  811. "sourLevel": 2,
  812. "description": "酱料特色描述"
  813. }`
  814. const response = await aiClient.post('/chat/completions', {
  815. model: AI_CONFIG.model,
  816. messages: [
  817. {
  818. role: 'system',
  819. content:
  820. '你是一位专业的酱料制作大师,精通各种传统和创新酱料的制作方法。请根据用户提供的酱料名称,生成详细、实用的制作教程。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  821. },
  822. {
  823. role: 'user',
  824. content: prompt
  825. }
  826. ],
  827. temperature: 0.7,
  828. stream: false
  829. })
  830. const aiResponse = response.data.choices[0].message.content
  831. let cleanResponse = aiResponse.trim()
  832. if (cleanResponse.startsWith('```json')) {
  833. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  834. } else if (cleanResponse.startsWith('```')) {
  835. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  836. }
  837. const sauceData = JSON.parse(cleanResponse)
  838. const sauceRecipe: SauceRecipe = {
  839. id: `sauce-${Date.now()}`,
  840. name: sauceData.name || sauceName,
  841. category: sauceData.category || 'complex',
  842. ingredients: sauceData.ingredients || ['主要食材', '调料'],
  843. steps: sauceData.steps || [
  844. { step: 1, description: '准备所有食材', time: 5 },
  845. { step: 2, description: '按照传统方法制作', time: 20 }
  846. ],
  847. makingTime: sauceData.makingTime || 25,
  848. difficulty: sauceData.difficulty || 'medium',
  849. tips: sauceData.tips || ['注意火候控制', '调味要适中'],
  850. storage: sauceData.storage || {
  851. method: '密封保存',
  852. duration: '1周',
  853. temperature: '常温'
  854. },
  855. pairings: sauceData.pairings || ['面条', '蔬菜'],
  856. tags: sauceData.tags || ['家常', '经典'],
  857. spiceLevel: sauceData.spiceLevel || 2,
  858. sweetLevel: sauceData.sweetLevel || 2,
  859. saltLevel: sauceData.saltLevel || 3,
  860. sourLevel: sauceData.sourLevel || 2,
  861. description: sauceData.description || '经典酱料配方'
  862. }
  863. return sauceRecipe
  864. } catch (error) {
  865. console.error(`生成"${sauceName}"酱料配方失败:`, error)
  866. throw new Error(`酱料大师表示"${sauceName}"这个配方太有挑战性了,请稍后重试`)
  867. }
  868. }
  869. /**
  870. * 根据用户偏好推荐酱料
  871. * @param preferences 用户偏好配置
  872. * @returns Promise<string[]>
  873. */
  874. export const recommendSauces = async (preferences: SaucePreference): Promise<string[]> => {
  875. try {
  876. const useCaseMap = {
  877. noodles: '拌面',
  878. dipping: '蘸菜',
  879. cooking: '炒菜',
  880. bbq: '烧烤',
  881. hotpot: '火锅'
  882. }
  883. const useCaseText = preferences.useCase.map(uc => (useCaseMap as Record<string, string>)[uc] || uc).join('、')
  884. const ingredientsText = preferences.availableIngredients.length > 0 ? preferences.availableIngredients.join('、') : '无特殊要求'
  885. const prompt = `请根据以下用户偏好推荐合适的酱料:
  886. 用户偏好:
  887. - 辣度偏好:${preferences.spiceLevel}/5
  888. - 甜度偏好:${preferences.sweetLevel}/5
  889. - 咸度偏好:${preferences.saltLevel}/5
  890. - 酸度偏好:${preferences.sourLevel}/5
  891. - 使用场景:${useCaseText}
  892. - 现有食材:${ingredientsText}
  893. 请推荐5-8种最适合的酱料,按匹配度排序。
  894. 请按照以下JSON格式返回推荐结果:
  895. {
  896. "recommendations": [
  897. "酱料名称1",
  898. "酱料名称2",
  899. "酱料名称3"
  900. ]
  901. }`
  902. const response = await aiClient.post('/chat/completions', {
  903. model: AI_CONFIG.model,
  904. messages: [
  905. {
  906. role: 'system',
  907. content: '你是一位专业的酱料推荐专家,能够根据用户的口味偏好和使用场景推荐最合适的酱料。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  908. },
  909. {
  910. role: 'user',
  911. content: prompt
  912. }
  913. ],
  914. temperature: 0.8,
  915. stream: false
  916. })
  917. const aiResponse = response.data.choices[0].message.content
  918. let cleanResponse = aiResponse.trim()
  919. if (cleanResponse.startsWith('```json')) {
  920. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  921. } else if (cleanResponse.startsWith('```')) {
  922. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  923. }
  924. const recommendationData = JSON.parse(cleanResponse)
  925. return recommendationData.recommendations || []
  926. } catch (error) {
  927. console.error('获取酱料推荐失败:', error)
  928. throw new Error('酱料大师暂时想不出好的推荐,请稍后重试')
  929. }
  930. }
  931. /**
  932. * 创建自定义酱料配方
  933. * @param request 自定义酱料创作请求
  934. * @returns Promise<SauceRecipe>
  935. */
  936. export const createCustomSauce = async (request: CustomSauceRequest): Promise<SauceRecipe> => {
  937. try {
  938. const baseTypeMap = {
  939. oil: '油性酱料',
  940. water: '水性酱料',
  941. paste: '膏状酱料',
  942. granular: '颗粒状酱料'
  943. }
  944. const flavorMap = {
  945. spicy: '辣味',
  946. sweet: '甜味',
  947. sour: '酸味',
  948. umami: '鲜味',
  949. aromatic: '香味'
  950. }
  951. const prompt = `请根据以下要求创作一个独特的酱料配方:
  952. 创作要求:
  953. - 基础类型:${baseTypeMap[request.baseType]}
  954. - 主要风味:${flavorMap[request.flavorDirection]}
  955. - 特殊食材:${request.specialIngredients.join('、')}
  956. - 期望口感:${request.expectedTexture}
  957. - 用途:${request.intendedUse}
  958. ${request.customRequirements ? `- 特殊要求:${request.customRequirements}` : ''}
  959. 请创作一个创新的酱料配方,要有独特性和实用性。
  960. 请按照以下JSON格式返回酱料配方:
  961. {
  962. "name": "创新酱料名称",
  963. "category": "fusion",
  964. "ingredients": ["主料1 200g", "调料1 适量"],
  965. "steps": [
  966. {
  967. "step": 1,
  968. "description": "详细步骤描述",
  969. "time": 5,
  970. "temperature": "中火",
  971. "technique": "制作技法"
  972. }
  973. ],
  974. "makingTime": 30,
  975. "difficulty": "medium",
  976. "tips": ["创作技巧1", "注意事项2"],
  977. "storage": {
  978. "method": "保存方法",
  979. "duration": "保质期",
  980. "temperature": "保存温度"
  981. },
  982. "pairings": ["搭配建议1", "搭配建议2"],
  983. "tags": ["创新", "自制"],
  984. "spiceLevel": 3,
  985. "sweetLevel": 2,
  986. "saltLevel": 3,
  987. "sourLevel": 2,
  988. "description": "创新酱料特色描述"
  989. }`
  990. const response = await aiClient.post('/chat/completions', {
  991. model: AI_CONFIG.model,
  992. messages: [
  993. {
  994. role: 'system',
  995. content: '你是一位富有创意的酱料创作大师,擅长根据用户需求创作独特的酱料配方。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  996. },
  997. {
  998. role: 'user',
  999. content: prompt
  1000. }
  1001. ],
  1002. temperature: 0.9,
  1003. stream: false
  1004. })
  1005. const aiResponse = response.data.choices[0].message.content
  1006. let cleanResponse = aiResponse.trim()
  1007. if (cleanResponse.startsWith('```json')) {
  1008. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  1009. } else if (cleanResponse.startsWith('```')) {
  1010. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  1011. }
  1012. const sauceData = JSON.parse(cleanResponse)
  1013. const customSauce: SauceRecipe = {
  1014. id: `custom-sauce-${Date.now()}`,
  1015. name: sauceData.name || '自创酱料',
  1016. category: 'fusion',
  1017. ingredients: sauceData.ingredients || ['创新食材'],
  1018. steps: sauceData.steps || [
  1019. { step: 1, description: '准备创新食材', time: 5 },
  1020. { step: 2, description: '按照创意方法制作', time: 20 }
  1021. ],
  1022. makingTime: sauceData.makingTime || 25,
  1023. difficulty: sauceData.difficulty || 'medium',
  1024. tips: sauceData.tips || ['发挥创意', '调整口味'],
  1025. storage: sauceData.storage || {
  1026. method: '密封保存',
  1027. duration: '1周',
  1028. temperature: '冷藏'
  1029. },
  1030. pairings: sauceData.pairings || ['创意搭配'],
  1031. tags: sauceData.tags || ['创新', '自制'],
  1032. spiceLevel: sauceData.spiceLevel || 2,
  1033. sweetLevel: sauceData.sweetLevel || 2,
  1034. saltLevel: sauceData.saltLevel || 3,
  1035. sourLevel: sauceData.sourLevel || 2,
  1036. description: sauceData.description || '独特的自创酱料'
  1037. }
  1038. return customSauce
  1039. } catch (error) {
  1040. console.error('创建自定义酱料失败:', error)
  1041. throw new Error('酱料大师表示这个自定义配方太有挑战性了,请稍后重试')
  1042. }
  1043. }
  1044. /**
  1045. * 获取酱料搭配建议
  1046. * @param sauceName 酱料名称
  1047. * @returns Promise<string[]>
  1048. */
  1049. export const getSaucePairings = async (sauceName: string): Promise<string[]> => {
  1050. try {
  1051. const prompt = `请为"${sauceName}"这种酱料推荐最佳的搭配菜品和使用方法。
  1052. 要求:
  1053. 1. 推荐5-8种最适合的搭配菜品
  1054. 2. 说明具体的使用方法
  1055. 3. 考虑不同的烹饪场景
  1056. 请按照以下JSON格式返回搭配建议:
  1057. {
  1058. "pairings": [
  1059. "搭配菜品1 - 使用方法",
  1060. "搭配菜品2 - 使用方法",
  1061. "搭配菜品3 - 使用方法"
  1062. ]
  1063. }`
  1064. const response = await aiClient.post('/chat/completions', {
  1065. model: AI_CONFIG.model,
  1066. messages: [
  1067. {
  1068. role: 'system',
  1069. content: '你是一位专业的美食搭配专家,精通各种酱料与菜品的最佳搭配方法。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  1070. },
  1071. {
  1072. role: 'user',
  1073. content: prompt
  1074. }
  1075. ],
  1076. temperature: 0.7,
  1077. stream: false
  1078. })
  1079. const aiResponse = response.data.choices[0].message.content
  1080. let cleanResponse = aiResponse.trim()
  1081. if (cleanResponse.startsWith('```json')) {
  1082. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  1083. } else if (cleanResponse.startsWith('```')) {
  1084. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  1085. }
  1086. const pairingData = JSON.parse(cleanResponse)
  1087. return pairingData.pairings || []
  1088. } catch (error) {
  1089. console.error('获取酱料搭配建议失败:', error)
  1090. return ['面条 - 拌面使用', '蔬菜 - 蘸食使用', '肉类 - 调味使用']
  1091. }
  1092. }
  1093. /**
  1094. * 生成今日运势菜
  1095. * @param params 运势参数
  1096. * @returns Promise<FortuneResult>
  1097. */
  1098. export const generateDailyFortune = async (params: DailyFortuneParams): Promise<FortuneResult> => {
  1099. try {
  1100. const prompt = `你是一位神秘的料理占卜师,请根据以下信息为用户推荐今日幸运菜:
  1101. 星座:${params.zodiac}
  1102. 生肖:${params.animal}
  1103. 日期:${params.date}
  1104. 请结合星座特性、生肖属性和今日能量,推荐一道能带来好运的菜品。
  1105. 请按照以下JSON格式返回占卜结果:
  1106. {
  1107. "dishName": "菜品名称",
  1108. "reason": "选择这道菜的占卜理由",
  1109. "luckyIndex": 8,
  1110. "description": "详细的占卜解析和菜品介绍",
  1111. "tips": ["烹饪技巧1", "幸运提示2"],
  1112. "difficulty": "medium",
  1113. "cookingTime": 30,
  1114. "mysticalMessage": "神秘的占卜师话语",
  1115. "ingredients": ["主要食材1", "主要食材2"],
  1116. "steps": ["制作步骤1", "制作步骤2"]
  1117. }`
  1118. const response = await aiClient.post('/chat/completions', {
  1119. model: AI_CONFIG.model,
  1120. messages: [
  1121. {
  1122. role: 'system',
  1123. content:
  1124. '你是一位神秘而智慧的料理占卜师,精通星座学、生肖学和美食文化。你的话语充满神秘色彩,善于将占卜元素与美食完美结合。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  1125. },
  1126. {
  1127. role: 'user',
  1128. content: prompt
  1129. }
  1130. ],
  1131. temperature: 0.8,
  1132. stream: false
  1133. })
  1134. const aiResponse = response.data.choices[0].message.content
  1135. let cleanResponse = aiResponse.trim()
  1136. if (cleanResponse.startsWith('```json')) {
  1137. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  1138. } else if (cleanResponse.startsWith('```')) {
  1139. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  1140. }
  1141. const fortuneData = JSON.parse(cleanResponse)
  1142. const fortune: FortuneResult = {
  1143. id: `daily-fortune-${Date.now()}`,
  1144. type: 'daily',
  1145. date: params.date,
  1146. dishName: fortuneData.dishName || '幸运料理',
  1147. reason: fortuneData.reason || '星座与生肖的神秘指引',
  1148. luckyIndex: fortuneData.luckyIndex || Math.floor(Math.random() * 3) + 7,
  1149. description: fortuneData.description || '这道菜将为您带来今日好运',
  1150. tips: fortuneData.tips || ['用心制作', '保持好心情'],
  1151. difficulty: fortuneData.difficulty || 'medium',
  1152. cookingTime: fortuneData.cookingTime || 30,
  1153. mysticalMessage: fortuneData.mysticalMessage || '命运之轮正在转动,美味即将降临...',
  1154. ingredients: fortuneData.ingredients || [],
  1155. steps: fortuneData.steps || []
  1156. }
  1157. return fortune
  1158. } catch (error) {
  1159. console.error('生成今日运势菜失败:', error)
  1160. throw new Error('占卜师暂时无法感应到星象,请稍后重试')
  1161. }
  1162. }
  1163. /**
  1164. * 生成心情料理
  1165. * @param params 心情参数
  1166. * @returns Promise<FortuneResult>
  1167. */
  1168. export const generateMoodCooking = async (params: MoodFortuneParams): Promise<FortuneResult> => {
  1169. try {
  1170. const moodText = params.moods.join('、')
  1171. const intensityText = ['很轻微', '轻微', '一般', '强烈', '非常强烈'][params.intensity - 1]
  1172. const prompt = `你是一位擅长情感治愈的料理占卜师,请根据以下心情状态推荐治愈菜品:
  1173. 当前心情:${moodText}
  1174. 情绪强度:${intensityText}
  1175. 请推荐一道能够治愈这种心情的菜品,并给出温暖的情感分析。
  1176. 请按照以下JSON格式返回占卜结果:
  1177. {
  1178. "dishName": "菜品名称",
  1179. "reason": "这道菜如何治愈当前心情",
  1180. "luckyIndex": 7,
  1181. "description": "详细的情感分析和菜品治愈功效",
  1182. "tips": ["情感治愈建议1", "烹饪心得2"],
  1183. "difficulty": "easy",
  1184. "cookingTime": 25,
  1185. "mysticalMessage": "温暖治愈的话语",
  1186. "ingredients": ["治愈食材1", "治愈食材2"],
  1187. "steps": ["治愈步骤1", "治愈步骤2"]
  1188. }`
  1189. const response = await aiClient.post('/chat/completions', {
  1190. model: AI_CONFIG.model,
  1191. messages: [
  1192. {
  1193. role: 'system',
  1194. content:
  1195. '你是一位温暖而智慧的情感治愈师,深谙美食与情感的关系。你善于通过菜品来抚慰人心,话语温暖治愈。请严格按照JSON���式返回,不要包含任何其他文字。请务必用中文回答。'
  1196. },
  1197. {
  1198. role: 'user',
  1199. content: prompt
  1200. }
  1201. ],
  1202. temperature: 0.7,
  1203. stream: false
  1204. })
  1205. const aiResponse = response.data.choices[0].message.content
  1206. let cleanResponse = aiResponse.trim()
  1207. if (cleanResponse.startsWith('```json')) {
  1208. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  1209. } else if (cleanResponse.startsWith('```')) {
  1210. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  1211. }
  1212. const fortuneData = JSON.parse(cleanResponse)
  1213. const fortune: FortuneResult = {
  1214. id: `mood-fortune-${Date.now()}`,
  1215. type: 'mood',
  1216. date: new Date().toISOString().split('T')[0],
  1217. dishName: fortuneData.dishName || '治愈料理',
  1218. reason: fortuneData.reason || '这道菜能温暖你的心',
  1219. luckyIndex: fortuneData.luckyIndex || Math.floor(Math.random() * 3) + 6,
  1220. description: fortuneData.description || '美食是最好的情感治愈师',
  1221. tips: fortuneData.tips || ['慢慢品味', '感受温暖'],
  1222. difficulty: fortuneData.difficulty || 'easy',
  1223. cookingTime: fortuneData.cookingTime || 25,
  1224. mysticalMessage: fortuneData.mysticalMessage || '让美食抚慰你的心灵,一切都会好起来的...',
  1225. ingredients: fortuneData.ingredients || [],
  1226. steps: fortuneData.steps || []
  1227. }
  1228. return fortune
  1229. } catch (error) {
  1230. console.error('生成心情料理失败:', error)
  1231. throw new Error('情感占卜师暂时感应不到你的心情,请稍后重试')
  1232. }
  1233. }
  1234. /**
  1235. * 生成缘分配菜
  1236. * @param params 双人参数
  1237. * @returns Promise<FortuneResult>
  1238. */
  1239. export const generateCoupleCooking = async (params: CoupleFortuneParams): Promise<FortuneResult> => {
  1240. try {
  1241. const prompt = `你是一位专门分析人际关系的料理占卜师,请分析两人的配菜缘分:
  1242. 第一人:
  1243. - 星座:${params.user1.zodiac}
  1244. - 生肖:${params.user1.animal}
  1245. - 性格:${params.user1.personality.join('、')}
  1246. 第二人:
  1247. - 星座:${params.user2.zodiac}
  1248. - 生肖:${params.user2.animal}
  1249. - 性格:${params.user2.personality.join('、')}
  1250. 请分析两人的配菜默契度,推荐适合合作制作的菜品。
  1251. 请按照以下JSON格式返回占卜结果:
  1252. {
  1253. "dishName": "适合合作的菜品名称",
  1254. "reason": "为什么这道菜适合你们一起做",
  1255. "luckyIndex": 8,
  1256. "description": "详细的缘分分析和配菜建议",
  1257. "tips": ["合作建议1", "默契提升技巧2"],
  1258. "difficulty": "medium",
  1259. "cookingTime": 40,
  1260. "mysticalMessage": "关于缘分和合作的神秘话语",
  1261. "ingredients": ["需要合作的食材1", "需要合作的食材2"],
  1262. "steps": ["合作步骤1", "合作步骤2"]
  1263. }`
  1264. const response = await aiClient.post('/chat/completions', {
  1265. model: AI_CONFIG.model,
  1266. messages: [
  1267. {
  1268. role: 'system',
  1269. content:
  1270. '你是一位精通人际关系和美食文化的占卜师,善于分析人与人之间的默契和缘分。你的话语充满智慧和温暖。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  1271. },
  1272. {
  1273. role: 'user',
  1274. content: prompt
  1275. }
  1276. ],
  1277. temperature: 0.8,
  1278. stream: false
  1279. })
  1280. const aiResponse = response.data.choices[0].message.content
  1281. let cleanResponse = aiResponse.trim()
  1282. if (cleanResponse.startsWith('```json')) {
  1283. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  1284. } else if (cleanResponse.startsWith('```')) {
  1285. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  1286. }
  1287. const fortuneData = JSON.parse(cleanResponse)
  1288. const fortune: FortuneResult = {
  1289. id: `couple-fortune-${Date.now()}`,
  1290. type: 'couple',
  1291. date: new Date().toISOString().split('T')[0],
  1292. dishName: fortuneData.dishName || '缘分料理',
  1293. reason: fortuneData.reason || '你们的星座组合很适合这道菜',
  1294. luckyIndex: fortuneData.luckyIndex || Math.floor(Math.random() * 3) + 7,
  1295. description: fortuneData.description || '这道菜将增进你们的默契',
  1296. tips: fortuneData.tips || ['互相配合', '享受过程'],
  1297. difficulty: fortuneData.difficulty || 'medium',
  1298. cookingTime: fortuneData.cookingTime || 40,
  1299. mysticalMessage: fortuneData.mysticalMessage || '缘分天注定,美食见真情...',
  1300. ingredients: fortuneData.ingredients || [],
  1301. steps: fortuneData.steps || []
  1302. }
  1303. return fortune
  1304. } catch (error) {
  1305. console.error('生成缘分配菜失败:', error)
  1306. throw new Error('缘分占卜师暂时无法感应到你们的默契,请稍后重试')
  1307. }
  1308. }
  1309. /**
  1310. * 生成幸运数字菜
  1311. * @param params 数字参数
  1312. * @returns Promise<FortuneResult>
  1313. */
  1314. export const generateNumberFortune = async (params: NumberFortuneParams): Promise<FortuneResult> => {
  1315. try {
  1316. const numberSource = params.isRandom ? '随机生成' : '用户选择'
  1317. const prompt = `你是一位精通数字占卜的料理大师,请根据幸运数字推荐菜品:
  1318. 幸运数字:${params.number}
  1319. 数字来源:${numberSource}
  1320. 请解析这个数字的寓意,并推荐对应的幸运菜品。
  1321. 请按照以下JSON格式返回占卜结果:
  1322. {
  1323. "dishName": "与数字相关的菜品名称",
  1324. "reason": "数字寓意和选择理由",
  1325. "luckyIndex": 9,
  1326. "description": "数字占卜解析和菜品象征意义",
  1327. "tips": ["数字相关的幸运建议1", "制作要点2"],
  1328. "difficulty": "medium",
  1329. "cookingTime": 35,
  1330. "mysticalMessage": "关于数字和命运的神秘话语",
  1331. "ingredients": ["象征性食材1", "象征性食材2"],
  1332. "steps": ["制作步骤1", "制作步骤2"]
  1333. }`
  1334. const response = await aiClient.post('/chat/completions', {
  1335. model: AI_CONFIG.model,
  1336. messages: [
  1337. {
  1338. role: 'system',
  1339. content:
  1340. '你是一位精通数字学和美食文化的神秘占卜师,善于解读数字的深层含义并与菜品联系。你的话语充满神秘和智慧。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
  1341. },
  1342. {
  1343. role: 'user',
  1344. content: prompt
  1345. }
  1346. ],
  1347. temperature: 0.8,
  1348. stream: false
  1349. })
  1350. const aiResponse = response.data.choices[0].message.content
  1351. let cleanResponse = aiResponse.trim()
  1352. if (cleanResponse.startsWith('```json')) {
  1353. cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
  1354. } else if (cleanResponse.startsWith('```')) {
  1355. cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
  1356. }
  1357. const fortuneData = JSON.parse(cleanResponse)
  1358. const fortune: FortuneResult = {
  1359. id: `number-fortune-${Date.now()}`,
  1360. type: 'number',
  1361. date: new Date().toISOString().split('T')[0],
  1362. dishName: fortuneData.dishName || '数字料理',
  1363. reason: fortuneData.reason || `数字${params.number}带来的神秘指引`,
  1364. luckyIndex: fortuneData.luckyIndex || Math.floor(Math.random() * 3) + 7,
  1365. description: fortuneData.description || '数字中蕴含着美食的秘密',
  1366. tips: fortuneData.tips || ['相信数字的力量', '用心制作'],
  1367. difficulty: fortuneData.difficulty || 'medium',
  1368. cookingTime: fortuneData.cookingTime || 35,
  1369. mysticalMessage: fortuneData.mysticalMessage || '数字是宇宙的语言,美食是心灵的慰藉...',
  1370. ingredients: fortuneData.ingredients || [],
  1371. steps: fortuneData.steps || []
  1372. }
  1373. return fortune
  1374. } catch (error) {
  1375. console.error('生成幸运数字菜失败:', error)
  1376. throw new Error('数字占卜师暂时无法解读这个数字,请稍后重试')
  1377. }
  1378. }
  1379. // 导出配置更新函数,供外部使用
  1380. export { AI_CONFIG }
  1381. /**
  1382. * 通用聊天(流式)
  1383. * @param messages 对话历史
  1384. * @param onDelta 每次增量文本回调
  1385. * @param onComplete 结束回调(携带完整文本)
  1386. * @param onError 错误回调
  1387. */
  1388. export const chatStream = async (
  1389. messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
  1390. onDelta: (deltaText: string) => void,
  1391. onComplete?: (fullText: string) => void,
  1392. onError?: (err: unknown) => void
  1393. ): Promise<void> => {
  1394. const url = AI_CONFIG.baseURL.replace(/\/$/, '') + '/chat/completions'
  1395. const headers: Record<string, string> = {
  1396. 'Content-Type': 'application/json',
  1397. Authorization: `Bearer ${AI_CONFIG.apiKey}`
  1398. }
  1399. const body = JSON.stringify({
  1400. model: AI_CONFIG.model,
  1401. messages,
  1402. temperature: AI_CONFIG.temperature,
  1403. stream: true
  1404. })
  1405. try {
  1406. const response = await fetch(url, {
  1407. method: 'POST',
  1408. headers,
  1409. body
  1410. })
  1411. if (!response.ok || !response.body) {
  1412. throw new Error(`请求失败: ${response.status}`)
  1413. }
  1414. const reader = response.body.getReader()
  1415. const decoder = new TextDecoder('utf-8')
  1416. let buffer = ''
  1417. let fullText = ''
  1418. while (true) {
  1419. const { value, done } = await reader.read()
  1420. if (done) break
  1421. buffer += decoder.decode(value, { stream: true })
  1422. // 以SSE事件为单位切分
  1423. const parts = buffer.split(/\n\n/)
  1424. buffer = parts.pop() || ''
  1425. for (const part of parts) {
  1426. const lines = part
  1427. .split('\n')
  1428. .map(l => l.trim())
  1429. .filter(Boolean)
  1430. for (const line of lines) {
  1431. if (!line.startsWith('data:')) continue
  1432. const data = line.slice(5).trim()
  1433. if (data === '[DONE]') {
  1434. if (onComplete) onComplete(fullText)
  1435. return
  1436. }
  1437. try {
  1438. const json = JSON.parse(data)
  1439. // 兼容OpenAI/同构流式规范
  1440. const delta = json.choices?.[0]?.delta?.content ?? json.choices?.[0]?.message?.content ?? ''
  1441. if (delta) {
  1442. fullText += delta
  1443. onDelta(delta)
  1444. }
  1445. } catch (e) {
  1446. // 非JSON行,忽略
  1447. continue
  1448. }
  1449. }
  1450. }
  1451. }
  1452. // 读流结束但未收到DONE
  1453. if (onComplete) onComplete(fullText)
  1454. } catch (err) {
  1455. if (onError) onError(err)
  1456. else console.error('chatStream error:', err)
  1457. throw err
  1458. }
  1459. }