aiService.ts 66 KB

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