Files
portal/app/app/Jobs/Bot/ProcessChatMessageJob.php
T

143 lines
5.9 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Jobs\Bot;
use App\Mail\BotContactRequestMail;
use App\Models\BotDialog;
use App\Services\Bot\BotAnswer;
use App\Services\Bot\BotAnswerService;
use App\Services\Bot\ContactCapture;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
/**
* Оркестратор ответа бота. Очередь `bot` — отдельный worker, чтобы поток лидов
* не задерживал ответы чата (скорость — требование №1). $tries=1: ретраить разговор
* бессмысленно. Реплику клиента пишет ChatController; сюда приходит её номер ($inId),
* чтобы история разговора не включала текущий вопрос дважды.
*
* Эскалация (правило владельца 12.07.2026): живого оператора в чате нет — бот просит
* телефон, и контакт уходит письмом в поддержку.
*/
class ProcessChatMessageJob implements ShouldQueue
{
use Queueable;
public int $timeout = 12;
public int $tries = 1;
public function __construct(
public readonly string $chatId,
public readonly int $inId,
public readonly string $text,
) {
$this->onQueue('bot');
}
/**
* Сколько прошлых реплик отдаём боту как память разговора (5 пар вопрос-ответ).
* Было 3 пары — в живом диалоге бот «забывал» цифру, названную им же в начале
* (прогон диалогами 12.07.2026).
*/
private const HISTORY_LIMIT = 10;
public function handle(): void
{
$startedAt = hrtime(true);
$history = BotDialog::query()
->where('chat_id', $this->chatId)
->where('id', '<', $this->inId)
->orderByDesc('id')
->limit(self::HISTORY_LIMIT)
->get(['direction', 'message'])
->reverse()
->map(fn (BotDialog $d) => [
'role' => $d->direction === 'in' ? 'user' : 'assistant',
'text' => trim((string) preg_replace('/\n*👉 Показать на портале:.*$/su', '', $d->message)),
])
->values()
->all();
// Номер в реплике = человек хочет, чтобы ему перезвонили. Принимаем ВСЕГДА,
// просил бот телефон или нет (прогон диалогами 12.07.2026: контакт терялся).
$answer = ($this->awaitsContact() || app(ContactCapture::class)->extractPhone($this->text) !== null)
? $this->captureContact($history)
: app(BotAnswerService::class)->answer($this->text, $history);
BotDialog::create([
'chat_id' => $this->chatId,
'direction' => 'out',
'message' => $answer->text,
'matched_chunks' => $answer->matchedChunkIds,
'latency_ms' => (int) ((hrtime(true) - $startedAt) / 1_000_000),
'escalated' => $answer->escalate,
'created_at' => now(),
]);
}
/** Ждём ли мы от клиента телефон: последний ответ бота был просьбой оставить номер. */
private function awaitsContact(): bool
{
$lastOut = BotDialog::query()
->where('chat_id', $this->chatId)
->where('direction', 'out')
->where('id', '<', $this->inId)
->latest('id')
->value('message');
return is_string($lastOut) && str_contains($lastOut, 'номер телефона');
}
/**
* @param list<array{role: string, text: string}> $history
*/
private function captureContact(array $history = []): BotAnswer
{
$phone = app(ContactCapture::class)->extractPhone($this->text);
if ($phone === null) {
// Клиент передумал и задал новый вопрос — отвечаем С ПАМЯТЬЮ разговора.
return app(BotAnswerService::class)->answer($this->text, $history);
}
$rows = BotDialog::query()
->where('chat_id', $this->chatId)
->orderBy('id')
->get(['direction', 'message', 'source', 'user_id']);
$question = $rows
->filter(fn (BotDialog $d) => $d->direction === 'in')
->slice(0, -1)
->last()?->message ?? $this->text;
$transcript = $rows
->map(fn (BotDialog $d) => ($d->direction === 'in' ? 'Клиент: ' : 'Бот: ').$d->message)
->all();
$client = $rows->firstWhere(fn (BotDialog $d) => $d->direction === 'in');
$source = (string) ($client->source ?? 'portal');
$userId = $client->user_id !== null ? (int) $client->user_id : null;
$to = (string) config('services.support.email');
if ($to !== '') {
// Письмо — В ОЧЕРЕДЬ. Прогон 12.07.2026: почта не ответила → упала вся задача
// (tries=1), и клиент, оставивший номер, не получил НИЧЕГО.
try {
Mail::to($to)->queue(new BotContactRequestMail($phone, $question, $this->chatId, $transcript, $source, $userId));
} catch (\Throwable $e) {
Log::error('Не удалось поставить в очередь письмо с контактом', [
'chat_id' => $this->chatId,
'error' => $e->getMessage(),
]);
}
}
return new BotAnswer(BotAnswerService::CONTACT_TAKEN_TEXT, escalate: true);
}
}