import { ref, computed } from 'vue' import { requestAiStream, parsePartialDietPlainText } from '../utils/aiStreamRequest.js' const DIET_AI_PREFILL_KEY = 'tongji_diet_ai_prefill' const emptyDietAiRecommend = () => ({ breakfast: '', lunch: '', dinner: '', tips: '', avoid: [], analysis: [], exercise: null, disclaimer: '', rules_summary: '', source: '', date: '' }) /** AI 饮食助手(今日推荐 + 能不能吃) */ export function useDietAi(proxy, { diagnosisId, showUserToast, formatUserMessage }) { const dietAiRecommend = ref(emptyDietAiRecommend()) const dietAiLoading = ref(false) const dietAiStreaming = ref(false) const dietAiReplacing = ref(false) const dietAiStreamField = ref('') let dietStreamBuffer = '' const dietAiAskText = ref('') const dietAiAsking = ref(false) const dietAiAskResult = ref({ advice: '', level: '', level_label: '', portion: '', food: '' }) const hasDietAiMealContent = computed(() => { const r = dietAiRecommend.value return !!(r.breakfast || r.lunch || r.dinner || r.tips) }) function detectDietStreamField(text) { const tail = String(text || '').match(/(?:^|\n)(早餐|午餐|晚餐|提示|少碰)[::]\s*([^\n]*)$/) if (!tail) return 'breakfast' const map = { '早餐': 'breakfast', '午餐': 'lunch', '晚餐': 'dinner', '提示': 'tips', '少碰': 'tips' } return map[tail[1]] || 'breakfast' } function mergePartialDietFromStream() { const partial = parsePartialDietPlainText(dietStreamBuffer) const next = { ...dietAiRecommend.value } for (const key of ['breakfast', 'lunch', 'dinner', 'tips']) { if (partial[key] != null) next[key] = partial[key] } if (Array.isArray(partial.avoid)) next.avoid = partial.avoid dietAiRecommend.value = next dietAiStreamField.value = detectDietStreamField(dietStreamBuffer) } function applyDietRecommendPayload(payload) { if (!payload) return const prevAnalysis = dietAiRecommend.value.analysis const prevExercise = dietAiRecommend.value.exercise dietAiRecommend.value = { ...emptyDietAiRecommend(), ...payload, avoid: Array.isArray(payload.avoid) ? payload.avoid : [], analysis: Array.isArray(payload.analysis) && payload.analysis.length ? payload.analysis : (Array.isArray(prevAnalysis) ? prevAnalysis : []), exercise: (payload.exercise && typeof payload.exercise === 'object') ? payload.exercise : (prevExercise || null) } } function resolveDietDonePayload(payload) { if (!payload) return payload if (payload.source !== 'rule' || !dietStreamBuffer) return payload const streamed = parsePartialDietPlainText(dietStreamBuffer) if (!streamed.breakfast) return payload return { ...emptyDietAiRecommend(), ...payload, breakfast: streamed.breakfast, lunch: streamed.lunch || payload.lunch || '', dinner: streamed.dinner || payload.dinner || '', tips: streamed.tips || payload.tips || '', avoid: (Array.isArray(streamed.avoid) && streamed.avoid.length) ? streamed.avoid : (payload.avoid || []), source: 'ai', disclaimer: 'AI 建议供参考,仍请结合医嘱与血糖监测。' } } function onDietAiStreamEvent(event, payload) { if (event === 'analysis') { if (Array.isArray(payload?.analysis)) { dietAiRecommend.value = { ...dietAiRecommend.value, analysis: payload.analysis } } } else if (event === 'exercise') { if (payload?.exercise && typeof payload.exercise === 'object') { dietAiRecommend.value = { ...dietAiRecommend.value, exercise: payload.exercise } } } else if (event === 'delta') { dietAiStreaming.value = true if (payload.buffer != null) { dietStreamBuffer = payload.buffer } else { dietStreamBuffer += payload.text || '' } mergePartialDietFromStream() } else if (event === 'done') { applyDietRecommendPayload(resolveDietDonePayload(payload)) dietStreamBuffer = '' dietAiStreamField.value = '' } else if (event === 'error') { showUserToast(formatUserMessage(payload?.message, '饮食建议加载失败')) } } async function fetchDietAiRecommendFallback(refresh) { const res = await proxy.apiUrl({ url: '/api/tcm/dailyDietAiRecommend', method: 'GET', timeout: 35000, data: { diagnosis_id: diagnosisId.value, refresh: refresh ? 1 : 0, ...(refresh ? { _t: Date.now() } : {}) } }, false) if (res?.code === 1 && res.data) { applyDietRecommendPayload(res.data) } else if (res?.msg) { showUserToast(res.msg) } } async function fetchDietAiRecommend(refresh = false) { if (!diagnosisId.value || dietAiLoading.value) return dietAiLoading.value = true dietAiStreaming.value = false dietAiReplacing.value = !!refresh dietStreamBuffer = '' dietAiStreamField.value = '' if (refresh) { applyDietRecommendPayload(emptyDietAiRecommend()) } try { await requestAiStream({ baseUrl: proxy.$url, url: '/api/tcm/dailyDietAiRecommendStream', method: 'GET', data: { diagnosis_id: diagnosisId.value, refresh: refresh ? 1 : 0, ...(refresh ? { _t: Date.now() } : {}) }, onEvent: onDietAiStreamEvent, fallback: () => fetchDietAiRecommendFallback(refresh) }) } catch (e) { try { await fetchDietAiRecommendFallback(refresh) } catch (e2) { showUserToast('饮食建议加载失败') } } finally { dietAiLoading.value = false dietAiStreaming.value = false dietAiReplacing.value = false dietAiStreamField.value = '' } } function refreshDietAiRecommend() { if (dietAiLoading.value) return fetchDietAiRecommend(true) } async function askDietAiFoodFallback(q) { const res = await proxy.apiUrl({ url: '/api/tcm/dailyDietAiAsk', method: 'POST', timeout: 35000, data: { diagnosis_id: diagnosisId.value, question: q } }, false) if (res?.code === 1 && res.data) { dietAiAskResult.value = { advice: res.data.advice || '', level: res.data.level || '', level_label: res.data.level_label || '', portion: res.data.portion || '', food: res.data.food || q } } else { showUserToast(formatUserMessage(res?.msg, '咨询失败')) } } async function askDietAiFood() { const q = String(dietAiAskText.value || '').trim() if (!q) { showUserToast('请输入想咨询的食物') return } if (!diagnosisId.value) return dietAiAsking.value = true dietAiAskResult.value = { advice: '', level: '', level_label: '', portion: '', food: '' } try { await requestAiStream({ baseUrl: proxy.$url, url: '/api/tcm/dailyDietAiAskStream', method: 'POST', data: { diagnosis_id: diagnosisId.value, question: q }, onEvent: (event, payload) => { if (event === 'start' && !dietAiAskResult.value.advice) { dietAiAskResult.value = { ...dietAiAskResult.value, advice: payload.message || '正在想…' } } else if (event === 'delta') { dietAiAskResult.value = { ...dietAiAskResult.value, advice: payload.advice || dietAiAskResult.value.advice } } else if (event === 'done') { dietAiAskResult.value = { advice: payload.advice || '', level: payload.level || '', level_label: payload.level_label || '', portion: payload.portion || '', food: payload.food || q } } else if (event === 'error') { showUserToast(formatUserMessage(payload?.message, '咨询失败')) } }, fallback: () => askDietAiFoodFallback(q) }) } catch (e) { try { await askDietAiFoodFallback(q) } catch (e2) { showUserToast('网络异常,请稍后再试') } } finally { dietAiAsking.value = false } } function onDietAiAskInput(e) { dietAiAskText.value = e?.detail?.value ?? '' } function saveDietPrefillToStorage() { const r = dietAiRecommend.value if (!r.breakfast) return try { uni.setStorageSync(DIET_AI_PREFILL_KEY, { breakfast: r.breakfast, lunch: r.lunch, dinner: r.dinner, tips: r.tips || '' }) } catch (e) {} } function goMoreDietForm(withPrefill = false) { if (withPrefill) saveDietPrefillToStorage() uni.navigateTo({ url: '/tongji/pages/more?openDiet=1' }) } async function applyDietAiToForm() { if (!dietAiRecommend.value.breakfast) { await fetchDietAiRecommend(false) } goMoreDietForm(true) } function openDietForm() { goMoreDietForm(false) } return { dietAiRecommend, dietAiLoading, dietAiStreaming, dietAiReplacing, dietAiStreamField, dietAiAskText, dietAiAsking, dietAiAskResult, hasDietAiMealContent, fetchDietAiRecommend, refreshDietAiRecommend, askDietAiFood, onDietAiAskInput, applyDietAiToForm, openDietForm } } /** more 页打开饮食表单时读取并清除预填 */ export function consumeDietAiPrefill() { try { const data = uni.getStorageSync(DIET_AI_PREFILL_KEY) uni.removeStorageSync(DIET_AI_PREFILL_KEY) if (data && typeof data === 'object' && data.breakfast) return data } catch (e) {} return null }