feat(автоподбор): второй справочник канала А — Яндекс.Карты через локальный Playwright + слияние
Firecrawl Яндекс.Карты не рендерит (0-2 орг) — по §12.2 Яндекс берём локальным Playwright. render-yandex-list.cjs скроллит ленту результатов → 113 орг за ~18с (быстрее xfetch-2ГИС). YandexDirectory (граница) + PlaywrightYandexDirectory (живой, Process→node). Яндекс = имя+карточка (сайта в списке нет — только на карточке, не открываем). Оркестратор: канал А = 2ГИС(сайт)+Яндекс, слияние (mergeCompetitors union-find) схлопывает одного конкурента из обоих справочников в одну карточку с двумя directory_urls; сайт из 2ГИС. Провайдер подключает живой Яндекс. listingHtml → общий хелпер tests/Pest.php. Модуль 136 unit + 74 feature зелёные. За флагом; на проде не меняется. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -100,6 +100,28 @@ function stubPages(array $byNeedle): PageFetcher
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Разметка списка категории 2ГИС для тестов канала А: фирмы [[id,name,site?],...].
|
||||
* Общий хелпер (используют CategoryScraperTest и LiveFindCompetitorsTest).
|
||||
*
|
||||
* @param array<int, array{0:int|string,1:string,2?:?string}> $firms
|
||||
*/
|
||||
function listingHtml(array $firms): string
|
||||
{
|
||||
$h = '';
|
||||
foreach ($firms as $f) {
|
||||
$id = $f[0];
|
||||
$name = $f[1];
|
||||
$site = $f[2] ?? null;
|
||||
$h .= '<a href="/krasnoyarsk/firm/'.$id.'?stat=X" class="_1rehek"><span class="_lvwrwt"><span>'.$name.'</span></span></a>';
|
||||
if ($site !== null) {
|
||||
$h .= '{"caption":"Перейти на сайт","url":"https://link.2gis.ru/e/project7/'.$id.'/null/H?http://'.$site.'/"}';
|
||||
}
|
||||
}
|
||||
|
||||
return $h;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link a Лидерра-project to a supplier_project via the M:N pivot
|
||||
* (Plan 1 model). Post-Plan-2 LeadRouter eligibility queries the pivot
|
||||
|
||||
@@ -27,22 +27,7 @@ final class MapFetcher implements PageFetcher
|
||||
}
|
||||
}
|
||||
|
||||
/** Разметка списка категории 2ГИС: фирмы [[id,name,site?],...]. */
|
||||
function listingHtml(array $firms): string
|
||||
{
|
||||
$h = '';
|
||||
foreach ($firms as $f) {
|
||||
$id = $f[0];
|
||||
$name = $f[1];
|
||||
$site = $f[2] ?? null;
|
||||
$h .= '<a href="/krasnoyarsk/firm/'.$id.'?stat=X" class="_1rehek"><span class="_lvwrwt"><span>'.$name.'</span></span></a>';
|
||||
if ($site !== null) {
|
||||
$h .= '{"caption":"Перейти на сайт","url":"https://link.2gis.ru/e/project7/'.$id.'/null/H?http://'.$site.'/"}';
|
||||
}
|
||||
}
|
||||
|
||||
return $h;
|
||||
}
|
||||
// listingHtml() — общий хелпер в tests/Pest.php.
|
||||
|
||||
it('мульти-запрос × пагинация: имя+карточка+сайт со всех страниц, дедуп между запросами', function () {
|
||||
// id фирм — реалистично длинные (как у 2ГИС: 985690700540003)
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Services\Autopodbor\Agent\Aggregator\AggregatorFilter;
|
||||
use App\Services\Autopodbor\Agent\ChannelA\CategoryListingParser;
|
||||
use App\Services\Autopodbor\Agent\ChannelA\CategoryScraper;
|
||||
use App\Services\Autopodbor\Agent\ChannelA\QueryAnalyzer;
|
||||
use App\Services\Autopodbor\Agent\ChannelA\YandexDirectory;
|
||||
use App\Services\Autopodbor\Agent\ChannelB\ChannelBSearch;
|
||||
use App\Services\Autopodbor\Agent\ChannelB\ExaSiteFinder;
|
||||
use App\Services\Autopodbor\Agent\ChannelB\ResearcherClient;
|
||||
@@ -77,11 +78,25 @@ function noAiAssembler(): FindCompetitorsAssembler
|
||||
);
|
||||
}
|
||||
|
||||
function leanEngine($pages, ResearcherClient $researcher, HttpFactory $http, array $queries): LiveFindCompetitors
|
||||
function fakeYandex(array $rows): YandexDirectory
|
||||
{
|
||||
return new class($rows) implements YandexDirectory
|
||||
{
|
||||
public function __construct(private array $rows) {}
|
||||
|
||||
public function collect(string $city, array $queries): array
|
||||
{
|
||||
return $this->rows;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function leanEngine($pages, ResearcherClient $researcher, HttpFactory $http, array $queries, array $yandexRows = []): LiveFindCompetitors
|
||||
{
|
||||
return new LiveFindCompetitors(
|
||||
fakeAnalyzer($queries),
|
||||
new CategoryScraper($pages, new CategoryListingParser, maxPages: 2),
|
||||
fakeYandex($yandexRows),
|
||||
new ChannelBSearch($researcher, new ResearcherParser),
|
||||
new ExaSiteFinder($http),
|
||||
noAiAssembler(),
|
||||
@@ -118,6 +133,34 @@ it('сквозной: канал А (2ГИС-список: имя+карточк
|
||||
->and($cm['site_url'])->toBe('carmoney.ru');
|
||||
});
|
||||
|
||||
it('слияние 2ГИС+Яндекс: один конкурент в обоих справочниках → одна карточка с двумя ссылками', function () {
|
||||
Config::set('services.exa.key', '');
|
||||
$http = app(HttpFactory::class);
|
||||
|
||||
$pages = stubPages([
|
||||
'2gis.ru/krasnoyarsk/search/' => listingHtml([[985690700540003, 'КрасЛомбард', 'kraslombard24.ru']]),
|
||||
]);
|
||||
// Яндекс дал того же КрасЛомбарда (без сайта) + новую фирму, которой нет в 2ГИС
|
||||
$yandex = [
|
||||
['name' => 'КрасЛомбард', 'card_url' => 'https://yandex.ru/maps/org/kraslombard/175852236692'],
|
||||
['name' => 'ЯрКомиссионка', 'card_url' => 'https://yandex.ru/maps/org/yarkomissionka/226908207223'],
|
||||
];
|
||||
|
||||
$res = leanEngine($pages, fakeResearcher(['[]']), $http, ['ломбард'], $yandex)
|
||||
->find(new FindCompetitorsRequest(29, [], ['ломбард', 'lkomega.ru'], false, 20));
|
||||
|
||||
$names = array_column($res->competitors, 'name');
|
||||
// КрасЛомбард — один раз (2ГИС+Яндекс слиты), плюс новая из Яндекса
|
||||
expect(array_count_values($names)['КрасЛомбард'])->toBe(1)
|
||||
->and($names)->toContain('ЯрКомиссионка');
|
||||
|
||||
// у слитого КрасЛомбарда — обе ссылки-справочники и сайт из 2ГИС
|
||||
$kl = collect($res->competitors)->firstWhere('name', 'КрасЛомбард');
|
||||
expect($kl['site_url'])->toBe('kraslombard24.ru')
|
||||
->and(collect($kl['directory_urls'])->contains(fn ($u) => str_contains($u, '2gis.ru')))->toBeTrue()
|
||||
->and(collect($kl['directory_urls'])->contains(fn ($u) => str_contains($u, 'yandex.ru')))->toBeTrue();
|
||||
});
|
||||
|
||||
it('канал В: имя без сайта в EXA выкидывается (нет якоря)', function () {
|
||||
Config::set('services.exa.key', 'k');
|
||||
$http = app(HttpFactory::class);
|
||||
|
||||
Reference in New Issue
Block a user