diff --git a/app/Jobs/AiBaccaratBetJob.php b/app/Jobs/AiBaccaratBetJob.php index 027f038..9e13467 100644 --- a/app/Jobs/AiBaccaratBetJob.php +++ b/app/Jobs/AiBaccaratBetJob.php @@ -6,7 +6,7 @@ * 在每局百家乐开启时延迟调度执行: * 1. 检查是否存在连输休息惩罚(1小时) * 2. 检查可用金币,确保留存底金 - * 3. 获取近期路单进行简单决策 + * 3. 调用 AI 接口预测路单走势决定下注方向(AI 不可用时回退本地决策) * 4. 提交下注 * * @author ChatRoom Laravel @@ -22,6 +22,7 @@ use App\Models\BaccaratRound; use App\Models\GameConfig; use App\Models\Sysparam; use App\Models\User; +use App\Services\BaccaratPredictionService; use App\Services\UserCurrencyService; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; @@ -82,30 +83,34 @@ class AiBaccaratBetJob implements ShouldQueue return; } - // 4. 决策逻辑:简单分析近期路单 - // 取最近 10 局 + // 4. 获取近期路单(最多取 20 局) $recentResults = BaccaratRound::query() ->where('status', 'settled') ->orderByDesc('id') - ->limit(10) + ->limit(20) ->pluck('result') ->toArray(); - $bigCount = count(array_filter($recentResults, fn ($r) => $r === 'big')); - $smallCount = count(array_filter($recentResults, fn ($r) => $r === 'small')); + // 5. 优先调用 AI 接口预测下注方向 + $predictionService = app(BaccaratPredictionService::class); + $betType = $predictionService->predict($recentResults); - // 基础策略:追逐热点 (跟大部队) 或 均值回归 (逆势) - // 这里做一个简单的随机倾向: - $strategy = rand(1, 100); - if ($strategy <= 10) { - $betType = 'triple'; // 10% 概率博豹子 - } elseif ($bigCount > $smallCount) { - // 大偏热,70%概率顺势买大,30%逆势买小 - $betType = rand(1, 100) <= 70 ? 'big' : 'small'; - } elseif ($smallCount > $bigCount) { - $betType = rand(1, 100) <= 70 ? 'small' : 'big'; - } else { - $betType = rand(0, 1) ? 'big' : 'small'; + // AI 不可用时回退本地路单决策(保底逻辑) + if ($betType === null) { + $bigCount = count(array_filter($recentResults, fn (string $r) => $r === 'big')); + $smallCount = count(array_filter($recentResults, fn (string $r) => $r === 'small')); + + $strategy = rand(1, 100); + if ($strategy <= 10) { + $betType = 'triple'; // 10% 概率博豹子 + } elseif ($bigCount > $smallCount) { + // 大偏热:70% 概率顺势买大,30% 逆势买小 + $betType = rand(1, 100) <= 70 ? 'big' : 'small'; + } elseif ($smallCount > $bigCount) { + $betType = rand(1, 100) <= 70 ? 'small' : 'big'; + } else { + $betType = rand(0, 1) ? 'big' : 'small'; + } } // 5. 执行下注 (同 BaccaratController::bet 逻辑) diff --git a/app/Services/BaccaratPredictionService.php b/app/Services/BaccaratPredictionService.php new file mode 100644 index 0000000..c109f54 --- /dev/null +++ b/app/Services/BaccaratPredictionService.php @@ -0,0 +1,222 @@ + $recentResults 近期已结算路单('big'|'small'|'triple',从最新到最旧) + * @return string|null 预测结果:'big'|'small'|'triple',或 null(AI 不可用) + */ + public function predict(array $recentResults): ?string + { + $provider = AiProviderConfig::getDefault(); + + if (! $provider) { + Log::warning('百家乐 AI 预测:无可用 AI 厂商配置,跳过 AI 预测'); + + return null; + } + + $prompt = $this->buildPredictionPrompt($recentResults); + + try { + return $this->callProvider($provider, $prompt); + } catch (\Exception $e) { + Log::warning('百家乐 AI 预测调用失败,将使用本地决策兜底', [ + 'provider' => $provider->name, + 'error' => $e->getMessage(), + ]); + + return null; + } + } + + /** + * 构建发送给 AI 的预测提示词 + * + * 将路单数据转为人类可读格式,要求 AI 仅回复三种固定关键词之一。 + * + * @param array $recentResults 路单数组 + */ + private function buildPredictionPrompt(array $recentResults): string + { + // 将英文 key 转为中文展示,方便 AI 理解 + $labelMap = ['big' => '大', 'small' => '小', 'triple' => '豹子']; + + $roadmap = array_map( + fn (string $result) => $labelMap[$result] ?? $result, + $recentResults + ); + + // 路单从旧到新排列(最后一条是最近一局) + $roadmapText = implode(' → ', array_reverse($roadmap)); + $total = count($recentResults); + + return <<getDecryptedApiKey(); + + // 智能拼接端点 URL(与 AiChatService 保持一致) + $base = rtrim($config->api_endpoint, '/'); + $endpoint = str_ends_with($base, '/v1') + ? $base.'/chat/completions' + : $base.'/v1/chat/completions'; + + try { + $response = Http::withToken($apiKey) + ->timeout(self::REQUEST_TIMEOUT) + ->post($endpoint, [ + 'model' => $config->model, + 'temperature' => 0.3, // 预测任务偏确定性,使用较低温度 + 'max_tokens' => 10, // 只需要输出单个词 + 'messages' => [ + [ + 'role' => 'system', + 'content' => '你是百家乐路单分析专家,只输出"大"、"小"或"豹子"三个词之一,不输出任何其他内容。', + ], + [ + 'role' => 'user', + 'content' => $prompt, + ], + ], + ]); + + $responseTimeMs = (int) ((microtime(true) - $startTime) * 1000); + + if (! $response->successful()) { + $this->logUsage($config, $responseTimeMs, false, $response->body()); + throw new \Exception("HTTP {$response->status()}: {$response->body()}"); + } + + $data = $response->json(); + $reply = trim($data['choices'][0]['message']['content'] ?? ''); + + $promptTokens = $data['usage']['prompt_tokens'] ?? 0; + $completionTokens = $data['usage']['completion_tokens'] ?? 0; + $this->logUsage($config, $responseTimeMs, true, null, $promptTokens, $completionTokens); + + // 将 AI 回复的中文词解析为系统内部 key + return $this->parseReply($reply); + } catch (\Illuminate\Http\Client\ConnectionException $e) { + $responseTimeMs = (int) ((microtime(true) - $startTime) * 1000); + $this->logUsage($config, $responseTimeMs, false, $e->getMessage()); + + throw new \Exception("连接超时: {$e->getMessage()}"); + } + } + + /** + * 将 AI 回复的中文词解析为系统内部下注键 + * + * @param string $reply AI 返回的原始文本 + * @return string 'big'|'small'|'triple' + * + * @throws \Exception 无法识别时抛出 + */ + private function parseReply(string $reply): string + { + // 从回复中提取核心词(容忍多余空白/标点) + $cleaned = trim(preg_replace('/\s+/', '', $reply)); + + return match ($cleaned) { + '大' => 'big', + '小' => 'small', + '豹子' => 'triple', + default => throw new \Exception("AI 返回无法识别的预测结果:{$reply}"), + }; + } + + /** + * 记录 AI 调用日志到 ai_usage_logs 表 + * + * @param AiProviderConfig $config AI 厂商配置 + * @param int $responseTimeMs 响应时间(毫秒) + * @param bool $success 是否成功 + * @param string|null $errorMessage 错误信息 + * @param int $promptTokens 输入 token 数 + * @param int $completionTokens 输出 token 数 + */ + private function logUsage( + AiProviderConfig $config, + int $responseTimeMs, + bool $success, + ?string $errorMessage = null, + int $promptTokens = 0, + int $completionTokens = 0, + ): void { + try { + AiUsageLog::create([ + 'user_id' => 0, // AI 系统行为,无对应用户 + 'provider' => $config->provider, + 'model' => $config->model, + 'action' => 'baccarat_predict', + 'prompt_tokens' => $promptTokens, + 'completion_tokens' => $completionTokens, + 'total_tokens' => $promptTokens + $completionTokens, + 'response_time_ms' => $responseTimeMs, + 'success' => $success, + 'error_message' => $errorMessage ? mb_substr($errorMessage, 0, 500) : null, + ]); + } catch (\Exception $e) { + Log::error('百家乐 AI 预测日志记录失败', ['error' => $e->getMessage()]); + } + } +}