false, 'error' => '缺少就诊卡']; } $context = self::buildCareContext($diagnosisId); if (!$context['ok']) { return ['ok' => false, 'error' => $context['error'] ?? '获取档案失败']; } $gate = self::ensureEnoughBloodDays($context['data']); if (!$gate['ok']) { return ['ok' => false, 'error' => $gate['error']]; } $analysis = self::buildAnalysisPayload($context['data']); $cacheKey = self::careCacheKey($diagnosisId); if (!$refresh) { $cached = Cache::get($cacheKey); if (is_array($cached) && !empty($cached['summary'])) { $cached['cached'] = true; $cached['analysis'] = $analysis; return ['ok' => true, 'data' => $cached]; } } $data = null; if (AiChatService::isEnabled()) { $raw = self::aiDailyCarePlan($context['data'], $refresh); if (is_array($raw) && !empty($raw['summary'])) { $data = self::finalizeCarePlan($raw); } } if (!$data) { $data = GiKnowledge::fallbackBloodCarePlan( $context['data']['stats_7d'] ?? [], $context['data']['stats_30d'] ?? [], $context['data'] ); } $data['date'] = date('Y-m-d'); $data['cached'] = false; $data['analysis'] = $analysis; Cache::set($cacheKey, $data, self::CACHE_TTL); return ['ok' => true, 'data' => $data]; } /** * @param callable(string, array):void $emit */ public static function streamDailyCarePlan(int $diagnosisId, bool $refresh, callable $emit): void { if ($diagnosisId <= 0) { $emit('error', ['message' => '缺少就诊卡']); return; } $context = self::buildCareContext($diagnosisId); if (!$context['ok']) { $emit('error', ['message' => $context['error'] ?? '获取档案失败']); return; } $gate = self::ensureEnoughBloodDays($context['data']); if (!$gate['ok']) { $emit('error', ['message' => $gate['error']]); return; } $analysis = self::buildAnalysisPayload($context['data']); $emit('analysis', ['analysis' => $analysis]); $cacheKey = self::careCacheKey($diagnosisId); if (!$refresh) { $cached = Cache::get($cacheKey); if (is_array($cached) && !empty($cached['summary'])) { $cached['cached'] = true; $cached['analysis'] = $analysis; $emit('done', $cached); return; } } $data = null; $fullText = ''; if (AiChatService::isEnabled()) { [$system, $user] = self::buildCareStreamPrompts($context['data'], $refresh); $streamRes = AiChatService::streamChat([ ['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => $user], ], static function (string $delta) use (&$fullText, $emit): void { $fullText .= $delta; $emit('delta', ['text' => $delta, 'buffer' => $fullText]); }, [ 'temperature' => $refresh ? 0.75 : 0.4, 'max_tokens' => 720, 'timeout' => self::AI_STREAM_TIMEOUT_SEC, ]); if ($streamRes['ok'] && trim($fullText) !== '') { $parsed = self::parseCarePlainText(trim($fullText)); if ($parsed && !empty($parsed['summary'])) { $data = self::finalizeCarePlan($parsed); } } } if (!$data) { $data = GiKnowledge::fallbackBloodCarePlan( $context['data']['stats_7d'] ?? [], $context['data']['stats_30d'] ?? [], $context['data'] ); } $data['date'] = date('Y-m-d'); $data['cached'] = false; $data['analysis'] = $analysis; Cache::set($cacheKey, $data, self::CACHE_TTL); $emit('done', $data); } private static function careCacheKey(int $diagnosisId): string { return 'daily_blood_care_ai_v1:' . $diagnosisId . ':' . date('Y-m-d'); } /** * @return array{ok:bool,error?:string,data?:array} */ private static function buildCareContext(int $diagnosisId): array { $base = DailyDietAiLogic::getPatientContext($diagnosisId); if (!$base['ok']) { return $base; } $diagnosis = Diagnosis::where('id', $diagnosisId)->where('delete_time', null)->find(); if ($diagnosis) { $row = $diagnosis->toArray(); $base['data']['syndrome_type'] = trim((string) ($row['syndrome_type'] ?? '')); $base['data']['symptoms'] = trim((string) ($row['symptoms'] ?? '')); $base['data']['remark'] = trim((string) ($row['remark'] ?? '')); } return $base; } /** * @param array $context * @return array{ok:bool,error?:string} */ private static function ensureEnoughBloodDays(array $context): array { $days30 = (int) (($context['stats_30d']['record_days'] ?? 0)); $days7 = (int) (($context['stats_7d']['record_days'] ?? 0)); $total = max($days30, $days7); if ($total < self::MIN_RECORD_DAYS) { return ['ok' => false, 'error' => '累计记录满7天后可生成照护建议']; } return ['ok' => true]; } /** * @param array $context * @return array> */ private static function buildAnalysisPayload(array $context): array { $out = []; foreach (['stats_7d', 'stats_30d'] as $key) { $s = $context[$key] ?? null; if (is_array($s)) { $out[] = $s; } } return $out; } /** * @param array $context * @return array|null */ private static function aiDailyCarePlan(array $context, bool $refresh): ?array { [$system, $user] = self::buildCareJsonPrompts($context, $refresh); $res = AiChatService::chat([ ['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => $user], ], [ 'temperature' => $refresh ? 0.7 : 0.35, 'max_tokens' => 650, 'timeout' => self::AI_TIMEOUT_SEC, 'response_format' => ['type' => 'json_object'], ]); if (empty($res['ok']) || empty($res['content'])) { return null; } $json = self::parseJsonObject((string) $res['content']); return is_array($json) ? $json : null; } /** * @param array $context * @return array{0:string,1:string} */ private static function buildCareJsonPrompts(array $context, bool $refresh): array { $bloodLine = self::bloodLine($context); $stat7Line = self::statsLine($context['stats_7d'] ?? []); $stat30Line = self::statsLine($context['stats_30d'] ?? []); $syndrome = trim((string) ($context['syndrome_type'] ?? '')); $symptoms = trim((string) ($context['symptoms'] ?? '')); $system = << $context * @return array{0:string,1:string} */ private static function buildCareStreamPrompts(array $context, bool $refresh): array { $bloodLine = self::bloodLine($context); $stat7Line = self::statsLine($context['stats_7d'] ?? []); $stat30Line = self::statsLine($context['stats_30d'] ?? []); $system = << $context */ private static function bloodLine(array $context): string { $recent = $context['blood_recent'] ?? []; if (!is_array($recent) || empty($recent)) { return '近7日无逐日明细'; } return implode(';', $recent); } /** * @param array $stats */ private static function statsLine(array $stats): string { $label = (string) ($stats['label'] ?? '统计'); if (($stats['record_days'] ?? 0) <= 0) { return $label . '无记录'; } $parts = [ $label . "有记录{$stats['record_days']}天", "偏高{$stats['high_days']}天", "达标率{$stats['compliance']}%", ]; if ($stats['fasting_avg'] !== null) { $parts[] = "空腹均值{$stats['fasting_avg']}"; } if ($stats['postprandial_avg'] !== null) { $parts[] = "餐后均值{$stats['postprandial_avg']}"; } return implode(',', $parts); } /** * @return array|null */ private static function parseCarePlainText(string $text): ?array { $text = trim($text); if ($text === '') { return null; } if ($text[0] === '{') { $json = self::parseJsonObject($text); return is_array($json) ? $json : null; } $map = []; if (preg_match('/总评[::]\s*(.+)$/mu', $text, $m)) { $map['summary'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]); } if (preg_match('/血糖[::]\s*(.+)$/mu', $text, $m)) { $map['blood_advice'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]); } if (preg_match('/中医[::]\s*(.+)$/mu', $text, $m)) { $map['tcm_plan'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]); } if (preg_match('/留意[::]\s*(.+)$/mu', $text, $m)) { $line = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]); $parts = preg_split('/[、,,;;\s]+/u', $line) ?: []; $map['watch_points'] = array_values(array_filter(array_map('trim', $parts))); } if (preg_match('/复诊[::]\s*(.+)$/mu', $text, $m)) { $map['next_steps'] = trim(preg_split('/\R/u', $m[1])[0] ?? $m[1]); } if (empty($map['summary'])) { return null; } return $map; } /** * @param array $raw * @return array */ private static function finalizeCarePlan(array $raw): array { $watch = $raw['watch_points'] ?? []; if (!is_array($watch)) { $watch = preg_split('/[、,,;;\n]+/u', (string) $watch) ?: []; } $watch = array_values(array_filter(array_map('trim', $watch))); return [ 'summary' => trim((string) ($raw['summary'] ?? '')), 'blood_advice' => trim((string) ($raw['blood_advice'] ?? '')), 'tcm_plan' => trim((string) ($raw['tcm_plan'] ?? '')), 'watch_points' => $watch, 'next_steps' => trim((string) ($raw['next_steps'] ?? '')), 'control_level' => (string) ($raw['control_level'] ?? ''), 'control_label' => (string) ($raw['control_label'] ?? ''), 'disclaimer' => trim((string) ($raw['disclaimer'] ?? 'AI 建议供参考,不能替代医师面诊、开方与调药。')), 'source' => (string) ($raw['source'] ?? 'ai'), ]; } /** * @return array|null */ private static function parseJsonObject(string $text): ?array { $text = trim($text); if ($text === '') { return null; } if (preg_match('/\{[\s\S]*\}/u', $text, $m)) { $text = $m[0]; } $data = json_decode($text, true); return is_array($data) ? $data : null; } }