feat(百家乐): AI小班长改用AI接口预测路单走势下注
- 新增 BaccaratPredictionService,调用 AI 厂商(OpenAI 兼容协议) 根据近期 20 局路单给出预测(大/小/豹子) - AiBaccaratBetJob 优先使用 AI 预测结果; AI 不可用(超时/无配置)时自动回退本地路单统计决策 - 复用 AiProviderConfig 多厂商配置与故障转移逻辑 - AI 调用结果写入 ai_usage_logs(action=baccarat_predict)
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
* 在每局百家乐开启时延迟调度执行:
|
* 在每局百家乐开启时延迟调度执行:
|
||||||
* 1. 检查是否存在连输休息惩罚(1小时)
|
* 1. 检查是否存在连输休息惩罚(1小时)
|
||||||
* 2. 检查可用金币,确保留存底金
|
* 2. 检查可用金币,确保留存底金
|
||||||
* 3. 获取近期路单进行简单决策
|
* 3. 调用 AI 接口预测路单走势决定下注方向(AI 不可用时回退本地决策)
|
||||||
* 4. 提交下注
|
* 4. 提交下注
|
||||||
*
|
*
|
||||||
* @author ChatRoom Laravel
|
* @author ChatRoom Laravel
|
||||||
@@ -22,6 +22,7 @@ use App\Models\BaccaratRound;
|
|||||||
use App\Models\GameConfig;
|
use App\Models\GameConfig;
|
||||||
use App\Models\Sysparam;
|
use App\Models\Sysparam;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Services\BaccaratPredictionService;
|
||||||
use App\Services\UserCurrencyService;
|
use App\Services\UserCurrencyService;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
@@ -82,30 +83,34 @@ class AiBaccaratBetJob implements ShouldQueue
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 决策逻辑:简单分析近期路单
|
// 4. 获取近期路单(最多取 20 局)
|
||||||
// 取最近 10 局
|
|
||||||
$recentResults = BaccaratRound::query()
|
$recentResults = BaccaratRound::query()
|
||||||
->where('status', 'settled')
|
->where('status', 'settled')
|
||||||
->orderByDesc('id')
|
->orderByDesc('id')
|
||||||
->limit(10)
|
->limit(20)
|
||||||
->pluck('result')
|
->pluck('result')
|
||||||
->toArray();
|
->toArray();
|
||||||
|
|
||||||
$bigCount = count(array_filter($recentResults, fn ($r) => $r === 'big'));
|
// 5. 优先调用 AI 接口预测下注方向
|
||||||
$smallCount = count(array_filter($recentResults, fn ($r) => $r === 'small'));
|
$predictionService = app(BaccaratPredictionService::class);
|
||||||
|
$betType = $predictionService->predict($recentResults);
|
||||||
|
|
||||||
// 基础策略:追逐热点 (跟大部队) 或 均值回归 (逆势)
|
// AI 不可用时回退本地路单决策(保底逻辑)
|
||||||
// 这里做一个简单的随机倾向:
|
if ($betType === null) {
|
||||||
$strategy = rand(1, 100);
|
$bigCount = count(array_filter($recentResults, fn (string $r) => $r === 'big'));
|
||||||
if ($strategy <= 10) {
|
$smallCount = count(array_filter($recentResults, fn (string $r) => $r === 'small'));
|
||||||
$betType = 'triple'; // 10% 概率博豹子
|
|
||||||
} elseif ($bigCount > $smallCount) {
|
$strategy = rand(1, 100);
|
||||||
// 大偏热,70%概率顺势买大,30%逆势买小
|
if ($strategy <= 10) {
|
||||||
$betType = rand(1, 100) <= 70 ? 'big' : 'small';
|
$betType = 'triple'; // 10% 概率博豹子
|
||||||
} elseif ($smallCount > $bigCount) {
|
} elseif ($bigCount > $smallCount) {
|
||||||
$betType = rand(1, 100) <= 70 ? 'small' : 'big';
|
// 大偏热:70% 概率顺势买大,30% 逆势买小
|
||||||
} else {
|
$betType = rand(1, 100) <= 70 ? 'big' : 'small';
|
||||||
$betType = rand(0, 1) ? 'big' : 'small';
|
} elseif ($smallCount > $bigCount) {
|
||||||
|
$betType = rand(1, 100) <= 70 ? 'small' : 'big';
|
||||||
|
} else {
|
||||||
|
$betType = rand(0, 1) ? 'big' : 'small';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 执行下注 (同 BaccaratController::bet 逻辑)
|
// 5. 执行下注 (同 BaccaratController::bet 逻辑)
|
||||||
|
|||||||
@@ -0,0 +1,222 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文件功能:百家乐 AI 预测服务
|
||||||
|
*
|
||||||
|
* 通过调用已配置的 AI 厂商接口(OpenAI 兼容协议),
|
||||||
|
* 根据近期路单数据分析走势,预测下一局应押注"大"、"小"还是"豹子"。
|
||||||
|
*
|
||||||
|
* 复用 AiChatService 中相同的厂商配置与 HTTP 调用模式。
|
||||||
|
*
|
||||||
|
* @author ChatRoom Laravel
|
||||||
|
*
|
||||||
|
* @version 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\AiProviderConfig;
|
||||||
|
use App\Models\AiUsageLog;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class BaccaratPredictionService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* AI 请求超时时间(秒)
|
||||||
|
*
|
||||||
|
* 百家乐预测对延迟敏感,超时则回退本地决策。
|
||||||
|
*/
|
||||||
|
private const REQUEST_TIMEOUT = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 AI 接口预测百家乐下注方向
|
||||||
|
*
|
||||||
|
* 将近期路单格式化后发给 AI,解析其返回的预测结果。
|
||||||
|
* 若 AI 不可用或解析失败,返回 null 以便调用方回退本地逻辑。
|
||||||
|
*
|
||||||
|
* @param array<int, string> $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<int, string> $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 <<<PROMPT
|
||||||
|
你是一位百家乐路单分析专家。请根据以下最近 {$total} 局的开奖路单,预测下一局最可能的结果。
|
||||||
|
|
||||||
|
路单(从最早到最新):{$roadmapText}
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- "大" 表示骰子总点数为 11~17
|
||||||
|
- "小" 表示骰子总点数为 4~10
|
||||||
|
- "豹子" 表示三颗骰子点数相同(极小概率)
|
||||||
|
|
||||||
|
请综合分析路单走势(如连庄、跳变、交替等特征),仅输出以下三个词之一:
|
||||||
|
大 / 小 / 豹子
|
||||||
|
|
||||||
|
【重要】只输出单个词,不要包含任何其他文字、标点、换行或解释。
|
||||||
|
PROMPT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调用 AI 厂商接口并解析预测结果
|
||||||
|
*
|
||||||
|
* @param AiProviderConfig $config AI 厂商配置
|
||||||
|
* @param string $prompt 预测提示词
|
||||||
|
* @return string 预测结果:'big'|'small'|'triple'
|
||||||
|
*
|
||||||
|
* @throws \Exception 调用失败或无法解析结果时抛出
|
||||||
|
*/
|
||||||
|
private function callProvider(AiProviderConfig $config, string $prompt): string
|
||||||
|
{
|
||||||
|
$startTime = microtime(true);
|
||||||
|
$apiKey = $config->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()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user