feat(автоподбор): шаг1 — проводка живого findCompetitors за флагом autopodbor.real_find
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Autopodbor\Agent\Fetch;
|
||||
|
||||
use Symfony\Component\Process\Exception\ProcessTimedOutException;
|
||||
use Symfony\Component\Process\Process;
|
||||
|
||||
/**
|
||||
* Живая добыча страниц для поиска конкурентов (§12.2 движка v4): 2ГИС — через xfetch (обход
|
||||
* антибота); Яндекс — тоже через xfetch, а при пустом ответе fallback на ЛОКАЛЬНЫЙ Playwright
|
||||
* (бесплатный, проверенный рендер) через scripts/render-page.cjs. Прочие домены не грузим
|
||||
* (поиск конкурентов ходит только в справочники). Любая ошибка → '' (как контракт PageFetcher).
|
||||
*/
|
||||
final class LivePageFetcher implements PageFetcher
|
||||
{
|
||||
public function __construct(
|
||||
private readonly XfetchClient $xfetch,
|
||||
private readonly string $nodeBin = 'node',
|
||||
private readonly string $renderScript = 'scripts/render-page.cjs',
|
||||
private readonly int $renderTimeoutSec = 90,
|
||||
) {}
|
||||
|
||||
public function html(string $url): string
|
||||
{
|
||||
if (str_contains($url, '2gis.ru')) {
|
||||
return $this->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 '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Живой движок поиска конкурентов (шаг 1)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| true → findCompetitors использует НАСТОЯЩИЙ движок (ниша → поиск 2ГИС/Яндекс →
|
||||
| резолв → сборка). false → демо-заглушка FakeCompetitorAgent. По умолчанию ВЫКЛ —
|
||||
| включается осознанно (локально/за тумблером), т.к. ходит в живые сервисы.
|
||||
|
|
||||
*/
|
||||
'real_find' => env('AUTOPODBOR_REAL_FIND', false),
|
||||
];
|
||||
Reference in New Issue
Block a user