143 lines
5.9 KiB
PHP
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);
|
|
}
|
|
}
|