From 48e65d231c2ffeca4fa0f7ecb9c843b6269990c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Tue, 30 Jun 2026 18:26:38 +0300 Subject: [PATCH] =?UTF-8?q?feat(=D0=B0=D0=B2=D1=82=D0=BE=D0=BF=D0=BE=D0=B4?= =?UTF-8?q?=D0=B1=D0=BE=D1=80):=20=D1=88=D0=B0=D0=B31=20=E2=80=94=20=D0=BF?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=BE=D0=B4=D0=BA=D0=B0=20=D0=B6=D0=B8=D0=B2?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20findCompetitors=20=D0=B7=D0=B0=20=D1=84?= =?UTF-8?q?=D0=BB=D0=B0=D0=B3=D0=BE=D0=BC=20autopodbor.real=5Ffind?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LivePageFetcher (2ГИС→xfetch, Яндекс→xfetch+fallback локальный Playwright). Провайдер при config('autopodbor.real_find')=true собирает LiveFindCompetitors (без ИИ-ключа: null-классификатор + нулевой эмбеддер → сырой список без отсева площадок и без %). RealCompetitorAgent.findCompetitors использует живой движок, если подключён, иначе заглушку. Флаг по умолчанию ВЫКЛ — на проде без изменений. Тесты: Автоподбор unit+feature 172/172 (флаг выкл — биндинг цел); Pint чисто. Co-Authored-By: Claude Opus 4.8 --- .../Providers/AutopodborServiceProvider.php | 63 +++++++++++++++++-- .../Agent/Fetch/LivePageFetcher.php | 59 +++++++++++++++++ .../Autopodbor/Agent/RealCompetitorAgent.php | 6 +- app/config/autopodbor.php | 15 +++++ 4 files changed, 135 insertions(+), 8 deletions(-) create mode 100644 app/app/Services/Autopodbor/Agent/Fetch/LivePageFetcher.php create mode 100644 app/config/autopodbor.php diff --git a/app/app/Providers/AutopodborServiceProvider.php b/app/app/Providers/AutopodborServiceProvider.php index 6c3f89df..fa6041ab 100644 --- a/app/app/Providers/AutopodborServiceProvider.php +++ b/app/app/Providers/AutopodborServiceProvider.php @@ -2,24 +2,38 @@ namespace App\Providers; +use App\Services\Autopodbor\Agent\Aggregator\AggregatorClassifier; +use App\Services\Autopodbor\Agent\Aggregator\AggregatorFilter; use App\Services\Autopodbor\Agent\CompetitorAgent; use App\Services\Autopodbor\Agent\FakeCompetitorAgent; use App\Services\Autopodbor\Agent\Fetch\CompositeFetcher; use App\Services\Autopodbor\Agent\Fetch\CurlPlaywrightFetcher; +use App\Services\Autopodbor\Agent\Fetch\LivePageFetcher; use App\Services\Autopodbor\Agent\Fetch\XfetchClient; use App\Services\Autopodbor\Agent\Fetch\XfetchDirectoryFetcher; +use App\Services\Autopodbor\Agent\FindCompetitorsAssembler; +use App\Services\Autopodbor\Agent\LiveFindCompetitors; use App\Services\Autopodbor\Agent\RealCompetitorAgent; +use App\Services\Autopodbor\Agent\Resolve\TwoGisResolver; +use App\Services\Autopodbor\Agent\Resolve\YandexResolver; +use App\Services\Autopodbor\Agent\Search\SearchResultsParser; +use App\Services\Autopodbor\Agent\Similarity\Embedder; +use App\Services\Autopodbor\Agent\Similarity\EmbeddingRelevance; +use App\Services\Autopodbor\AutopodborDedup; +use App\Services\Autopodbor\AutopodborNormalizer; use Illuminate\Support\ServiceProvider; class AutopodborServiceProvider extends ServiceProvider { public function register(): void { - // Шаг 2 (изучение конкурента) — настоящий движок: сайт конкурента берём обычным - // curl + локальный Playwright (бесплатно), справочники 2ГИС/Яндекс — через антибот - // xfetch.ru. Поиск/резолв (шаг 1) ещё не реальны — RealCompetitorAgent делегирует - // их заглушке FakeCompetitorAgent (fallback). Когда шаг 1 будет готов — заменить - // fallback на настоящую реализацию find/resolve. + // Шаг 2 (изучение конкурента) — настоящий движок: сайт конкурента берём обычным curl + + // локальный Playwright, справочники 2ГИС/Яндекс — через антибот xfetch.ru. + // + // Шаг 1 (поиск конкурентов): при включённом флаге autopodbor.real_find подключается ЖИВОЙ + // движок (ниша → поиск 2ГИС/Яндекс → резолв → сборка). Без ИИ-ключа отсев агрегаторов и + // похожесть-% отключены (null-классификатор + нулевой эмбеддер) — выдаётся сырой список. + // Флаг ВЫКЛ → findCompetitors отдаёт демо-заглушку (как раньше). resolveByName — заглушка. $this->app->bind(CompetitorAgent::class, function ($app): CompetitorAgent { $xfetch = new XfetchClient( apiKey: config('services.xfetch.key'), @@ -31,7 +45,44 @@ class AutopodborServiceProvider extends ServiceProvider directoryFetcher: new XfetchDirectoryFetcher($xfetch), ); - return new RealCompetitorAgent($fetcher, new FakeCompetitorAgent); + $liveFind = config('autopodbor.real_find') + ? $this->buildLiveFind($xfetch) + : null; + + return new RealCompetitorAgent($fetcher, new FakeCompetitorAgent, liveFind: $liveFind); }); } + + /** Живой движок поиска шага 1 (канал А). Без ИИ-ключа — без отсева агрегаторов и без %. */ + private function buildLiveFind(XfetchClient $xfetch): LiveFindCompetitors + { + $nullClassifier = new class implements AggregatorClassifier + { + public function isAggregator(string $name, ?string $siteUrl, ?string $description): ?bool + { + return null; // нет ИИ — никого не выкидываем + } + }; + $zeroEmbedder = new class implements Embedder + { + public function embed(array $texts): array + { + return array_map(static fn (): array => [0.0], $texts); + } + }; + + $assembler = new FindCompetitorsAssembler( + new AggregatorFilter($nullClassifier), + new AutopodborDedup(new AutopodborNormalizer), + new EmbeddingRelevance($zeroEmbedder), + ); + + return new LiveFindCompetitors( + new LivePageFetcher($xfetch), + new SearchResultsParser, + new TwoGisResolver, + new YandexResolver, + $assembler, + ); + } } diff --git a/app/app/Services/Autopodbor/Agent/Fetch/LivePageFetcher.php b/app/app/Services/Autopodbor/Agent/Fetch/LivePageFetcher.php new file mode 100644 index 00000000..be508438 --- /dev/null +++ b/app/app/Services/Autopodbor/Agent/Fetch/LivePageFetcher.php @@ -0,0 +1,59 @@ +xfetch->html($url); + } + + if (str_contains($url, 'yandex.')) { + $html = $this->xfetch->html($url); + + return $html !== '' ? $html : $this->renderLocally($url); + } + + return ''; + } + + /** Локальный Playwright-рендер (бесплатный запас для Яндекса). */ + private function renderLocally(string $url): string + { + try { + $process = new Process([$this->nodeBin, base_path($this->renderScript), $url]); + $process->setTimeout($this->renderTimeoutSec); + $process->run(); + + if (! $process->isSuccessful()) { + return ''; + } + + $decoded = json_decode($process->getOutput(), true); + + return is_array($decoded) && isset($decoded['html']) ? (string) $decoded['html'] : ''; + } catch (ProcessTimedOutException|\Throwable) { + return ''; + } + } +} diff --git a/app/app/Services/Autopodbor/Agent/RealCompetitorAgent.php b/app/app/Services/Autopodbor/Agent/RealCompetitorAgent.php index fcfffd7d..9b3875c4 100644 --- a/app/app/Services/Autopodbor/Agent/RealCompetitorAgent.php +++ b/app/app/Services/Autopodbor/Agent/RealCompetitorAgent.php @@ -21,15 +21,17 @@ final class RealCompetitorAgent implements CompetitorAgent { public function __construct( private Fetcher $fetcher, - private CompetitorAgent $fallback, // для find/resolve, пока они не реальны + private CompetitorAgent $fallback, // для resolve, и для find пока не подключён живой private CandidateBuilder $builder = new CandidateBuilder, private SourceAggregator $aggregator = new SourceAggregator, private AutopodborNormalizer $norm = new AutopodborNormalizer, + private ?LiveFindCompetitors $liveFind = null, // живой поиск шага 1 (если подключён за флагом) ) {} public function findCompetitors(FindCompetitorsRequest $r): FindCompetitorsResult { - return $this->fallback->findCompetitors($r); + // Подключён живой движок поиска — используем его; иначе заглушка (демо-данные). + return $this->liveFind?->find($r) ?? $this->fallback->findCompetitors($r); } public function resolveByName(ResolveByNameRequest $r): ResolveByNameResult diff --git a/app/config/autopodbor.php b/app/config/autopodbor.php new file mode 100644 index 00000000..e5443d32 --- /dev/null +++ b/app/config/autopodbor.php @@ -0,0 +1,15 @@ + env('AUTOPODBOR_REAL_FIND', false), +];