1400]); } public static function analyzeMatchSection(int $matchId, string $section, array $matchData, int $userId = 0): array { $validSections = ['prediction', 'factors', 'odds', 'summary']; if (!in_array($section, $validSections, true)) { return ['success' => false, 'error' => '无效的分析模块']; } $retrieval = self::retrieveMatchEvidence($matchId, $matchData, $userId, 'match_' . $section); $systemPrompt = AiConfig::getVal('match_predict_' . $section . '_prompt', self::matchSectionPrompt($section)); $userMessage = self::buildMatchUserMessage($matchData, $retrieval, false, $section); return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($section, $retrieval) { return self::normalizeMatchSectionResult($section, $parsed, $retrieval); }, ['max_tokens' => 1000]); } public static function analyzeArticle(int $articleId, array $articleData, int $userId = 0): array { $retrieval = KbService::retrieve( 'article_analysis', self::buildArticleQuery($articleData), [ 'user_id' => $userId, 'ref_id' => $articleId, 'domains' => ['article', 'post'], 'preferred_domain' => 'article', 'cid' => (int) ($articleData['cid'] ?? 0), 'exclude_source' => ['domain' => 'article', 'source_id' => $articleId], ], [ 'title' => (string) ($articleData['title'] ?? ''), 'summary' => (string) ($articleData['abstract'] ?? $articleData['desc'] ?? ''), ], self::intConfig('kb_final_topk', 8) ); $systemPrompt = AiConfig::getVal('article_analysis_prompt', self::defaultArticlePrompt()); $userMessage = "当前文章:\n" . json_encode([ 'id' => $articleId, 'title' => $articleData['title'] ?? '', 'abstract' => $articleData['abstract'] ?? '', 'author' => $articleData['author'] ?? '', 'category' => $articleData['category'] ?? '', 'content' => self::clipText(strip_tags((string) ($articleData['content'] ?? '')), 2500), ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n知识库证据:\n" . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n请严格依据当前文章和知识库证据输出JSON。"; return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($retrieval) { return self::normalizeArticleResult($parsed, $retrieval); }); } public static function analyzePost(int $postId, array $postData, int $userId = 0, bool $isLotteryPost = false): array { $tags = $postData['tags'] ?? []; if ($isLotteryPost) { $retrieval = self::emptyRetrieval(); $systemPrompt = self::lotteryPostImagePrompt(); $userMessage = "当前六合彩帖子与图片识别内容:\n" . json_encode([ 'id' => $postId, 'content' => self::clipText((string) ($postData['content'] ?? ''), 1800), 'tags' => $tags, 'lottery_key' => (string) (($postData['ext']['lottery_key'] ?? '') ?: ''), 'lottery_analysis_content' => self::clipText((string) ($postData['lottery_analysis_content'] ?? ''), 2200), ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n请只依据当前帖子图片识别内容和帖子文案自由解读,不需要知识库证据,也不要输出“知识库证据不足”。"; return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($retrieval, $postData) { return self::normalizePostResult($parsed, $retrieval, $postData); }, self::gptPostOptions()); } $domains = ['post', 'article']; $retrieval = KbService::retrieve( 'post_analysis', self::buildPostQuery($postData), [ 'user_id' => $userId, 'ref_id' => $postId, 'domains' => $domains, 'preferred_domain' => 'post', 'tags' => is_array($tags) ? $tags : [], 'exclude_source' => ['domain' => 'post', 'source_id' => $postId], ], [ 'title' => (string) ($postData['content'] ?? ''), ], self::intConfig('kb_final_topk', 8) ); $systemPrompt = AiConfig::getVal('post_analysis_prompt', self::defaultPostPrompt()); $userMessage = "当前帖子:\n" . json_encode([ 'id' => $postId, 'content' => self::clipText((string) ($postData['content'] ?? ''), 1800), 'tags' => $tags, 'post_type' => $postData['post_type'] ?? 0, 'is_paid' => $postData['is_paid'] ?? 0, 'lottery_key' => (string) (($postData['ext']['lottery_key'] ?? '') ?: ''), 'lottery_analysis_content' => self::clipText((string) ($postData['lottery_analysis_content'] ?? ''), 1000), ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n知识库证据:\n" . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n请输出JSON,不允许编造知识库里不存在的历史事实。"; return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($retrieval, $postData) { return self::normalizePostResult($parsed, $retrieval, $postData); }, self::gptPostOptions()); } public static function analyzeLotteryOverview(array $lotteryData, int $userId = 0): array { $code = (string) ($lotteryData['lottery_type'] ?? ''); $latestIssue = (string) ($lotteryData['recent_draws'][0]['issue'] ?? ''); $retrieval = KbService::retrieve( 'lottery_analysis', self::buildLotteryQuery($lotteryData), [ 'user_id' => $userId, 'ref_id' => 0, 'domains' => ['lottery', 'post'], 'preferred_domain' => 'lottery', 'code' => $code, 'issue' => $latestIssue, ], [ 'title' => (string) ($lotteryData['lottery_name'] ?? $code), ], self::intConfig('kb_final_topk', 8) ); $systemPrompt = AiConfig::getVal('lottery_analysis_prompt', self::defaultLotteryOverviewPrompt()); $userMessage = "当前彩种结构化数据:\n" . json_encode($lotteryData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n知识库证据:\n" . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n请结合历史知识库证据和当前结构化数据输出JSON。"; return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($retrieval) { return self::normalizeLotteryOverview($parsed, $retrieval); }, ['max_tokens' => 1800]); } public static function analyzeLotteryModule(array $lotteryData, string $module, int $userId = 0): array { $code = (string) ($lotteryData['lottery_type'] ?? ''); $latestIssue = (string) ($lotteryData['recent_draws'][0]['issue'] ?? ''); $retrieval = KbService::retrieve( 'lottery_' . $module, self::buildLotteryQuery($lotteryData), [ 'user_id' => $userId, 'ref_id' => 0, 'domains' => ['lottery', 'post'], 'preferred_domain' => 'lottery', 'code' => $code, 'issue' => $latestIssue, ], [ 'title' => (string) ($lotteryData['lottery_name'] ?? $code), ], self::intConfig('kb_final_topk', 8) ); $systemPrompt = self::modulePrompt($module); $userMessage = "当前彩种结构化数据:\n" . json_encode($lotteryData, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n知识库证据:\n" . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n请结合结构化数据和知识库证据输出JSON。"; return self::runJsonPrompt($systemPrompt, $userMessage, static function (array $parsed) use ($module, $retrieval) { return self::normalizeLotteryModule($module, $parsed, $retrieval); }, ['max_tokens' => 1200]); } private static function runJsonPrompt(string $systemPrompt, string $userMessage, callable $normalizer, array $options = []): array { $client = new DeepSeekClient(); $result = $client->chatJson($systemPrompt, $userMessage, $options); if (!$result['success']) { return $result; } $parsed = is_array($result['parsed'] ?? null) ? $result['parsed'] : []; $normalized = $normalizer($parsed); $result['parsed'] = $normalized; $result['content'] = json_encode($normalized, JSON_UNESCAPED_UNICODE); return $result; } private static function gptPostOptions(): array { return DeepSeekClient::defaultModelOptions([ 'max_tokens' => 1200, 'temperature' => 0.35, ]); } private static function compactHits(array $hits): array { return array_map(static function (array $hit) { return [ 'domain' => $hit['domain'] ?? '', 'subtype' => $hit['subtype'] ?? '', 'source_id' => $hit['source_id'] ?? 0, 'title' => $hit['title'] ?? '', 'summary' => $hit['summary'] ?? '', 'content' => self::clipText((string) ($hit['chunk_text'] ?? ''), 260), 'score' => $hit['score'] ?? 0, 'metadata' => $hit['metadata'] ?? [], ]; }, $hits); } private static function normalizeArticleResult(array $parsed, array $retrieval): array { $bettingAnalysis = $parsed['betting_analysis'] ?? [ 'prediction' => '谨慎观望', 'confidence' => 55, 'reasoning' => '当前证据不足,建议谨慎观望。', 'risk_level' => 'medium', 'key_factors' => [], 'value_bet' => '无明显价值', ]; $bettingAnalysis['risk_level'] = self::normalizeRiskLevel((string) ($bettingAnalysis['risk_level'] ?? 'medium')); return [ 'overall_score' => (int) ($parsed['overall_score'] ?? 68), 'summary' => (string) ($parsed['summary'] ?? '暂无明确结论,请结合原文谨慎判断。'), 'leagues_and_teams' => $parsed['leagues_and_teams'] ?? ['leagues' => [], 'teams' => [], 'match_context' => ''], 'upcoming_matches' => $parsed['upcoming_matches'] ?? [], 'head_to_head' => $parsed['head_to_head'] ?? ['available' => false, 'records' => [], 'summary' => ''], 'betting_analysis' => $bettingAnalysis, 'risk_warnings' => $parsed['risk_warnings'] ?? ['以上内容仅供参考,请理性判断。'], 'key_points' => $parsed['key_points'] ?? [], 'opposite_views' => $parsed['opposite_views'] ?? [], 'sentiment' => $parsed['sentiment'] ?? ['label' => 'neutral', 'confidence' => 55], 'tags' => $parsed['tags'] ?? [], 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), 'from_kb' => !empty($retrieval['hits']), 'retrieval_meta' => self::retrievalMeta($retrieval), ]; } private static function normalizePostResult(array $parsed, array $retrieval, array $postData): array { $result = [ 'overall_score' => (int) ($parsed['overall_score'] ?? 66), 'summary' => (string) ($parsed['summary'] ?? '当前帖子可参考信息有限,请谨慎判断。'), 'credibility_signals' => $parsed['credibility_signals'] ?? [], 'risk_warning' => $parsed['risk_warning'] ?? ['历史样本有限,不宜过度放大单条帖子观点。'], 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), 'from_kb' => !empty($retrieval['hits']), 'retrieval_meta' => self::retrievalMeta($retrieval), ]; if (self::isLiuhePost($postData, $retrieval['hits'] ?? [])) { $result['summary'] = self::normalizeLiuheImageText( $result['summary'], '本次分析仅依据当前图片和帖子文案进行自由解读,可重点关注图片中出现的诗句、生肖、波色、尾数、胆码和旺弱号码等线索;结果仅供内容解读。' ); $result['credibility_signals'] = self::normalizeLiuheImageList( $result['credibility_signals'], ['当前分析基于图片识别出的可见文字、号码和图表线索。'] ); $result['risk_warning'] = self::normalizeLiuheImageList( $result['risk_warning'], ['彩票结果具有随机性,图片解读仅供参考,不构成投注建议。'] ); $result['next_prediction'] = self::normalizeLiuhePrediction($parsed['next_prediction'] ?? []); $result['next_prediction']['reasons'] = self::normalizeLiuheImageList( $result['next_prediction']['reasons'], ['推荐仅来自当前图片中的诗句、胆码、波色、尾数或生肖等线索。'] ); $result['image_interpretation'] = self::normalizeStringList($parsed['image_interpretation'] ?? []); $result['poem_analysis'] = (string) ($parsed['poem_analysis'] ?? ''); $result['analysis_source'] = (string) ($parsed['analysis_source'] ?? 'image_free_v1'); } return $result; } private static function emptyRetrieval(): array { return [ 'hits' => [], 'hit_count' => 0, 'domains' => [], 'top_scores' => [], ]; } private static function normalizeLotteryOverview(array $parsed, array $retrieval): array { return [ 'overall_score' => (int) ($parsed['overall_score'] ?? 62), 'summary' => (string) ($parsed['summary'] ?? '当前仅能给出基于历史样本的统计参考。'), 'hot_numbers' => $parsed['hot_numbers'] ?? [], 'cold_numbers' => $parsed['cold_numbers'] ?? [], 'trend_analysis' => $parsed['trend_analysis'] ?? '', 'recommended_combinations' => $parsed['recommended_combinations'] ?? [], 'statistics' => $parsed['statistics'] ?? [], 'trend_direction' => $parsed['trend_direction'] ?? '震荡', 'risk_level' => $parsed['risk_level'] ?? '中', 'key_evidence' => $parsed['key_evidence'] ?? [], 'similar_issues' => $parsed['similar_issues'] ?? self::similarHistory($retrieval['hits'] ?? []), 'disclaimer' => $parsed['disclaimer'] ?? '本分析仅基于历史数据与知识库证据,不保证准确性,请理性参考。', 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), 'from_kb' => !empty($retrieval['hits']), 'retrieval_meta' => self::retrievalMeta($retrieval), ]; } private static function normalizeLotteryModule(string $module, array $parsed, array $retrieval): array { $base = [ 'overall_score' => (int) ($parsed['overall_score'] ?? 60), 'risk_warning' => $parsed['risk_warning'] ?? ['本结果只反映历史统计规律,不保证未来表现。'], 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), 'from_kb' => !empty($retrieval['hits']), 'retrieval_meta' => self::retrievalMeta($retrieval), ]; return match ($module) { 'hot_cold' => array_merge($base, [ 'hot_numbers' => $parsed['hot_numbers'] ?? [], 'cold_numbers' => $parsed['cold_numbers'] ?? [], 'hot_analysis' => $parsed['hot_analysis'] ?? '', 'cold_analysis' => $parsed['cold_analysis'] ?? '', ]), 'trend' => array_merge($base, [ 'trend_analysis' => $parsed['trend_analysis'] ?? '', 'recommended_combinations' => $parsed['recommended_combinations'] ?? [], 'disclaimer' => $parsed['disclaimer'] ?? '趋势分析仅供参考。', 'trend_direction' => $parsed['trend_direction'] ?? '震荡', ]), 'stats' => array_merge($base, [ 'odd_even_ratio' => $parsed['odd_even_ratio'] ?? '', 'big_small_ratio' => $parsed['big_small_ratio'] ?? '', 'sum_range' => $parsed['sum_range'] ?? '', 'consecutive_analysis' => $parsed['consecutive_analysis'] ?? '', 'span_analysis' => $parsed['span_analysis'] ?? '', 'frequency_table' => $parsed['frequency_table'] ?? [], 'summary' => $parsed['summary'] ?? '', ]), 'color_zodiac' => array_merge($base, [ 'color_analysis' => $parsed['color_analysis'] ?? [], 'zodiac_analysis' => $parsed['zodiac_analysis'] ?? [], 'recommended' => $parsed['recommended'] ?? [], ]), default => array_merge($base, $parsed), }; } private static function retrievalMeta(array $retrieval): array { return [ 'hit_count' => (int) ($retrieval['hit_count'] ?? 0), 'domains' => $retrieval['domains'] ?? [], 'top_scores' => $retrieval['top_scores'] ?? [], ]; } private static function similarHistory(array $hits): array { $rows = []; foreach ($hits as $hit) { $metadata = $hit['metadata'] ?? []; $rows[] = [ 'domain' => $hit['domain'] ?? '', 'source_id' => $hit['source_id'] ?? 0, 'title' => $hit['title'] ?? '', 'issue' => $metadata['issue'] ?? '', 'code' => $metadata['code'] ?? '', 'score' => $hit['score'] ?? 0, ]; } return array_slice($rows, 0, 5); } private static function buildArticleQuery(array $articleData): string { return trim(implode(' ', array_filter([ $articleData['title'] ?? '', $articleData['abstract'] ?? '', $articleData['category'] ?? '', $articleData['desc'] ?? '', ]))); } private static function buildPostQuery(array $postData): string { $tags = is_array($postData['tags'] ?? null) ? implode(' ', $postData['tags']) : ''; $lotteryKey = (string) (($postData['ext']['lottery_key'] ?? '') ?: ''); return trim(implode(' ', array_filter([ self::clipText((string) ($postData['content'] ?? ''), 300), $tags, $lotteryKey, ]))); } private static function buildLotteryQuery(array $lotteryData): string { $recentIssues = array_column($lotteryData['recent_draws'] ?? [], 'issue'); return trim(implode(' ', array_filter([ $lotteryData['lottery_type'] ?? '', $lotteryData['lottery_name'] ?? '', implode(' ', array_slice($recentIssues, 0, 5)), ]))); } private static function buildMatchQuery(array $matchData): string { $matchInfo = $matchData['match_info'] ?? []; $headToHead = $matchData['head_to_head'] ?? []; $keywords = [ $matchInfo['home_team'] ?? '', $matchInfo['away_team'] ?? '', $matchInfo['league'] ?? '', ]; foreach (array_slice($headToHead, 0, 3) as $row) { $keywords[] = $row['home_team'] ?? ''; $keywords[] = $row['away_team'] ?? ''; $keywords[] = $row['league_name'] ?? ($row['league'] ?? ''); } return trim(implode(' ', array_filter($keywords))); } private static function retrieveMatchEvidence(int $matchId, array $matchData, int $userId, string $scene): array { $matchInfo = $matchData['match_info'] ?? []; return KbService::retrieve( $scene, self::buildMatchQuery($matchData), [ 'user_id' => $userId, 'ref_id' => $matchId, 'domains' => ['article', 'post'], 'preferred_domain' => 'article', ], [ 'title' => trim(($matchInfo['home_team'] ?? '') . ' vs ' . ($matchInfo['away_team'] ?? '')), 'league' => (string) ($matchInfo['league'] ?? ''), 'teams' => array_values(array_filter([ $matchInfo['home_team'] ?? '', $matchInfo['away_team'] ?? '', ])), ], self::intConfig('kb_final_topk', 8) ); } private static function buildMatchUserMessage( array $matchData, array $retrieval, bool $full = false, string $section = '' ): string { $label = $full ? '完整赛事分析' : ('赛事分段分析-' . $section); return $label . ":\n当前赛事结构化数据:\n" . json_encode([ 'match_info' => $matchData['match_info'] ?? [], 'head_to_head' => self::compactMatchRows($matchData['head_to_head'] ?? [], 5), 'home_recent' => self::compactMatchRows($matchData['home_recent'] ?? [], 5), 'away_recent' => self::compactMatchRows($matchData['away_recent'] ?? [], 5), ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n知识库证据(资讯/帖子):\n" . json_encode(self::compactHits($retrieval['hits'] ?? []), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n\n请优先依据当前赛事结构化数据与知识库证据输出,不允许编造未命中的历史事实。"; } private static function compactMatchRows(array $rows, int $limit = 5): array { $compact = []; foreach (array_slice($rows, 0, $limit) as $row) { $compact[] = [ 'match_time' => $row['match_time'] ?? '', 'league' => $row['league_name'] ?? ($row['league'] ?? ''), 'home_team' => $row['home_team'] ?? ($row['team_name'] ?? ''), 'away_team' => $row['away_team'] ?? ($row['opponent'] ?? ''), 'score' => [ 'home' => $row['home_score'] ?? ($row['team_score'] ?? null), 'away' => $row['away_score'] ?? ($row['opponent_score'] ?? null), ], 'result' => $row['result'] ?? '', ]; } return $compact; } private static function clipText(string $text, int $length): string { $text = trim(preg_replace('/\s+/u', ' ', $text) ?: ''); return mb_strlen($text) > $length ? mb_substr($text, 0, $length) . '...' : $text; } private static function intConfig(string $name, int $default): int { return (int) AiConfig::getVal($name, (string) $default); } private static function defaultArticlePrompt(): string { return '你是一位体育资讯AI研报助手。请严格根据当前文章和知识库证据生成分析,不允许虚构未命中的历史事实。' . '必须输出JSON,字段包含:' . '{"overall_score":72,"summary":"...","leagues_and_teams":{"leagues":[],"teams":[],"match_context":""},"upcoming_matches":[],"head_to_head":{"available":false,"records":[],"summary":""},"betting_analysis":{"prediction":"谨慎观望","confidence":55,"reasoning":"...","risk_level":"medium","key_factors":[],"value_bet":"无明显价值"},"risk_warnings":["..."],"key_points":["..."],"opposite_views":["..."],"sentiment":{"label":"neutral","confidence":60},"tags":["..."],"key_evidence":["..."]}'; } private static function defaultMatchPrompt(): string { return '你是一位体育赛事AI分析师。请结合当前赛事结构化数据(交手记录、双方近期战绩、赔率)与知识库中召回的相关资讯/帖子,生成完整比赛分析。' . '若知识库证据不足,要明确说明证据不足。' . '必须输出JSON:{"prediction":{"result":"主胜/平局/客胜","confidence":75,"score_predict":"2-1","reasoning":"..."},"analysis":{"home_strength":78,"away_strength":72,"key_factors":["..."],"risk_factors":["..."]},"odds_opinion":{"value_bet":"主胜/平局/客胜/无","explanation":"..."},"summary":"...","evidence_list":[],"risk_warning":["..."]}'; } private static function defaultPostPrompt(): string { return '你是一位社区内容分析助手。请根据当前帖子和知识库证据,判断帖子的核心观点、可信线索与风险。' . '如果知识库证据不足,必须明确说明证据不足。' . '如果帖子属于六合彩(例如旧澳六合/新澳六合)资料或开奖类帖子,请额外给出基于当前帖子内容与知识库历史样本的下一期预测,字段包含推荐号码、信心和依据。' . '输出JSON:{"overall_score":66,"summary":"...","credibility_signals":["..."],"risk_warning":["..."],"next_prediction":{"recommended_numbers":["..."],"confidence":68,"reasons":["..."]}}'; } private static function isLiuhePost(array $postData, array $hits): bool { $tags = is_array($postData['tags'] ?? null) ? $postData['tags'] : []; if (in_array('旧澳六合', $tags, true) || in_array('新澳六合', $tags, true)) { return true; } $ext = is_array($postData['ext'] ?? null) ? $postData['ext'] : []; $lotteryKey = (string) ($ext['lottery_key'] ?? ''); if (in_array($lotteryKey, ['a6', 'xa6'], true)) { return true; } foreach ($hits as $hit) { $title = (string) ($hit['title'] ?? ''); $metadata = $hit['metadata'] ?? []; $hitLotteryKey = (string) ($metadata['lottery_key'] ?? ''); if (str_contains($title, '旧澳六合') || str_contains($title, '新澳六合') || in_array($hitLotteryKey, ['a6', 'xa6'], true)) { return true; } } return false; } private static function normalizeLiuhePrediction(array $prediction): array { $numbers = self::normalizeStringList($prediction['recommended_numbers'] ?? []); $reasons = self::normalizeStringList($prediction['reasons'] ?? []); return [ 'recommended_numbers' => $numbers, 'confidence' => max(0, min(100, (int) ($prediction['confidence'] ?? 60))), 'reasons' => $reasons, ]; } private static function normalizeStringList($items): array { if (is_string($items)) { $items = preg_split('/[\r\n;;]+/u', $items) ?: []; } if (!is_array($items)) { return []; } return array_values(array_filter(array_map(static function ($item) { $text = trim((string) $item); return $text === '' ? null : $text; }, $items))); } private static function normalizeLiuheImageText(string $text, string $fallback): string { $text = trim($text); if ($text === '' || str_contains($text, '知识库')) { return $fallback; } return $text; } private static function normalizeLiuheImageList($items, array $fallback): array { $rows = array_values(array_filter(self::normalizeStringList($items), static function (string $item) { return !str_contains($item, '知识库'); })); return $rows ?: $fallback; } private static function lotteryPostImagePrompt(): string { return '你是一位六合彩图片资料解读助手。请只依据当前帖子文案与图片识别内容进行分析,不需要知识库证据。' . '如果识别内容里包含诗句、字谜、藏宝图、玄机图、生肖、波色、尾数、胆码、旺弱号码、推荐组合,请逐项自由解读它们可能指向的生肖或号码。' . '不要因为没有历史知识库证据而给出“证据不足”的主结论;可以说明彩票随机性和不保证命中,但主体要围绕图片内容展开。' . '不要承诺必中,不要编造图片中不存在的文字或号码。' . '输出JSON:{"overall_score":66,"summary":"基于图片内容的综合解读","image_interpretation":["逐项解读"],"poem_analysis":"诗句/字谜解读,没有则为空","credibility_signals":["来自图片的可信线索"],"risk_warning":["理性提示"],"next_prediction":{"recommended_numbers":["..."],"confidence":60,"reasons":["..."]},"analysis_source":"image_free_v1"}'; } private static function defaultLotteryOverviewPrompt(): string { return '你是一位彩票数据分析助手。请结合当前结构化开奖数据与知识库中的历史开奖、历史AI分析、相关帖子证据,输出兼容旧接口的JSON。' . '必须包含字段:{"overall_score":62,"summary":"...","hot_numbers":[],"cold_numbers":[],"trend_analysis":"...","recommended_combinations":[],"statistics":{},"trend_direction":"震荡","risk_level":"中","key_evidence":["..."],"similar_issues":[],"disclaimer":"..."}'; } private static function matchSectionPrompt(string $module): string { return match ($module) { 'prediction' => '你是一位体育赛事AI分析师。请结合当前赛事结构化数据和知识库资讯/帖子证据,预测比赛结果。输出JSON:{"prediction":{"result":"主胜/平局/客胜","confidence":75,"score_predict":"2-1","reasoning":"..."},"analysis":{"home_strength":78,"away_strength":72},"evidence_list":[],"risk_warning":["..."]}', 'factors' => '你是一位体育赛事AI分析师。请结合当前赛事结构化数据和知识库资讯/帖子证据,分析关键因素与风险。输出JSON:{"key_factors":["..."],"risk_factors":["..."],"evidence_list":[],"risk_warning":["..."]}', 'odds' => '你是一位体育赛事AI分析师。请结合当前赛事结构化数据和知识库资讯/帖子证据,分析赔率与投注方向。输出JSON:{"odds_opinion":{"value_bet":"主胜/平局/客胜/无","explanation":"..."},"evidence_list":[],"risk_warning":["..."]}', 'summary' => '你是一位体育赛事AI分析师。请结合当前赛事结构化数据和知识库资讯/帖子证据,给出简洁结论。输出JSON:{"summary":"...","evidence_list":[],"risk_warning":["..."]}', default => '请输出JSON。', }; } private static function modulePrompt(string $module): string { return match ($module) { 'hot_cold' => '你是一位彩票热冷号分析助手。请结合当前结构化开奖数据和知识库证据输出JSON:{"overall_score":60,"hot_numbers":[],"cold_numbers":[],"hot_analysis":"...","cold_analysis":"...","risk_warning":["..."]}', 'trend' => '你是一位彩票趋势分析助手。请结合当前结构化开奖数据和知识库证据输出JSON:{"overall_score":60,"trend_analysis":"...","recommended_combinations":[],"trend_direction":"震荡","disclaimer":"...","risk_warning":["..."]}', 'stats' => '你是一位彩票统计分析助手。请结合当前结构化开奖数据和知识库证据输出JSON:{"overall_score":60,"odd_even_ratio":"","big_small_ratio":"","sum_range":"","consecutive_analysis":"...","span_analysis":"...","frequency_table":[],"summary":"...","risk_warning":["..."]}', 'color_zodiac' => '你是一位六合彩颜色生肖分析助手。请结合当前结构化开奖数据和知识库证据输出JSON:{"overall_score":60,"color_analysis":{},"zodiac_analysis":{},"recommended":{},"risk_warning":["..."]}', default => '请输出JSON。', }; } private static function normalizeRiskLevel(string $value): string { $value = strtolower(trim($value)); return match ($value) { '低', 'low' => 'low', '高', 'high' => 'high', default => 'medium', }; } private static function normalizeMatchResult(array $parsed, array $retrieval): array { return [ 'prediction' => $parsed['prediction'] ?? [ 'result' => '谨慎观望', 'confidence' => 55, 'score_predict' => '-', 'reasoning' => '当前证据不足,建议谨慎观望。', ], 'analysis' => array_merge([ 'home_strength' => 50, 'away_strength' => 50, 'key_factors' => [], 'risk_factors' => [], ], is_array($parsed['analysis'] ?? null) ? $parsed['analysis'] : []), 'odds_opinion' => $parsed['odds_opinion'] ?? [ 'value_bet' => '无', 'explanation' => '赔率证据不足,建议谨慎观望。', ], 'summary' => (string) ($parsed['summary'] ?? '当前仅能给出基于结构化数据和有限知识库证据的保守结论。'), 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), 'from_kb' => !empty($retrieval['hits']), 'retrieval_meta' => self::retrievalMeta($retrieval), ]; } private static function normalizeMatchSectionResult(string $section, array $parsed, array $retrieval): array { $base = [ 'evidence_list' => self::compactHits($retrieval['hits'] ?? []), 'similar_history' => self::similarHistory($retrieval['hits'] ?? []), 'from_kb' => !empty($retrieval['hits']), 'retrieval_meta' => self::retrievalMeta($retrieval), 'risk_warning' => $parsed['risk_warning'] ?? ['以上分析仅供参考,请结合比赛临场信息谨慎判断。'], ]; return match ($section) { 'prediction' => array_merge($base, [ 'prediction' => $parsed['prediction'] ?? [ 'result' => '谨慎观望', 'confidence' => 55, 'score_predict' => '-', 'reasoning' => '当前证据不足,建议谨慎观望。', ], 'analysis' => array_merge([ 'home_strength' => 50, 'away_strength' => 50, ], is_array($parsed['analysis'] ?? null) ? $parsed['analysis'] : []), ]), 'factors' => array_merge($base, [ 'key_factors' => $parsed['key_factors'] ?? [], 'risk_factors' => $parsed['risk_factors'] ?? [], ]), 'odds' => array_merge($base, [ 'odds_opinion' => $parsed['odds_opinion'] ?? [ 'value_bet' => '无', 'explanation' => '赔率证据不足,建议谨慎观望。', ], ]), 'summary' => array_merge($base, [ 'summary' => (string) ($parsed['summary'] ?? '当前仅能给出基于结构化数据和有限知识库证据的保守结论。'), ]), default => array_merge($base, $parsed), }; } }