Что это. Сервис, который каждый день сам решает «какой товар куда переставить» внутри складов Wildberries, отправляет задачи в кабинет WBcon и следит за их выполнением. CEO не одобряет задачи поштучно — только настраивает защитные правила и смотрит отчёт.
Единственная цель. Поднять индекс локализации с текущих 67% до 90%+. Чем выше локализация — тем дешевле логистика WB, выше выкуп, быстрее доставка.
Где живёт. Сервер timeweb (cron + Python). Данные — в нашей Supabase. Кабинет — в WBcon. Сводки — в Telegram @wookiee_alerts_bot.
Проблема, которую решаем. У Wookiee 920+ артикулов лежат на 23 рабочих складах WB по всей России. Когда покупатель из Новосибирска заказывает товар, который физически лежит в Коледино — WB везёт его 5 дней через всю страну и берёт с нас за это повышенный коэффициент тарифа (КТР) и сниженный коэффициент региональной приоритезации (КРП). Покупатель ждёт долго → выкуп падает. Карточка не попадает в ТОП регионального поиска → продаж меньше.
Что делает сервис. Каждый день автоматически считает: «вот этот товар лучше переставить из Коледино в Новосибирск, потому что там его всё равно покупают». Отправляет такие задачи в WBcon. Следит, чтобы за неделю индекс локализации (доля заказов с ближайшего склада) рос.
Контроль возвращает данные в алгоритм: какие пары (склад A → склад B) выполняются успешно, какие WB отклоняет, какие зависают. Это та самая «калибровка по факту», которая будет в Sub-project B.
Сервис общается с тремя внешними системами. Понимать кто кому хозяин — критично, иначе непонятно почему мы не можем просто «прийти и взять».
| Система | Чья | Что оттуда нужно | Можем ли писать? |
|---|---|---|---|
| Wildberries (сам маркетплейс) | WB | Источник всех данных. WB не отдаёт нам данные напрямую — только через свой API (медленный и ограниченный). | Только через стандартный API селлера. Перестановки — отдельно через WBcon. |
| Wildberries DB (89.23.119.253) | Сторонний подрядчик | Готовая агрегированная база: остатки по складам, заказы, движения, история. Подрядчик сам ETL-ит данные из WB и держит свою копию. | ТОЛЬКО ЧТЕНИЕ Подрядчик нам не разрешает писать. |
| WBcon (сторонний сервис) | Сторонний сервис, наш кабинет | Это система, через которую WB разрешает делать перестановки. Звонит на телефон при создании задачи. У нас 2 номера-кабинета: ООО ВУКИ + ИП Медведева. | ЧТЕНИЕ + ЗАПИСЬ Поставить новую задачу, удалить старую, посмотреть статус. |
| Supabase (наша) | Наша | Наш «мозг» — здесь хранятся снимки WBcon, итоги расчётов алгоритма, аудит всех POSTов, история индекса. | ЧТЕНИЕ + ЗАПИСЬ Полный контроль. |
Что это. Доля заказов, которые ушли с ближайшего к покупателю склада. Если покупатель из Новосибирска купил товар, который физически лежал в Новосибирске — это +1 к локализации. Если приехал с Коледино — 0.
Зачем поднимать.
Как сейчас. Колеблется около 67% последнюю неделю. Цель — 90%+. Каждая точка процента индекса = сотни тысяч рублей в месяц (точная цифра — задача Sub-project C).
| Дата | Индекс | SKU в выборке | Что произошло |
|---|---|---|---|
| 22 мая | 67.11% | 275 | Расширенная выборка |
| 23 мая | 67.28% | 201 | Перешли на core-выборку |
| 26 мая | 67.22% | 201 | Первый live-батч 483 POST (вручную) |
| 27 мая | 67.19% | 201 | |
| 28 мая | 67.16% | 201 | Cron запущен · 164 POST автономно |
Важно Колебание ±0.1 п.п. за неделю — это шум, не сигнал. Решения смотрим на горизонте 14–30 дней. Автопилот стартовал вчера.
Каждый день сервис проходит один и тот же цикл из 6 этапов. Все времена — по московскому времени.
Это сердце сервиса. Объясняю по шагам — как для человека, не как для разработчика.
Для каждой пары (SKU + размер + склад-донор + склад-получатель) сервис задаёт вопрос: «может, стоит переставить?». На 920 артикулах × ~6 размерах × 23 склада × 23 склада это потенциально миллион пар. Большинство отсеивается сразу — нет товара на доноре, нет смысла везти на получателя, не хватает квоты направления.
После первичного отсева остаётся 700–2500 живых кандидатов в день (зависит от того, сколько SKU «съехало» с целевой локализации за сутки).
Главная формула:
rank = feasibility × max(0, impact_рублей) × (1 + priority_bonus)
Три компонента:
«Доедет ли задача?» Произведение четырёх вероятностей:
policy_mult — пара складов разрешена? (1.0 если auto, 0.5 если manual, 0 если blocked)quota_stability — направление стабильное в истории?quota_fit — наша задача помещается в дневную квоту WB?success_rate — историческая успешность пары (по умолчанию 0.85, пока wb2_pair_outcomes не накопил)«Сколько мы заработаем/сэкономим, если этот SKU переедет?» Считается так:
current_loc (где сейчас процент локализации этого SKU)projected_loc (где будет, если переставим планируемое qty)«Насколько срочно тащить именно этот SKU вверх?» Плавный приоритет (без жёстких порогов):
Пример: SKU на 42% → bonus 2.28. На 88% → 0.02.
Это вторая защитная сетка: даже отранжированных кандидатов алгоритм разделяет по «дорожкам» в зависимости от текущей локализации SKU и приоритетности направления WB:
| Дорожка | Условие | Что значит |
|---|---|---|
| A | SKU <60% локализация + направление tier=high | Самое приоритетное — критично низкая локализация на горячем направлении |
| B | SKU 60–75% + tier=high | Подтянуть «середняка» на горячем направлении |
| C | SKU <60% + tier=medium | Низкая локализация на среднем направлении (~90% сегодняшних кандидатов сюда) |
| D | SKU 75–90% + tier=high | Дотянуть «хорошо» до «отлично» на горячем направлении |
| E | SKU 60–75% + tier=medium | Средний приоритет |
| F | Заблокирован / низкое направление / уже >90% | Не предлагаем |
В POST идут отсортированные по: (1) Новосибирск в конец (там высокая конкуренция за квоту, не блокируем остальные направления), (2) по убыванию rank-score. Топ-N (сейчас 500) реально отправляется в WBcon.
| Что | Где лежит | Кто меняет |
|---|---|---|
| Веса формулы (0.30 локализация, 0.20 выручка, …) | Код scoring.py | Будущий Sub-project B — автокалибровка |
| Пороги lanes (60%, 75%, 90%) | Код planner.py | Sub-project B |
| Список заблокированных складов | БД wbcon_constraints | CEO — UPDATE в БД |
| Cooldown 72ч | БД wbcon_constraints | CEO |
| Дневные лимиты (per-SKU 400, total 5000) | Код sync.py | CEO через обсуждение → правит код |
| candidate_limit=500 | Код sync.py | CEO — защитная заглушка первого дня |
Каждый день одна и та же воронка из 6 ступеней. Цифры в иллюстрации — типичные, в реальности зависят от дня (плюс-минус 30%).
| # | Ступень | Что отсевается и почему |
|---|---|---|
| 1 | Построение пар | Алгоритм не рассматривает пары: где на доноре нет товара, где донор и получатель совпадают, где у получателя уже хороший остаток, где направление закрыто WB. |
| 2 | Scoring + lane F | Кандидаты с заблокированными парами (politicy=blocked / direction_tier=low) уходят в lane F и не доходят до POST. |
| 3 | candidate_limit=500 | Жёсткая константа в sync.py:177. «Защитная заглушка первого дня». Берёт топ-500 по rank. Когда автопилот докажет стабильность — поднимем до 1500–2000. |
| 4 | Дневные лимиты | Не больше 400 шт одной модели в день (через все пары складов). Не больше 5000 шт всего. Лишнее переносится «на завтра» (но завтра алгоритм пересчитывает с нуля — это просто верхний потолок). |
| 5 | chrt_id resolve | Чтобы POST в WBcon прошёл, нужен chrt_id (внутренний ID размера в WBcon). 3 источника: колонка в БД (пока NULL), live-запрос в WBcon stocks (rate-limited), исторический кэш из wb2_task_outcomes (растёт сам). Сегодня покрытие ~35%, через 2 недели должно быть 70–90%. |
| 6 | POST в WBcon | Что осталось — реально летит в кабинет. Каждый запрос аудируется в wb2_task_outcomes: что отправили, что ответ, в какое время. Дальше задачу подхватывает WB и кладёт на исполнение склада. |
17 таблиц в Supabase. Делятся на 4 функциональные группы. Для каждой ключевой таблицы — что лежит, какие колонки, пример строки, какие срезы можно делать.
| Таблица | Что хранит | Строк |
|---|---|---|
wbcon_warehouses | Справочник 29 складов WB с их WBcon-ID | 29 |
wbcon_task_snapshots | Активные задачи WBcon — снимок на каждый цикл | ~2 500 |
wbcon_in_transit_snapshots | Товар «в пути» по парам складов | ~700 |
wbcon_quota_snapshots | Квоты направлений WBcon (сколько примет каждый склад) | ~200 |
wbcon_direction_snapshots | Снимки разрешённых направлений | ~1 500 |
wbcon_constraints | Защитные правила (см. §08) | 12 |
wbcon_task_snapshotsАктивные задачи WBcon. Колонки:
external_task_id — ID задачи в WBcon (например, 845)
normalized_status — нормализованный статус (active / in_transit / no_response)
nm_id, chrt_id — что за товар и размер
size_name — название размера ("S", "M")
src_id, dst_id — ID складов донора и получателя
src_name, dst_name — их названия
qty — сколько штук просим переставить
first_seen_at, last_seen_at — когда впервые увидели в WBcon и когда последний раз
Срезы которые можно делать:
dst_name='Краснодар' AND normalized_status='active'first_seen_at < now() - 7d AND normalized_status = 'no_response'SUM(qty) GROUP BY dst_name='Новосибирск'wbcon_in_transit_snapshotsТовар, который уже отправили, но ещё не доехал. Алгоритм использует это в формуле эффективного остатка (см. §05). Без этой таблицы он бы предлагал переставить одно и то же снова и снова.
| Таблица | Что хранит | Строк |
|---|---|---|
wb2_runs | 1 строка = 1 прогон алгоритма. Дата, статус, итоги в JSON. | ~10–15 в день |
wb2_candidate_moves | Каждый кандидат на перестановку с полной диагностикой scoring | 5 000+ |
wb2_rule_sets | Настройки алгоритма (cadence, thresholds) | 2 (1 active) |
wb2_candidate_movesЗдесь лежат ВСЕ кандидаты которые алгоритм когда-либо рассматривал — и принятые, и отброшенные. Это и есть «черный ящик» алгоритма наружу. Колонки:
id — уникальный ID кандидата
run_id — к какому прогону относится (FK на wb2_runs)
candidate_key — уникальный ключ (cabinet|article|size|src|dst)
article, nm_id, chrt_id, size_name — что за товар
src_warehouse_name, dst_warehouse_name — пара складов
lane — A/B/C/D/E/F (см. §05)
rank_score — итоговый ранг (feasibility × impact × priority)
expected_success_probability — вероятность что доедет
expected_localization_gain — на сколько вырастет локализация SKU (рубли/месяц)
score_features_json — ПОЛНАЯ диагностика scoring (см. ниже)
decision — selected / rejected / backlog / cooldown
planned_qty — сколько штук планируем переставить
decision_reason — почему такое решение
created_at — когда посчитан
Внутри score_features_json лежит scoring_diag — расшифровка каждого числа:
{
"impact": {
"price": 2122.12, // цена единицы
"current_loc": 48.9, // текущая локализация SKU
"projected_loc": 50.4, // прогнозируемая после перестановки
"monthly_orders": 45.2, // сколько заказов в месяц
"ktr_now": 1.2, "ktr_new": 1.1,
"krp_now": 2.05, "krp_new": 2.05,
"impact_ktr_rub": 361.0, // экономия логистики, ₽/мес
"impact_krp_rub": 0.0 // дополнительная выручка от КРП, ₽/мес
},
"feasibility": {
"policy_mult": 1.0, // разрешено
"quota_fit": 1.0, // помещаемся
"success_rate": 0.85, // историческая
"pair_history": [0, 0] // [успехов, попыток] — пока пусто
},
"priority": {
"projected_loc_pct": 100.0,
"bonus_primary": 0.0,
"bonus_secondary": 0.0
}
}
Срезы которые можно делать:
ORDER BY rank_score DESC LIMIT 20GROUP BY decision_reasonWHERE lane='A' GROUP BY nm_id HAVING COUNT(DISTINCT run_id::date) >= 5WHERE candidate_key=... ORDER BY created_at| Таблица | Что хранит | Строк |
|---|---|---|
wb2_task_outcomes | Аудит каждой операции POST/DELETE с полным request/response | 1 200+ |
wb2_run_log | Heartbeat + DB-lock каждого cron-этапа | ~10/день |
wb2_localization_history | Дневной снимок индекса локализации по каждому SKU | +201/день |
wb2_task_outcomesКаждая операция (POST новой задачи, DELETE устаревшей) пишет сюда строку. Это наш чёрный ящик — если что-то пошло не так, ищем здесь.
id — уникальный
cabinet_code — 'ooo' или 'ip'
external_task_id — ID задачи в WBcon (для POST=success)
candidate_move_id — FK на wb2_candidate_moves (откуда брали)
submitted_at, resolved_at — времена постановки и завершения
resolution — 'success' / 'unknown' / 'cancelled'
sku_loc_pct_before, sku_loc_pct_after — локализация SKU до/после
metadata_json — request, response, error в JSON
В metadata_json — полная история: что отправили, что получили, какая ошибка. Это и есть тот неявный кэш chrt_id — sync.py при следующих POSTах смотрит сюда «а постили ли мы такой SKU раньше».
Срезы:
GROUP BY (metadata_json->>'op'), resolutionWHERE resolution='wb_rejected'wb2_localization_history+201 строка в день (по SKU из core-выборки). Поля:
snapshot_date — дата
cabinet_code — ooo / ip
nm_id — SKU
loc_pct — индекс локализации в этот день
orders_period — заказы за расчётный период (для веса)
Это и есть та таблица, на которой будут все графики «индекс растёт?». Из неё считается средневзвешенный индекс по кабинету. И именно здесь Sub-project C будет искать каузальную связь «наши действия → рост».
Все живут в wbcon_warehouses. Разбиты по регионам (региональная группировка — для понимания, в БД её пока нет):
Двойники : Питание — отдельный режим WB, в перестановках не используется (заблокирован в защитных правилах).
wbcon_constraints«Не навреди» — главный принцип. Правила версионируются (effective_from / effective_to), менять = политическое решение, не эксперимент алгоритма.
| Правило | Значение | На человеческом |
|---|---|---|
blocked_to_warehouses |
Коледино, Подольск, СПБ Шушары, Шушары, Казань, Питание | Перегружены — никогда не отправляем сюда товар, даже если по математике выгодно |
blocked_from_warehouses |
Екатеринбург, Владимир, Новосемейкино, Самара, Пенза, Подольск | С этих складов забирать через автомат нельзя — пойдут на ручной разбор |
high_competition_warehouses |
Новосибирск, Екатеринбург, СПБ, Казань | Эти склады нарасхват — режем квоту, не лезем впереди других продавцов |
cooldown_hours_72 |
72 часа | Если вчера предложили пару (SKU + склад-донор) — следующие 3 суток повторно не предложим |
no_response_reserve_hours_23 |
23 часа | Задачи без ответа считаем «возможно едут» 23 часа после создания (потом списываем) |
in_transit_confidence_0_9 |
0.9 | Товар в пути считаем как 90% — 10% риск что не доедет (отмены, потери) |
safe_rate_per_phone_per_second_3 |
3 req/s | Безопасная скорость наших запросов к WBcon на номер |
rate_limit_429_threshold_4_5 |
4.5 req/s | Выше этого WBcon начнёт отдавать 429 (rate limit) |
default_daily_capacity_units_300 |
300 шт | Дневной потолок единиц для одного направления (когда квота направления неизвестна) |
default_capture_factor_0_6 |
0.6 | Можем занять до 60% квоты направления одной задачей — остальное оставляем другим |
wb2_rule_setsАктивный набор: phase-a-default. Это не правила WB, а наши тайминги:
{
"cycle_evening_msk": "22:00", ← вечерний расчёт по МСК
"cycle_morning_check_msk": "07:00", ← утренняя перепроверка
"cycle_post_msk": "09:00", ← окно WBcon открывается
"cycle_tracker_msk": ["12:00","15:00","18:00","21:00"],
"cycle_digest_msk": "21:30", ← вечерний дайджест
"morning_check_drift_threshold": 0.2, ← молчим если < 20% drift
"morning_check_replan_if_invalid_pct_gt": 0.5, ← URGENT если > 50%
"digest_threshold_tech_fail_pct": 20, ← порог tech-fail для дайджеста
"digest_threshold_wb_reject_pct": 5, ← порог отказов WB
"duplicate_alert_window_hours": 24, ← окно дедупа URGENT
"lock_ttl_minutes": 60, ← макс длительность DB-lock
"hanging_task_ttl_days": 7 ← когда списать «зависшую» задачу
}
Бот: @wookiee_alerts_bot (общий с другими сервисами Wookiee). 6 типов сообщений, переписаны на нормальный русский, без жаргона.
Файл /etc/cron.d/wb-loc-2 на сервере timeweb. TZ=Europe/Moscow в шапке файла.
| Время МСК | Этап | Cron expression |
|---|---|---|
| 22:00 | Вечер — построить план | 0 22 * * * |
| 07:00 | Утренняя перепроверка | 0 7 * * * |
| 09:00 | POST batch | 0 9 * * * |
| 12:00, 15:00, 18:00, 21:00 | Трекер | 0 12,15,18,21 * * * |
| 21:30 | Дайджест | 30 21 * * * |
| каждый час :30 | Сторож (cleanup + heartbeat) | 30 * * * * |
Сейчас Запущен только кабинет ООО ВУКИ. Кабинет ИП Медведева — на паузе по решению CEO.
WBcon ужесточил лимит на эндпоинт остатков — пустые ответы при превышении 4 req/window.
Обход: исторический кэш chrt_id. Сегодня ~35% покрытия, растёт с каждым POST.
Колонка есть (миграция 013), но планировщик пока её не заполняет — резолюция на POST-этапе.
План: в evening-этапе подмешивать chrt_id из кэша.
«Защитная заглушка» в sync.py. Сейчас режет ~282 кандидата из плана.
План: поднять до 1500–2000 после 2–3 успешных автономных циклов.
Cron работает только для ООО.
Когда включать: по решению CEO.
Считает «процент невалидных» — без разбора по причинам.
План: разложить причины в Sub-project B.
Не отвечаем на вопрос «индекс вырос благодаря нам или сам».
План: Sub-project C — отдельный движок атрибуции.
| Фаза | Что добавляет |
|---|---|
| B · Pair statistics | Таблица wb2_pair_outcomes копит «сколько раз пара (склад A→B) завершалась успехом». Алгоритм начинает учитывать реальные success-rate, а не догадку 0.85. |
| C · Causal attribution | Раскладывает прирост индекса по причинам: вклад наших действий vs алгоритм WB vs сезонность. Отвечает на главный вопрос «работает ли сервис». |
| D · LLM agents | Еженедельный обзор по pair_outcomes. Утренний дайджест CEO на человеческом языке («что произошло вчера, на что обратить внимание»). |
| E · Auto-stop | Логика «когда хватит». Если индекс на плато 14 дней — снижаем интенсивность. Если ROI падает — стоп. |
Макеты экранов вынесены в отдельный документ — это позволяет показывать паспорт сервиса (этот файл) отдельно от прототипа UI. Макеты собраны 1:1 на дизайн-системе настоящего Wookiee Hub (stone-палитра, DM Sans + Instrument Serif, двухуровневая боковая навигация, те же карточки и таблицы что и в каталоге).
В интерфейсе менеджер видит:
Прототип UI → wb-loc-2-hub-ui.pages.dev