| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622 |
- import axios from 'axios'
- import type {
- Recipe,
- CuisineType,
- NutritionAnalysis,
- WinePairing,
- SauceRecipe,
- SaucePreference,
- CustomSauceRequest,
- FortuneResult,
- DailyFortuneParams,
- MoodFortuneParams,
- CoupleFortuneParams,
- NumberFortuneParams
- } from '@/types'
- // AI服务配置 - 从环境变量读取(菜谱生成模型配置)
- const AI_CONFIG = {
- baseURL: import.meta.env.VITE_TEXT_GENERATION_BASE_URL || 'https://api.lingyiwanwu.com/v1/',
- apiKey: import.meta.env.VITE_TEXT_GENERATION_API_KEY,
- model: import.meta.env.VITE_TEXT_GENERATION_MODEL || 'yi-lightning',
- temperature: Number(import.meta.env.VITE_TEXT_GENERATION_TEMPERATURE) || 0.7,
- timeout: Number(import.meta.env.VITE_TEXT_GENERATION_TIMEOUT) || 300000
- }
- // 创建axios实例
- const aiClient = axios.create({
- baseURL: AI_CONFIG.baseURL,
- timeout: AI_CONFIG.timeout,
- headers: {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${AI_CONFIG.apiKey}`
- }
- })
- /**
- * 调用AI接口生成菜谱
- * @param ingredients 食材列表
- * @param cuisine 菜系信息
- * @param customPrompt 自定义提示词(可选)
- * @returns Promise<Recipe>
- */
- export const generateRecipe = async (ingredients: string[], cuisine: CuisineType, customPrompt?: string): Promise<Recipe> => {
- try {
- // 构建提示词
- let prompt = `${cuisine.prompt}
- 用户提供的食材:${ingredients.join('、')}`
- // 如果有自定义要求,添加到提示词中
- if (customPrompt) {
- prompt += `
- 用户的特殊要求:${customPrompt}`
- }
- prompt += `
- 请按照以下JSON格式返回菜谱,不包含营养分析和酒水搭配:
- {
- "name": "菜品名称",
- "ingredients": ["食材1", "食材2"],
- "steps": [
- {
- "step": 1,
- "description": "步骤描述",
- "time": 5,
- "temperature": "中火"
- }
- ],
- "cookingTime": 30,
- "difficulty": "medium",
- "tips": ["技巧1", "技巧2"]
- }`
- // 调用智谱AI接口
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content: '你是一位专业的厨师,请根据用户提供的食材和菜系要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: AI_CONFIG.temperature,
- stream: false
- })
- // 解析AI响应
- const aiResponse = response.data.choices[0].message.content
- // 清理响应内容,提取JSON部分
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const recipeData = JSON.parse(cleanResponse)
- // 构建完整的Recipe对象
- const recipe: Recipe = {
- id: `recipe-${cuisine.id}-${Date.now()}`,
- name: recipeData.name || `${cuisine.name}推荐菜品`,
- cuisine: cuisine.name,
- ingredients: recipeData.ingredients || ingredients,
- steps: recipeData.steps || [
- { step: 1, description: '准备所有食材', time: 5 },
- { step: 2, description: '按照传统做法烹饪', time: 20 }
- ],
- cookingTime: recipeData.cookingTime || 25,
- difficulty: recipeData.difficulty || 'medium',
- tips: recipeData.tips || ['注意火候控制', '调味要适中'],
- nutritionAnalysis: undefined,
- winePairing: undefined
- }
- return recipe
- } catch (error) {
- console.error(`生成${cuisine.name}菜谱失败:`, error)
- // 检查是否是400错误或其他特定错误
- if (error && typeof error === 'object' && 'response' in error) {
- const axiosError = error as any
- if (axiosError.response?.status === 400) {
- throw new Error(`${cuisine.name}表示这个食材搭配太有挑战性了`)
- }
- }
- // 抛出友好的错误信息
- throw new Error(`${cuisine.name}暂时学不会这道菜`)
- }
- }
- /**
- * 生成一桌菜的菜单
- * @param config 一桌菜配置
- * @returns Promise<DishInfo[]>
- */
- export const generateTableMenu = async (config: {
- dishCount: number
- flexibleCount: boolean
- tastes: string[]
- cuisineStyle: string
- diningScene: string
- nutritionFocus: string
- customRequirement: string
- customDishes: string[]
- }): Promise<
- Array<{
- name: string
- description: string
- category: string
- tags: string[]
- }>
- > => {
- try {
- // 构建提示词
- const tasteText = config.tastes.length > 0 ? config.tastes.join('、') : '适中'
- const sceneMap = {
- family: '家庭聚餐',
- friends: '朋友聚会',
- romantic: '浪漫晚餐',
- business: '商务宴请',
- festival: '节日庆祝',
- casual: '日常用餐'
- }
- const nutritionMap = {
- balanced: '营养均衡',
- protein: '高蛋白',
- vegetarian: '素食为主',
- low_fat: '低脂健康',
- comfort: '滋补养生'
- }
- const styleMap = {
- mixed: '混合菜系',
- chinese: '中式为主',
- western: '西式为主',
- japanese: '日式为主'
- }
- const styleText = (styleMap as Record<string, string>)[config.cuisineStyle] || '混合菜系'
- const sceneText = (sceneMap as Record<string, string>)[config.diningScene] || '家庭聚餐'
- const nutritionText = (nutritionMap as Record<string, string>)[config.nutritionFocus] || '营养均衡'
- let prompt = `请为我设计一桌菜,要求如下:
- - ${config.flexibleCount ? `参考菜品数量:${config.dishCount}道菜(可以根据实际情况智能调整,重点是搭配合理)` : `菜品数量:${config.dishCount}道菜(请严格按照这个数量生成)`}
- - 口味偏好:${tasteText}
- - 菜系风格:${styleText}
- - 用餐场景:${sceneText}
- - 营养搭配:${nutritionText}`
- if (config.customDishes.length > 0) {
- prompt += `\n- ${config.flexibleCount ? '优先考虑的菜品' : '必须包含的菜品'}:${config.customDishes.join('、')}${
- config.flexibleCount ? '(可以作为参考,根据搭配需要决定是否全部包含)' : '(请确保这些菜品都包含在菜单中)'
- }`
- }
- if (config.customRequirement) {
- prompt += `\n- 特殊要求:${config.customRequirement}`
- }
- if (config.flexibleCount) {
- prompt += `
- 智能搭配原则:
- 1. 菜品数量可以灵活调整(建议在${Math.max(2, config.dishCount - 2)}-${config.dishCount + 2}道之间),重点是搭配合理、营养均衡
- 2. 每道菜应该有独特的特色,避免食材和口味重复过多
- 3. 如果指定的菜品较少或重复度高,可以适当减少总菜品数量,确保每道菜都有特色
- 4. 优先考虑菜品的多样性和营养搭配,而不是强制凑够指定数量
- 5. 合理搭配不同类型的菜品:主菜、素菜、汤品、凉菜、主食等
- 6. 避免出现重复搭配,每道菜都应该有独特价值`
- } else {
- prompt += `
- 固定数量原则:
- 1. 严格按照${config.dishCount}道菜的数量生成菜单
- 2. 确保菜品搭配合理,营养均衡
- 3. 每道菜都有独特特色,尽量避免食材重复
- 4. 合理分配不同类型的菜品:主菜、素菜、汤品、凉菜、主食等`
- }
- prompt += `
- 请按照以下JSON格式返回菜单:
- {
- "dishes": [
- {
- "name": "菜品名称",
- "description": "菜品简介和特色描述",
- "category": "主菜/素菜/汤品/凉菜/主食/甜品",
- "tags": ["标签1", "标签2", "标签3"]
- }
- ]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content:
- '你是一位专业的菜单设计师,擅长根据不同场景和需求搭配合理的菜品组合。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答,包括菜名也要翻译成中文'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.8,
- stream: false
- })
- // 解析AI响应
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const menuData = JSON.parse(cleanResponse)
- return menuData.dishes || []
- } catch (error) {
- console.error('生成一桌菜菜单失败:', error)
- throw new Error('大厨表示这个菜单搭配太有挑战性了,请稍后重试')
- }
- }
- /**
- * 为一桌菜中的单个菜品生成详细菜谱
- * @param dishName 菜品名称
- * @param dishDescription 菜品描述
- * @param category 菜品分类
- * @returns Promise<Recipe>
- */
- export const generateDishRecipe = async (dishName: string, dishDescription: string, category: string): Promise<Recipe> => {
- try {
- const prompt = `请为以下菜品生成详细的菜谱:
- 菜品名称:${dishName}
- 菜品描述:${dishDescription}
- 菜品分类:${category}
- 请按照以下JSON格式返回菜谱:
- {
- "name": "菜品名称",
- "ingredients": ["食材1", "食材2"],
- "steps": [
- {
- "step": 1,
- "description": "步骤描述",
- "time": 5,
- "temperature": "中火"
- }
- ],
- "cookingTime": 30,
- "difficulty": "easy/medium/hard",
- "tips": ["技巧1", "技巧2"]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content: '你是一位专业的厨师,请根据菜品信息生成详细的制作菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.7,
- stream: false
- })
- // 解析AI响应
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const recipeData = JSON.parse(cleanResponse)
- const recipe: Recipe = {
- id: `dish-recipe-${Date.now()}`,
- name: recipeData.name || dishName,
- cuisine: category,
- ingredients: recipeData.ingredients || ['主要食材', '调料'],
- steps: recipeData.steps || [
- { step: 1, description: '准备食材', time: 5 },
- { step: 2, description: '开始制作', time: 15 }
- ],
- cookingTime: recipeData.cookingTime || 20,
- difficulty: recipeData.difficulty || 'medium',
- tips: recipeData.tips || ['注意火候', '调味适中']
- }
- return recipe
- } catch (error) {
- console.error('生成菜品菜谱失败:', error)
- throw new Error('大厨表示这道菜太有挑战性了,请稍后重试')
- }
- }
- // 使用自定义提示词生成菜谱
- export const generateCustomRecipe = async (ingredients: string[], customPrompt: string): Promise<Recipe> => {
- try {
- const prompt = `你是一位专业的厨师,请根据用户提供的食材和特殊要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。
- 用户提供的食材:${ingredients.join('、')}
- 用户的特殊要求:${customPrompt}
- 请按照以下JSON格式返回菜谱,不包含营养分析和酒水搭配:
- {
- "name": "菜品名称",
- "ingredients": ["食材1", "食材2"],
- "steps": [
- {
- "step": 1,
- "description": "步骤描述",
- "time": 5,
- "temperature": "中火"
- }
- ],
- "cookingTime": 30,
- "difficulty": "medium",
- "tips": ["技巧1", "技巧2"]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content: '你是一位专业的厨师,请根据用户提供的食材和特殊要求,生成详细的菜谱。请严格按照JSON格式返回,不要包含任何其他文字。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: AI_CONFIG.temperature,
- max_tokens: 2000,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const recipeData = JSON.parse(cleanResponse)
- const recipe: Recipe = {
- id: `recipe-custom-${Date.now()}`,
- name: recipeData.name || '自定义菜品',
- cuisine: '自定义',
- ingredients: recipeData.ingredients || ingredients,
- steps: recipeData.steps || [
- { step: 1, description: '准备所有食材', time: 5 },
- { step: 2, description: '按照要求烹饪', time: 20 }
- ],
- cookingTime: recipeData.cookingTime || 25,
- difficulty: recipeData.difficulty || 'medium',
- tips: recipeData.tips || ['根据个人口味调整', '注意火候控制'],
- nutritionAnalysis: undefined,
- winePairing: undefined
- }
- return recipe
- } catch (error) {
- console.error('生成自定义菜谱失败:', error)
- throw new Error('大厨表示这个自定义要求太有挑战性了,请稍后重试')
- }
- }
- // 流式生成多个菜系的菜谱
- export const generateMultipleRecipesStream = async (
- ingredients: string[],
- cuisines: CuisineType[],
- onRecipeGenerated: (recipe: Recipe, index: number, total: number) => void,
- onRecipeError?: (error: Error, index: number, cuisine: CuisineType, total: number) => void,
- customPrompt?: string
- ): Promise<void> => {
- const total = cuisines.length
- let completedCount = 0
- // 为了更好的用户体验,我们不并行生成,而是依次生成
- // 这样用户可以看到一个个菜谱依次完成的效果
- for (let index = 0; index < cuisines.length; index++) {
- const cuisine = cuisines[index]
- try {
- // 添加一些随机延迟,让生成过程更自然
- const delay = 1000 + Math.random() * 2000 // 1-3秒的随机延迟
- await new Promise(resolve => setTimeout(resolve, delay))
- const recipe = await generateRecipe(ingredients, cuisine, customPrompt)
- completedCount++
- onRecipeGenerated(recipe, index, total)
- } catch (error) {
- console.error(`生成${cuisine.name}菜谱失败:`, error)
- // 调用错误回调,让前端可以显示友好的错误信息
- if (onRecipeError) {
- const friendlyError = new Error(`${cuisine.name}不会这道菜,哈哈`)
- onRecipeError(friendlyError, index, cuisine, total)
- }
- // 即使某个菜系失败,也继续生成其他菜系
- continue
- }
- }
- if (completedCount === 0) {
- throw new Error('所有菜系生成都失败了,请稍后重试')
- }
- if (completedCount < total) {
- console.warn(`${total - completedCount}个菜系生成失败,但已成功生成${completedCount}个菜谱`)
- }
- }
- /**
- * 获取菜谱的营养分析
- * @param recipe 菜谱信息
- * @returns Promise<NutritionAnalysis>
- */
- export const getNutritionAnalysis = async (recipe: Recipe): Promise<NutritionAnalysis> => {
- try {
- const prompt = `请为以下菜谱生成详细的营养分析:
- 菜名:${recipe.name}
- 食材:${recipe.ingredients.join('、')}
- 烹饪方法:${recipe.steps.map(step => step.description).join(',')}
- 请按照以下JSON格式返回营养分析:
- {
- "nutrition": {
- "calories": 350,
- "protein": 25,
- "carbs": 45,
- "fat": 12,
- "fiber": 8,
- "sodium": 800,
- "sugar": 6,
- "vitaminC": 30,
- "calcium": 150,
- "iron": 3
- },
- "healthScore": 8,
- "balanceAdvice": ["建议搭配蔬菜沙拉增加维生素", "可适量减少盐分"],
- "dietaryTags": ["高蛋白", "低脂"],
- "servingSize": "1人份"
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content: '你是一位专业的营养师,请根据菜谱信息生成详细的营养分析。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答,包括菜名也要翻译成中文'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.5, // 使用更低的temperature以获得更准确的分析
- stream: false
- })
- // 解析AI响应
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const nutritionData = JSON.parse(cleanResponse)
- return nutritionData
- } catch (error) {
- console.error('获取营养分析失败:', error)
- return generateFallbackNutrition(recipe.ingredients)
- }
- }
- /**
- * 获取菜谱的酒水搭配建议
- * @param recipe 菜谱信息
- * @returns Promise<WinePairing>
- */
- export const getWinePairing = async (recipe: Recipe): Promise<WinePairing> => {
- try {
- const prompt = `请为以下菜谱推荐合适的酒水搭配:
- 菜名:${recipe.name}
- 菜系:${recipe.cuisine}
- 食材:${recipe.ingredients.join('、')}
- 请按照以下JSON格式返回酒水搭配建议:
- {
- "name": "推荐酒水名称",
- "type": "red_wine",
- "reason": "搭配理由说明",
- "servingTemperature": "16-18°C",
- "glassType": "波尔多杯",
- "alcoholContent": "13.5%",
- "flavor": "风味描述",
- "origin": "产地"
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content: '你是一位专业的侍酒师,请根据菜谱信息推荐合适的酒水搭配。请严格按照JSON格式返回,不要包含任何其他文字。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.7,
- stream: false
- })
- // 解析AI响应
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const wineData = JSON.parse(cleanResponse)
- return wineData
- } catch (error) {
- console.error('获取酒水搭配失败:', error)
- return generateFallbackWinePairing({ id: 'custom', name: recipe.cuisine } as CuisineType, recipe.ingredients)
- }
- }
- // 生成后备营养分析数据
- const generateFallbackNutrition = (ingredients: string[]): NutritionAnalysis => {
- const baseCalories = ingredients.length * 50 + Math.floor(Math.random() * 100) + 200
- const hasVegetables = ingredients.some(ing => ['菜', '瓜', '豆', '萝卜', '白菜', '菠菜', '西红柿', '黄瓜', '茄子', '土豆'].some(veg => ing.includes(veg)))
- const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
- const hasGrains = ingredients.some(ing => ['米', '面', '粉', '饭', '面条', '馒头'].some(grain => ing.includes(grain)))
- const dietaryTags: string[] = []
- if (hasVegetables && !hasMeat) dietaryTags.push('素食')
- if (hasMeat) dietaryTags.push('高蛋白')
- if (hasVegetables) dietaryTags.push('富含维生素')
- if (!hasGrains) dietaryTags.push('低碳水')
- if (baseCalories < 300) dietaryTags.push('低卡路里')
- const balanceAdvice: string[] = []
- if (!hasVegetables) balanceAdvice.push('建议搭配新鲜蔬菜增加维生素和膳食纤维')
- if (!hasMeat && !ingredients.some(ing => ['豆', '蛋', '奶'].some(protein => ing.includes(protein)))) {
- balanceAdvice.push('建议增加蛋白质来源,如豆类或蛋类')
- }
- if (hasGrains && hasMeat) balanceAdvice.push('营养搭配均衡,适合日常食用')
- if (ingredients.length > 5) balanceAdvice.push('食材丰富,营养全面')
- return {
- nutrition: {
- calories: baseCalories,
- protein: hasMeat ? 20 + Math.floor(Math.random() * 15) : 8 + Math.floor(Math.random() * 8),
- carbs: hasGrains ? 35 + Math.floor(Math.random() * 20) : 15 + Math.floor(Math.random() * 10),
- fat: hasMeat ? 12 + Math.floor(Math.random() * 8) : 5 + Math.floor(Math.random() * 5),
- fiber: hasVegetables ? 6 + Math.floor(Math.random() * 4) : 2 + Math.floor(Math.random() * 2),
- sodium: 600 + Math.floor(Math.random() * 400),
- sugar: 3 + Math.floor(Math.random() * 5),
- vitaminC: hasVegetables ? 20 + Math.floor(Math.random() * 30) : undefined,
- calcium: hasMeat || ingredients.some(ing => ['奶', '豆'].some(ca => ing.includes(ca))) ? 100 + Math.floor(Math.random() * 100) : undefined,
- iron: hasMeat ? 2 + Math.floor(Math.random() * 3) : undefined
- },
- healthScore: Math.floor(Math.random() * 3) + (hasVegetables ? 6 : 4) + (hasMeat ? 1 : 0),
- balanceAdvice: balanceAdvice.length > 0 ? balanceAdvice : ['营养搭配合理,可以放心享用'],
- dietaryTags: dietaryTags.length > 0 ? dietaryTags : ['家常菜'],
- servingSize: '1人份'
- }
- }
- // 生成后备酒水搭配数据
- const generateFallbackWinePairing = (cuisine: CuisineType, ingredients: string[]): WinePairing => {
- const hasSpicy = ingredients.some(ing => ['辣椒', '花椒', '胡椒', '姜', '蒜', '洋葱'].some(spice => ing.includes(spice)))
- const hasMeat = ingredients.some(ing => ['肉', '鸡', '鱼', '虾', '蛋', '牛', '猪', '羊'].some(meat => ing.includes(meat)))
- const hasSeafood = ingredients.some(ing => ['鱼', '虾', '蟹', '贝', '海带', '紫菜'].some(seafood => ing.includes(seafood)))
- const isLight = ingredients.some(ing => ['菜', '瓜', '豆腐', '蛋'].some(light => ing.includes(light)))
- const cuisineWineMap: Record<string, Partial<WinePairing>> = {
- 川菜大师: {
- name: '德国雷司令',
- type: 'white_wine',
- reason: '雷司令的甜度和酸度能很好地平衡川菜的麻辣,清洁口腔',
- servingTemperature: '8-10°C',
- glassType: '雷司令杯',
- alcoholContent: '11-12%',
- flavor: '清新果香,微甜带酸',
- origin: '德国莱茵河谷'
- },
- 粤菜大师: {
- name: '香槟',
- type: 'white_wine',
- reason: '香槟的气泡和酸度与粤菜的清淡鲜美完美搭配',
- servingTemperature: '6-8°C',
- glassType: '香槟杯',
- alcoholContent: '12%',
- flavor: '清爽气泡,柑橘香气',
- origin: '法国香槟区'
- },
- 日式料理大师: {
- name: '清酒',
- type: 'sake',
- reason: '清酒的清淡甘甜与日式料理的鲜美本味相得益彰',
- servingTemperature: '10-15°C',
- glassType: '清酒杯',
- alcoholContent: '15-16%',
- flavor: '清香甘甜,口感顺滑',
- origin: '日本'
- }
- }
- let winePairing = cuisineWineMap[cuisine.name] || {}
- if (!winePairing.name) {
- if (hasSpicy) {
- winePairing = {
- name: '德国雷司令',
- type: 'white_wine',
- reason: '甜白酒能很好地平衡辛辣食物,清洁口腔',
- servingTemperature: '8-10°C',
- glassType: '白酒杯',
- alcoholContent: '11-12%',
- flavor: '果香浓郁,微甜清新',
- origin: '德国'
- }
- } else if (hasSeafood) {
- winePairing = {
- name: '长相思白酒',
- type: 'white_wine',
- reason: '白酒的酸度和矿物质感与海鲜的鲜味完美搭配',
- servingTemperature: '8-10°C',
- glassType: '白酒杯',
- alcoholContent: '12-13%',
- flavor: '清新草本,柑橘香气',
- origin: '新西兰'
- }
- } else if (hasMeat && !isLight) {
- winePairing = {
- name: '赤霞珠红酒',
- type: 'red_wine',
- reason: '红酒的单宁与肉类的蛋白质结合,提升整体风味',
- servingTemperature: '16-18°C',
- glassType: '波尔多杯',
- alcoholContent: '13-14%',
- flavor: '浓郁果香,单宁丰富',
- origin: '法国波尔多'
- }
- } else {
- winePairing = {
- name: '绿茶',
- type: 'tea',
- reason: '绿茶的清香淡雅与清淡菜品相得益彰,有助消化',
- servingTemperature: '70-80°C',
- glassType: '茶杯',
- alcoholContent: '0%',
- flavor: '清香淡雅,回甘甜美',
- origin: '中国'
- }
- }
- }
- return {
- name: winePairing.name || '绿茶',
- type: winePairing.type || 'tea',
- reason: winePairing.reason || '经典搭配,有助消化',
- servingTemperature: winePairing.servingTemperature || '常温',
- glassType: winePairing.glassType,
- alcoholContent: winePairing.alcoholContent,
- flavor: winePairing.flavor || '清香怡人',
- origin: winePairing.origin
- }
- }
- /**
- * 测试AI服务连接
- * @returns Promise<boolean>
- */
- export const testAIConnection = async (): Promise<boolean> => {
- try {
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'user',
- content: '你好'
- }
- ],
- max_tokens: 10
- })
- return response.status === 200
- } catch (error) {
- console.error('AI服务连接测试失败:', error)
- return false
- }
- }
- /**
- * 根据菜名生成详细菜谱
- * @param dishName 菜品名称
- * @returns Promise<Recipe>
- */
- export const generateDishRecipeByName = async (dishName: string): Promise<Recipe> => {
- try {
- const prompt = `请为"${dishName}"这道菜生成详细的制作教程。
- 要求:
- 1. 提供完整的食材清单(包括主料和调料)
- 2. 详细的制作步骤,每个步骤要包含具体的时间和火候
- 3. 实用的烹饪技巧和注意事项
- 4. 如果是地方菜,请说明其特色和来源
- 请按照以下JSON格式返回菜谱:
- {
- "name": "菜品名称",
- "ingredients": ["主料1 200g", "调料1 适量", "调料2 1勺"],
- "steps": [
- {
- "step": 1,
- "description": "详细的步骤描述,包含具体操作方法",
- "time": 5,
- "temperature": "中火/大火/小火"
- }
- ],
- "cookingTime": 30,
- "difficulty": "easy/medium/hard",
- "tips": ["实用技巧1", "注意事项2", "口感调节3"]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content:
- '你是一位经验丰富的中华料理大师,精通各种菜系的制作方法。请根据用户提供的菜名,生成详细、实用的制作教程。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.7,
- stream: false
- })
- // 解析AI响应
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const recipeData = JSON.parse(cleanResponse)
- // 构建完整的Recipe对象
- const recipe: Recipe = {
- id: `dish-search-${Date.now()}`,
- name: recipeData.name || dishName,
- cuisine: '传统菜谱',
- ingredients: recipeData.ingredients || ['主要食材', '调料'],
- steps: recipeData.steps || [
- { step: 1, description: '准备所有食材', time: 5 },
- { step: 2, description: '按照传统方法制作', time: 20 }
- ],
- cookingTime: recipeData.cookingTime || 25,
- difficulty: recipeData.difficulty || 'medium',
- tips: recipeData.tips || ['注意火候控制', '调味要适中'],
- nutritionAnalysis: undefined,
- winePairing: undefined
- }
- return recipe
- } catch (error) {
- console.error(`生成"${dishName}"菜谱失败:`, error)
- throw new Error(`大厨表示"${dishName}"这道菜太有挑战性了,请稍后重试`)
- }
- }
- /**
- * 根据酱料名称生成详细制作方法
- * @param sauceName 酱料名称
- * @returns Promise<SauceRecipe>
- */
- export const generateSauceRecipe = async (sauceName: string): Promise<SauceRecipe> => {
- try {
- const prompt = `请为"${sauceName}"这种酱料生成详细的制作教程。
- 要求:
- 1. 提供完整的食材清单(主料、辅料、调料)
- 2. 详细的制作步骤,包含具体操作方法、时间、温度控制
- 3. 实用的制作技巧和注意事项
- 4. 保存方法和保质期信息
- 5. 推荐的搭配菜品
- 6. 口味特点描述
- 请按照以下JSON格式返回酱料配方:
- {
- "name": "酱料名称",
- "category": "spicy/garlic/sweet/complex/regional/fusion",
- "ingredients": ["主料1 200g", "调料1 适量", "调料2 1勺"],
- "steps": [
- {
- "step": 1,
- "description": "详细的步骤描述",
- "time": 5,
- "temperature": "中火",
- "technique": "炒制"
- }
- ],
- "makingTime": 30,
- "difficulty": "easy/medium/hard",
- "tips": ["制作技巧1", "注意事项2"],
- "storage": {
- "method": "密封冷藏",
- "duration": "1个月",
- "temperature": "4°C"
- },
- "pairings": ["搭配菜品1", "搭配菜品2"],
- "tags": ["标签1", "标签2"],
- "spiceLevel": 3,
- "sweetLevel": 1,
- "saltLevel": 4,
- "sourLevel": 2,
- "description": "酱料特色描述"
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content:
- '你是一位专业的酱料制作大师,精通各种传统和创新酱料的制作方法。请根据用户提供的酱料名称,生成详细、实用的制作教程。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.7,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const sauceData = JSON.parse(cleanResponse)
- const sauceRecipe: SauceRecipe = {
- id: `sauce-${Date.now()}`,
- name: sauceData.name || sauceName,
- category: sauceData.category || 'complex',
- ingredients: sauceData.ingredients || ['主要食材', '调料'],
- steps: sauceData.steps || [
- { step: 1, description: '准备所有食材', time: 5 },
- { step: 2, description: '按照传统方法制作', time: 20 }
- ],
- makingTime: sauceData.makingTime || 25,
- difficulty: sauceData.difficulty || 'medium',
- tips: sauceData.tips || ['注意火候控制', '调味要适中'],
- storage: sauceData.storage || {
- method: '密封保存',
- duration: '1周',
- temperature: '常温'
- },
- pairings: sauceData.pairings || ['面条', '蔬菜'],
- tags: sauceData.tags || ['家常', '经典'],
- spiceLevel: sauceData.spiceLevel || 2,
- sweetLevel: sauceData.sweetLevel || 2,
- saltLevel: sauceData.saltLevel || 3,
- sourLevel: sauceData.sourLevel || 2,
- description: sauceData.description || '经典酱料配方'
- }
- return sauceRecipe
- } catch (error) {
- console.error(`生成"${sauceName}"酱料配方失败:`, error)
- throw new Error(`酱料大师表示"${sauceName}"这个配方太有挑战性了,请稍后重试`)
- }
- }
- /**
- * 根据用户偏好推荐酱料
- * @param preferences 用户偏好配置
- * @returns Promise<string[]>
- */
- export const recommendSauces = async (preferences: SaucePreference): Promise<string[]> => {
- try {
- const useCaseMap = {
- noodles: '拌面',
- dipping: '蘸菜',
- cooking: '炒菜',
- bbq: '烧烤',
- hotpot: '火锅'
- }
- const useCaseText = preferences.useCase.map(uc => (useCaseMap as Record<string, string>)[uc] || uc).join('、')
- const ingredientsText = preferences.availableIngredients.length > 0 ? preferences.availableIngredients.join('、') : '无特殊要求'
- const prompt = `请根据以下用户偏好推荐合适的酱料:
- 用户偏好:
- - 辣度偏好:${preferences.spiceLevel}/5
- - 甜度偏好:${preferences.sweetLevel}/5
- - 咸度偏好:${preferences.saltLevel}/5
- - 酸度偏好:${preferences.sourLevel}/5
- - 使用场景:${useCaseText}
- - 现有食材:${ingredientsText}
- 请推荐5-8种最适合的酱料,按匹配度排序。
- 请按照以下JSON格式返回推荐结果:
- {
- "recommendations": [
- "酱料名称1",
- "酱料名称2",
- "酱料名称3"
- ]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content: '你是一位专业的酱料推荐专家,能够根据用户的口味偏好和使用场景推荐最合适的酱料。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.8,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const recommendationData = JSON.parse(cleanResponse)
- return recommendationData.recommendations || []
- } catch (error) {
- console.error('获取酱料推荐失败:', error)
- throw new Error('酱料大师暂时想不出好的推荐,请稍后重试')
- }
- }
- /**
- * 创建自定义酱料配方
- * @param request 自定义酱料创作请求
- * @returns Promise<SauceRecipe>
- */
- export const createCustomSauce = async (request: CustomSauceRequest): Promise<SauceRecipe> => {
- try {
- const baseTypeMap = {
- oil: '油性酱料',
- water: '水性酱料',
- paste: '膏状酱料',
- granular: '颗粒状酱料'
- }
- const flavorMap = {
- spicy: '辣味',
- sweet: '甜味',
- sour: '酸味',
- umami: '鲜味',
- aromatic: '香味'
- }
- const prompt = `请根据以下要求创作一个独特的酱料配方:
- 创作要求:
- - 基础类型:${baseTypeMap[request.baseType]}
- - 主要风味:${flavorMap[request.flavorDirection]}
- - 特殊食材:${request.specialIngredients.join('、')}
- - 期望口感:${request.expectedTexture}
- - 用途:${request.intendedUse}
- ${request.customRequirements ? `- 特殊要求:${request.customRequirements}` : ''}
- 请创作一个创新的酱料配方,要有独特性和实用性。
- 请按照以下JSON格式返回酱料配方:
- {
- "name": "创新酱料名称",
- "category": "fusion",
- "ingredients": ["主料1 200g", "调料1 适量"],
- "steps": [
- {
- "step": 1,
- "description": "详细步骤描述",
- "time": 5,
- "temperature": "中火",
- "technique": "制作技法"
- }
- ],
- "makingTime": 30,
- "difficulty": "medium",
- "tips": ["创作技巧1", "注意事项2"],
- "storage": {
- "method": "保存方法",
- "duration": "保质期",
- "temperature": "保存温度"
- },
- "pairings": ["搭配建议1", "搭配建议2"],
- "tags": ["创新", "自制"],
- "spiceLevel": 3,
- "sweetLevel": 2,
- "saltLevel": 3,
- "sourLevel": 2,
- "description": "创新酱料特色描述"
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content: '你是一位富有创意的酱料创作大师,擅长根据用户需求创作独特的酱料配方。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.9,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const sauceData = JSON.parse(cleanResponse)
- const customSauce: SauceRecipe = {
- id: `custom-sauce-${Date.now()}`,
- name: sauceData.name || '自创酱料',
- category: 'fusion',
- ingredients: sauceData.ingredients || ['创新食材'],
- steps: sauceData.steps || [
- { step: 1, description: '准备创新食材', time: 5 },
- { step: 2, description: '按照创意方法制作', time: 20 }
- ],
- makingTime: sauceData.makingTime || 25,
- difficulty: sauceData.difficulty || 'medium',
- tips: sauceData.tips || ['发挥创意', '调整口味'],
- storage: sauceData.storage || {
- method: '密封保存',
- duration: '1周',
- temperature: '冷藏'
- },
- pairings: sauceData.pairings || ['创意搭配'],
- tags: sauceData.tags || ['创新', '自制'],
- spiceLevel: sauceData.spiceLevel || 2,
- sweetLevel: sauceData.sweetLevel || 2,
- saltLevel: sauceData.saltLevel || 3,
- sourLevel: sauceData.sourLevel || 2,
- description: sauceData.description || '独特的自创酱料'
- }
- return customSauce
- } catch (error) {
- console.error('创建自定义酱料失败:', error)
- throw new Error('酱料大师表示这个自定义配方太有挑战性了,请稍后重试')
- }
- }
- /**
- * 获取酱料搭配建议
- * @param sauceName 酱料名称
- * @returns Promise<string[]>
- */
- export const getSaucePairings = async (sauceName: string): Promise<string[]> => {
- try {
- const prompt = `请为"${sauceName}"这种酱料推荐最佳的搭配菜品和使用方法。
- 要求:
- 1. 推荐5-8种最适合的搭配菜品
- 2. 说明具体的使用方法
- 3. 考虑不同的烹饪场景
- 请按照以下JSON格式返回搭配建议:
- {
- "pairings": [
- "搭配菜品1 - 使用方法",
- "搭配菜品2 - 使用方法",
- "搭配菜品3 - 使用方法"
- ]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content: '你是一位专业的美食搭配专家,精通各种酱料与菜品的最佳搭配方法。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.7,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const pairingData = JSON.parse(cleanResponse)
- return pairingData.pairings || []
- } catch (error) {
- console.error('获取酱料搭配建议失败:', error)
- return ['面条 - 拌面使用', '蔬菜 - 蘸食使用', '肉类 - 调味使用']
- }
- }
- /**
- * 生成今日运势菜
- * @param params 运势参数
- * @returns Promise<FortuneResult>
- */
- export const generateDailyFortune = async (params: DailyFortuneParams): Promise<FortuneResult> => {
- try {
- const prompt = `你是一位神秘的料理占卜师,请根据以下信息为用户推荐今日幸运菜:
- 星座:${params.zodiac}
- 生肖:${params.animal}
- 日期:${params.date}
- 请结合星座特性、生肖属性和今日能量,推荐一道能带来好运的菜品。
- 请按照以下JSON格式返回占卜结果:
- {
- "dishName": "菜品名称",
- "reason": "选择这道菜的占卜理由",
- "luckyIndex": 8,
- "description": "详细的占卜解析和菜品介绍",
- "tips": ["烹饪技巧1", "幸运提示2"],
- "difficulty": "medium",
- "cookingTime": 30,
- "mysticalMessage": "神秘的占卜师话语",
- "ingredients": ["主要食材1", "主要食材2"],
- "steps": ["制作步骤1", "制作步骤2"]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content:
- '你是一位神秘而智慧的料理占卜师,精通星座学、生肖学和美食文化。你的话语充满神秘色彩,善于将占卜元素与美食完美结合。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.8,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const fortuneData = JSON.parse(cleanResponse)
- const fortune: FortuneResult = {
- id: `daily-fortune-${Date.now()}`,
- type: 'daily',
- date: params.date,
- dishName: fortuneData.dishName || '幸运料理',
- reason: fortuneData.reason || '星座与生肖的神秘指引',
- luckyIndex: fortuneData.luckyIndex || Math.floor(Math.random() * 3) + 7,
- description: fortuneData.description || '这道菜将为您带来今日好运',
- tips: fortuneData.tips || ['用心制作', '保持好心情'],
- difficulty: fortuneData.difficulty || 'medium',
- cookingTime: fortuneData.cookingTime || 30,
- mysticalMessage: fortuneData.mysticalMessage || '命运之轮正在转动,美味即将降临...',
- ingredients: fortuneData.ingredients || [],
- steps: fortuneData.steps || []
- }
- return fortune
- } catch (error) {
- console.error('生成今日运势菜失败:', error)
- throw new Error('占卜师暂时无法感应到星象,请稍后重试')
- }
- }
- /**
- * 生成心情料理
- * @param params 心情参数
- * @returns Promise<FortuneResult>
- */
- export const generateMoodCooking = async (params: MoodFortuneParams): Promise<FortuneResult> => {
- try {
- const moodText = params.moods.join('、')
- const intensityText = ['很轻微', '轻微', '一般', '强烈', '非常强烈'][params.intensity - 1]
- const prompt = `你是一位擅长情感治愈的料理占卜师,请根据以下心情状态推荐治愈菜品:
- 当前心情:${moodText}
- 情绪强度:${intensityText}
- 请推荐一道能够治愈这种心情的菜品,并给出温暖的情感分析。
- 请按照以下JSON格式返回占卜结果:
- {
- "dishName": "菜品名称",
- "reason": "这道菜如何治愈当前心情",
- "luckyIndex": 7,
- "description": "详细的情感分析和菜品治愈功效",
- "tips": ["情感治愈建议1", "烹饪心得2"],
- "difficulty": "easy",
- "cookingTime": 25,
- "mysticalMessage": "温暖治愈的话语",
- "ingredients": ["治愈食材1", "治愈食材2"],
- "steps": ["治愈步骤1", "治愈步骤2"]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content:
- '你是一位温暖而智慧的情感治愈师,深谙美食与情感的关系。你善于通过菜品来抚慰人心,话语温暖治愈。请严格按照JSON���式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.7,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const fortuneData = JSON.parse(cleanResponse)
- const fortune: FortuneResult = {
- id: `mood-fortune-${Date.now()}`,
- type: 'mood',
- date: new Date().toISOString().split('T')[0],
- dishName: fortuneData.dishName || '治愈料理',
- reason: fortuneData.reason || '这道菜能温暖你的心',
- luckyIndex: fortuneData.luckyIndex || Math.floor(Math.random() * 3) + 6,
- description: fortuneData.description || '美食是最好的情感治愈师',
- tips: fortuneData.tips || ['慢慢品味', '感受温暖'],
- difficulty: fortuneData.difficulty || 'easy',
- cookingTime: fortuneData.cookingTime || 25,
- mysticalMessage: fortuneData.mysticalMessage || '让美食抚慰你的心灵,一切都会好起来的...',
- ingredients: fortuneData.ingredients || [],
- steps: fortuneData.steps || []
- }
- return fortune
- } catch (error) {
- console.error('生成心情料理失败:', error)
- throw new Error('情感占卜师暂时感应不到你的心情,请稍后重试')
- }
- }
- /**
- * 生成缘分配菜
- * @param params 双人参数
- * @returns Promise<FortuneResult>
- */
- export const generateCoupleCooking = async (params: CoupleFortuneParams): Promise<FortuneResult> => {
- try {
- const prompt = `你是一位专门分析人际关系的料理占卜师,请分析两人的配菜缘分:
- 第一人:
- - 星座:${params.user1.zodiac}
- - 生肖:${params.user1.animal}
- - 性格:${params.user1.personality.join('、')}
- 第二人:
- - 星座:${params.user2.zodiac}
- - 生肖:${params.user2.animal}
- - 性格:${params.user2.personality.join('、')}
- 请分析两人的配菜默契度,推荐适合合作制作的菜品。
- 请按照以下JSON格式返回占卜结果:
- {
- "dishName": "适合合作的菜品名称",
- "reason": "为什么这道菜适合你们一起做",
- "luckyIndex": 8,
- "description": "详细的缘分分析和配菜建议",
- "tips": ["合作建议1", "默契提升技巧2"],
- "difficulty": "medium",
- "cookingTime": 40,
- "mysticalMessage": "关于缘分和合作的神秘话语",
- "ingredients": ["需要合作的食材1", "需要合作的食材2"],
- "steps": ["合作步骤1", "合作步骤2"]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content:
- '你是一位精通人际关系和美食文化的占卜师,善于分析人与人之间的默契和缘分。你的话语充满智慧和温暖。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.8,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const fortuneData = JSON.parse(cleanResponse)
- const fortune: FortuneResult = {
- id: `couple-fortune-${Date.now()}`,
- type: 'couple',
- date: new Date().toISOString().split('T')[0],
- dishName: fortuneData.dishName || '缘分料理',
- reason: fortuneData.reason || '你们的星座组合很适合这道菜',
- luckyIndex: fortuneData.luckyIndex || Math.floor(Math.random() * 3) + 7,
- description: fortuneData.description || '这道菜将增进你们的默契',
- tips: fortuneData.tips || ['互相配合', '享受过程'],
- difficulty: fortuneData.difficulty || 'medium',
- cookingTime: fortuneData.cookingTime || 40,
- mysticalMessage: fortuneData.mysticalMessage || '缘分天注定,美食见真情...',
- ingredients: fortuneData.ingredients || [],
- steps: fortuneData.steps || []
- }
- return fortune
- } catch (error) {
- console.error('生成缘分配菜失败:', error)
- throw new Error('缘分占卜师暂时无法感应到你们的默契,请稍后重试')
- }
- }
- /**
- * 生成幸运数字菜
- * @param params 数字参数
- * @returns Promise<FortuneResult>
- */
- export const generateNumberFortune = async (params: NumberFortuneParams): Promise<FortuneResult> => {
- try {
- const numberSource = params.isRandom ? '随机生成' : '用户选择'
- const prompt = `你是一位精通数字占卜的料理大师,请根据幸运数字推荐菜品:
- 幸运数字:${params.number}
- 数字来源:${numberSource}
- 请解析这个数字的寓意,并推荐对应的幸运菜品。
- 请按照以下JSON格式返回占卜结果:
- {
- "dishName": "与数字相关的菜品名称",
- "reason": "数字寓意和选择理由",
- "luckyIndex": 9,
- "description": "数字占卜解析和菜品象征意义",
- "tips": ["数字相关的幸运建议1", "制作要点2"],
- "difficulty": "medium",
- "cookingTime": 35,
- "mysticalMessage": "关于数字和命运的神秘话语",
- "ingredients": ["象征性食材1", "象征性食材2"],
- "steps": ["制作步骤1", "制作步骤2"]
- }`
- const response = await aiClient.post('/chat/completions', {
- model: AI_CONFIG.model,
- messages: [
- {
- role: 'system',
- content:
- '你是一位精通数字学和美食文化的神秘占卜师,善于解读数字的深层含义并与菜品联系。你的话语充满神秘和智慧。请严格按照JSON格式返回,不要包含任何其他文字。请务必用中文回答。'
- },
- {
- role: 'user',
- content: prompt
- }
- ],
- temperature: 0.8,
- stream: false
- })
- const aiResponse = response.data.choices[0].message.content
- let cleanResponse = aiResponse.trim()
- if (cleanResponse.startsWith('```json')) {
- cleanResponse = cleanResponse.replace(/```json\s*/, '').replace(/```\s*$/, '')
- } else if (cleanResponse.startsWith('```')) {
- cleanResponse = cleanResponse.replace(/```\s*/, '').replace(/```\s*$/, '')
- }
- const fortuneData = JSON.parse(cleanResponse)
- const fortune: FortuneResult = {
- id: `number-fortune-${Date.now()}`,
- type: 'number',
- date: new Date().toISOString().split('T')[0],
- dishName: fortuneData.dishName || '数字料理',
- reason: fortuneData.reason || `数字${params.number}带来的神秘指引`,
- luckyIndex: fortuneData.luckyIndex || Math.floor(Math.random() * 3) + 7,
- description: fortuneData.description || '数字中蕴含着美食的秘密',
- tips: fortuneData.tips || ['相信数字的力量', '用心制作'],
- difficulty: fortuneData.difficulty || 'medium',
- cookingTime: fortuneData.cookingTime || 35,
- mysticalMessage: fortuneData.mysticalMessage || '数字是宇宙的语言,美食是心灵的慰藉...',
- ingredients: fortuneData.ingredients || [],
- steps: fortuneData.steps || []
- }
- return fortune
- } catch (error) {
- console.error('生成幸运数字菜失败:', error)
- throw new Error('数字占卜师暂时无法解读这个数字,请稍后重试')
- }
- }
- // 导出配置更新函数,供外部使用
- export { AI_CONFIG }
- /**
- * 通用聊天(流式)
- * @param messages 对话历史
- * @param onDelta 每次增量文本回调
- * @param onComplete 结束回调(携带完整文本)
- * @param onError 错误回调
- */
- export const chatStream = async (
- messages: Array<{ role: 'system' | 'user' | 'assistant'; content: string }>,
- onDelta: (deltaText: string) => void,
- onComplete?: (fullText: string) => void,
- onError?: (err: unknown) => void
- ): Promise<void> => {
- const url = AI_CONFIG.baseURL.replace(/\/$/, '') + '/chat/completions'
- const headers: Record<string, string> = {
- 'Content-Type': 'application/json',
- Authorization: `Bearer ${AI_CONFIG.apiKey}`
- }
- const body = JSON.stringify({
- model: AI_CONFIG.model,
- messages,
- temperature: AI_CONFIG.temperature,
- stream: true
- })
- try {
- const response = await fetch(url, {
- method: 'POST',
- headers,
- body
- })
- if (!response.ok || !response.body) {
- throw new Error(`请求失败: ${response.status}`)
- }
- const reader = response.body.getReader()
- const decoder = new TextDecoder('utf-8')
- let buffer = ''
- let fullText = ''
- while (true) {
- const { value, done } = await reader.read()
- if (done) break
- buffer += decoder.decode(value, { stream: true })
- // 以SSE事件为单位切分
- const parts = buffer.split(/\n\n/)
- buffer = parts.pop() || ''
- for (const part of parts) {
- const lines = part
- .split('\n')
- .map(l => l.trim())
- .filter(Boolean)
- for (const line of lines) {
- if (!line.startsWith('data:')) continue
- const data = line.slice(5).trim()
- if (data === '[DONE]') {
- if (onComplete) onComplete(fullText)
- return
- }
- try {
- const json = JSON.parse(data)
- // 兼容OpenAI/同构流式规范
- const delta = json.choices?.[0]?.delta?.content ?? json.choices?.[0]?.message?.content ?? ''
- if (delta) {
- fullText += delta
- onDelta(delta)
- }
- } catch (e) {
- // 非JSON行,忽略
- continue
- }
- }
- }
- }
- // 读流结束但未收到DONE
- if (onComplete) onComplete(fullText)
- } catch (err) {
- if (onError) onError(err)
- else console.error('chatStream error:', err)
- throw err
- }
- }
|