diff --git a/docs/superpowers/priyomka/stroyka-8/otchyot-pomoshchnika-nabor-nomera-2026-08-07.md b/docs/superpowers/priyomka/stroyka-8/otchyot-pomoshchnika-nabor-nomera-2026-08-07.md index bb61f376..48deaebe 100644 --- a/docs/superpowers/priyomka/stroyka-8/otchyot-pomoshchnika-nabor-nomera-2026-08-07.md +++ b/docs/superpowers/priyomka/stroyka-8/otchyot-pomoshchnika-nabor-nomera-2026-08-07.md @@ -89,15 +89,177 @@ ## 3. Что построил -*(заполняется по ходу)* +### 3.1. Сама деталь — `NabornikAsterisk` в `bots/lena-golos/priyomnik.py` + +Получив задание, она открывает короткое соединение к служебному входу станции на +`127.0.0.1:5038`, входит по имени и тайному слову и просит начать исходящий +звонок. Соединение тут же закрывается. + +Просьба выглядит так — и каждая строка выбрана, а не взята наугад: + +| строка | значение | зачем именно так | +|---|---|---| +| `Channel` | `Local/<номер>@lena-nabor/n` | набираем **через раздел диалплана**, который уже знает наш ствол и наш определитель номера. Своего номера в коде робота нет вовсе — и не появится | +| `Context` / `Exten` | `lena-inworld` / `s` | снятую трубку станция отдаёт разговорному разделу, где `Answer` и сведение звука с мостом | +| `Async` | `true` | **главное слово.** Станцию просим не ждать конца звонка — см. §1.1 | +| `Timeout` | 60 000 | столько гудков ждёт сама станция | +| `Variable` | `LENA_CALL_ID`, `LENA_ZNAK` | связь звонка с заданием, едет вместе со звонком | + +### 3.2. Три исхода — рубеж проведён буквально + +Рубеж один: **байты просьбы записаны в сокет**. До него любая беда — честное +«звонка не было». После — «не было» говорим **только** по поимённо знакомому +отказу станции (`OTKAZY_DO_NABORA`, белый список), а молчание, обрыв, незнакомая +ошибка и ответ третьего рода дают «не знаю». + +### 3.3. Форма номера проверяется до всякой сети + +Только цифры, 3–20 знаков; знак задания — только буквы, цифры, `-` и `_`. Не +подошло — «звонка не было», и до станции мы даже не стучимся. Зачем — §1.3. + +### 3.4. Почему эта дорога, а не две другие + +Замер коордиатора: служебный вход станции есть, но учётных записей в нём ноль; +веб-вход выключен; папка заданий на звонок нам не по правам. Выбран **служебный +вход**, и вот почему: + +- **веб-вход** пришлось бы включать целиком — лишний открытый порт и лишний + пароль на машине, где рядом крутится боевой «Поиск клиентов»; +- **папка заданий** вовсе не умеет отвечать. Положил файл — и не знаешь, приняла + ли станция задание, отказала или не заметила. Три исхода, на которых стоит вся + защита от второго звонка, там взять неоткуда. Эта дорога не просто неудобна — + она **лжёт**; +- **служебный вход** уже включён, уже слушает только сам себя и на каждую просьбу + отвечает словами. Единственная из трёх, где «принял», «не приму» и «молчу» + различимы. + +Что надо сделать на машине, чтобы дорога открылась, — списком для человека, не +программиста, — написано в `bots/lena-golos/README.md`, раздел «Как открыть +дорогу для набора». Коротко: завести станции **одну** учётную запись для робота с +правом входить и начинать звонки; тайное слово положить отдельным файлом с +правами 600; наружу вход **не** открывать; раскомментировать три строки в +`lena-priyomnik.service`; и только потом — отдельное окно на живую пробу при +владельце. С шага четвёртого робот вправду звонит живым людям и тратит деньги. +🔴 Ничего из этого сегодня не сделано и делать нельзя. + +### 3.5. Выбор детали остался вслух + +`LENA_NABORNIK`: умолчание и `net` — прежний `NabornikNeNastroen`, `stanciya` — +новый набор, любое другое имя роняет запуск с руганью, называющей оба годных +значения. В эталоне службы `lena-priyomnik.service` строки набора написаны и +**нарочно закомментированы**: приёмник поднимется с `net`, то есть звонить не +начнёт, пока их не раскомментируют. + +### 3.6. Связь звонка с заданием — в обе стороны + +По звонку — задание (`Pamyat.zvonok`), по заданию — звонок +(`Pamyat.zvonok_po_znaku`, заведена этой сменой). Та же пара уезжает со звонком +на станцию переменными канала. + +🔴 Попутно нашёл и починил тихую дыру: приёмник передавал набору **сырое тело** +задания, а знак портал вправе прислать одним заголовком, не кладя в тело. Тогда +набору было бы нечем привязать звонок к заданию, и он бы отказал по форме. Теперь +приёмник кладёт разобранный знак явно. Отпечаток тела снимается **до** этого и на +него не влияет — проверено отдельно. + +### 3.7. Кто зовёт написанное мной — поимённо, глазами + +🔴 **Что считает мой прибор.** Я искал по хранилищу поиском по тексту +(`Grep`), а он считает **упоминания**, а не вызовы: в его выдачу попадают и +пояснения, и таблицы README, и объявление самой вещи. Поэтому ниже разобрано +глазами, строка за строкой. + +| что написал | кто вправду зовёт | ловушка | +|---|---|---| +| `NabornikAsterisk` | `sozdat_nabornik()` — единственный в бою; проба дважды (через `sobrat_nabor` и напрямую) | упоминание в шапке файла — не зов | +| `sozdat_nabornik()` | `main()` и `podnyat()` | 🔴 **зов умолчанием довода**: в `podnyat()` он стоит в `nabornik or sozdat_nabornik()`. Проба передаёт свой набор — и этот зов не случается вовсе. Счётчик «сколько раз позвали» показал бы здесь ноль на всех прогонах пробы | +| `prochest_klyuch_ami()` | `NabornikAsterisk.__init__` | 🔴 **зов внутри условия**: только когда ключ не передан. Проба передаёт всегда → в пробе не зовётся **никогда**. Записано в §9 | +| `Pamyat.zvonok_po_znaku()` | сегодня **только проба** | зовущего в бою нет; сказано вслух в README и §9 | +| `_pochemu_nelzya`, `_bez_klyucha`, `_prosba`, `_razobrat`, `_otvet` | все — из `nabrat()`, каждый по разу-двум | `_razobrat` — статический, зовётся через `self` | +| `OTKAZY_DO_NABORA`, `CIFRY`, `ZNAKI_METKI` | **постоянные**, а не вызовы: зовущего у них нет по природе. Читаются в `_pochemu_nelzya` и `nabrat` | счётчик вызовов дал бы ноль и был бы прав, и это ничего не значило бы | +| настройка `LENA_NABORNIK` | читается ровно в одном месте — `sozdat_nabornik()` | 🔴 в эталоне службы её **нет вовсе** (строки закомментированы), то есть на машине она останется `net` | +| переменные канала `LENA_CALL_ID`, `LENA_ZNAK` | 🔴 **зов внутри строки-подстановки** — их не «зовёт» ни одна функция, они уезжают текстом в станцию. Читать их будет мост, которого пока нет | никакой поиск по коду робота их потребителя не найдёт: он на другой стороне звонка | + +### 3.8. Чего НЕ трогал + +`bots/lena-golos/extensions-lena.conf` — узкое горло — **не тронут ни одной +командой**. Оба нужных раздела (`[lena-nabor]`, `[lena-inworld]`) в нём уже были. ## 4. Сторожа и как показывал их красными -*(заполняется по ходу)* +Сторожа — раздел 12 и 13 в `bots/lena-golos/proba-priyomnika.py`. Подставная +станция говорит на языке служебного входа по настоящему сокету, но никуда не +звонит, а **считает, сколько раз её попросили**. Настоящий код набора при этом +работает без единой поблажки. + +Пять поломок, и каждая — своим ножом. + +| что ломал | что сказал сторож | +|---|---| +| молчание станции после просьбы → «звонка не было» вместо «не знаю» | `🔴 Е: станция замолчала после просьбы — исход «НЕ ЗНАЮ»: получено 'ne_nachat', а надо 'ne_znayu'` · `🔴🔴 Е: сказано «звонка не было» — портал повторит и позвонит ВТОРОЙ РАЗ` · и заодно `🔴 Ж` про обрыв. **Красных 3** | +| выключил проверку «только цифры» в номере | `🔴 Л: КОРОТКАЯ отрава с переводом строки: получено 'nachat', а надо 'ne_nachat'` · `🔴🔴 Л: и на короткой отраве станцию не просили звонить: получено 1, а надо 0` · плюс шесть соседних. **Красных 9** | +| выключил **обе** проверки номера — чтобы увидеть дыру целиком | `🔴🔴 Л: станцию НЕ ПРОСИЛИ звонить НИ РАЗУ: получено 2, а надо 0` — одно задание дало станции **две** просьбы о звонке. Дыра настоящая | +| белый список отказов сделал чёрным: любая ошибка → «звонка не было» | `🔴 И: НЕЗНАКОМЫЙ отказ станции — «не знаю», а не «звонка не было»: получено 'ne_nachat', а надо 'ne_znayu'`. **Красных 1** | +| закрытая дорога (вход не пустил) → «не знаю» вместо «звонка не было» — сторож, которого просил коордиатор | `🔴 Г: вход не пустил — исход «звонка ТОЧНО не было»: получено 'ne_znayu', а надо 'ne_nachat'`. **Красных 1** | +| `Async: true` → `false` и выбросил `LENA_ZNAK` | `🔴 А: станцию просим НЕ ЖДАТЬ конца звонка: получено 'false', а надо 'true'` · `🔴 А: со звонком едет знак задания: получено None` · плюс два в разделе 13. **Красных 4** | + +Возврат доказан слепком со снятием невидимых знаков конца строки: + +```text +в коммите: 4a8028034b6c89d03a39a2c650fc0deb726328a8 +на диске : 4a8028034b6c89d03a39a2c650fc0deb726328a8 +``` ## 5. Вырезание второго рода -*(заполняется по ходу)* +**Допущение, на котором стоит защита:** «станция ответила „принял" — значит +звонок пошёл, и второй раз этому человеку звонить нельзя». + +Пять годных по форме случаев мимо этого допущения. + +1. **Станция сказала «принял», а звонок не пошёл — ствол отвалился.** Мы + отвечаем «набор пошёл», и это **правильно**: сказать «не было» значит позвать + портал звонить второй раз. Но плата названа вслух: состояние такого звонка + так и останется «набираем», потому что станцию мы не слушаем. Это §9. +2. **Станция не ответила вовсе, связь оборвалась на середине.** Сторожа Е и Ж, + оба показаны красными. Ответ — «не знаю». +3. **Два задания на один номер почти одновременно.** Проверено (случай В): это + **два разных звонка**, и так и должно быть — защита считает по знаку задания, + а не по номеру. Разные кампании вправе звонить одному человеку. 🔴 Обратная + сторона названа вслух: набор **не** защищает от двойного звонка одному + человеку по двум разным заданиям. Это работа портала + (`ObzvonZapretObrabotki`), и она там есть. +4. **Станция ответила третьим — не «принял» и не «отказ».** Куда попадёт: + в «не знаю». Проверено (случай К) и показано красным через белый список. + Это же покрывает и незнакомую ошибку (случай И). +5. **Кто ещё может начать звонок на этой станции.** Сегодня — **никто**: все три + дороги закрыты, и это ответ про сегодня. + + Как только для нас откроют служебный вход, им сможет пользоваться не только + приёмник. Тогда защиту от двойного звонка охраняет: + + - **охраняет** — всё, что идёт через приёмник: память знаков на диске, + неделимая бронь, отпечаток тела. Два приёмника на одном файле памяти тоже + охраняются — этим занимается сама база; + - **не охраняет** — звонок, начатый мимо приёмника: тем же служебным входом с + теми же именем и словом; с пульта станции вручную (`channel originate` в + разделы `testtrunk`, `lena-nabor`, `from-dasha`); файлом в папке заданий от + имени станции. Ни один из этих путей память знаков не видит и увидеть не + может; + - **не охраняет отдельно** — два приёмника, поднятых с **разными** путями к + файлу памяти: они не увидят друг друга и оба наберут. Путь задаётся + настройкой `LENA_PAMYAT_ZADANIY`, и в эталоне службы он один. + + ⇒ Защита от двойного звонка охраняет **одну дверь из нескольких**, и охраняет + её честно. Остальные двери закрываются не кодом, а тем, кто имеет доступ к + машине. + +🔴 И отдельный случай мимо допущения, которого в задании не было и который я +нашёл сам: **годный по форме номер, внутри которого перевод строки**. Он мимо +всего: приёмник телефонов не разбирает вовсе, портал чистит номер для своих +нужд, а станция послушно исполнит вторую просьбу. Замерено: одно задание давало +станции две просьбы о звонке. Защита проверена **на больном случае** — на самой +отраве, а не на здоровом номере. ## 6. Полный прогон @@ -105,12 +267,78 @@ ## 7. Соседи -*(заполняется по ходу)* +**моего имени нет ни в одном выводе ни одного сторожа** + +**чужие файлы в дереве не тронуты** + +Замерено `git status --short`. Изменёнными числятся мои четыре: +`bots/lena-golos/priyomnik.py`, `bots/lena-golos/proba-priyomnika.py`, +`bots/lena-golos/lena-priyomnik.service`, `bots/lena-golos/README.md` — и мой +отчёт. Остальное изменённое в дереве (`docs/lending-sborka/…`, `package.json`, +`package-lock.json`, `docs/observer/STATUS.md`) было изменено **до начала моей +смены** и в мои коммиты не входило: каждый коммит делался поимённым перечислением +путей и перечитывался через `git show --stat`. + +`bots/mts-telegram-ads/src/runner.js` — чужое столкновение — так и числится +`UU`: ни одной командой не тронут. + +`bots/lena-golos/extensions-lena.conf` — узкое горло трёх задач — не тронут. +Понадобилось бы, если бы разделов `[lena-nabor]` и `[lena-inworld]` в нём не +было; они есть. ## 8. Мои ошибки -*(заполняется по ходу)* +Первыми, как просили. + +1. 🔴 **Мой сторож защиты от подмены был проверен не тем ножом.** Отрава с + переводом строки, которую я написал первой, **длинная** — и её ловила + проверка длины номера, а вовсе не проверка «только цифры». Выключи я проверку + цифр — сторож остался бы **зелёным при живой дыре**. Поймал себя только тем, + что уронил сторожа нарочно и посмотрел, какие именно строки покраснели: две + главные не покраснели. Добавил **короткую** отраву, которая в длину номера + помещается и ловится только проверкой цифр. Пояснение вписано в саму пробу, + чтобы следующий не наступил. +2. 🔴 **Датчик «набор не оставил своих потоков» считал чужие потоки.** Он брал + все потоки процесса подряд, а среди них были потоки самой подставной станции, + доживающие своё после закрытия связи. Датчик краснел **через раз**: три + прогона из шести. Починил не подгонкой числа, а именем: потоки станции теперь + подписаны, и датчик считает только чужие ей. Шестнадцать прогонов подряд + зелёные. +3. **Перехват журнала в двух потоках роняет чужие строки в вывод пробы.** Подмена + общего вывода — вещь на весь процесс. Перенёс перехват **вокруг** обоих + потоков. +4. **Числа в README были уже неверны до меня, и я чуть не оставил их такими.** + Там стояло «81 проверка», а проба давала 108. Исправил и вписал историю числа. ## 9. Что осталось незакрытым -*(заполняется по ходу)* +1. 🔴 **Живого звонка не было, и моё доказательство его не заменяет.** Подставная + станция отвечает так, как живая **обещает** отвечать, а не так, как она вправду + отвечает. Не доказано: что просьба доедет до живой станции; что она поймёт наш + `Local/<номер>@lena-nabor/n`; что звонок дойдёт до живого человека через ствол + Манго; что снятая трубка вправду сведётся с мостом. Это отдельная проба, в + отдельное окно и со словом владельца. +2. 🔴 **Соответствие слов станции проверено чтением, а не станцией.** Белый + список отказов и то, что при «не жди» ошибка приходит только до создания + канала, — моё знание о том, как устроен служебный вход, а не замер. Ошибись я + тут — знакомый отказ, случившийся **после** набора, дал бы человеку второй + звонок. Смягчено тем, что список белый: незнакомое падает в «не знаю». + Проверять это надо на живой станции, в том же окне, что и пункт 1. +3. **Чем кончился звонок, набор не узнаёт.** Состояние звонка остаётся + «набираем» навсегда. Портал, спросив `sostoyanie`, будет видеть «набираем» и + после того, как разговор кончился. Слушать станцию — вторая половина З-0.6. +4. **Дорога на машине закрыта, и открывать её я не стал** — это выкат и слово + владельца. Перечень шагов — §3.4 и README. +5. **`Pamyat.zvonok_po_znaku` сегодня зовёт только проба.** В бою её позовёт + вторая половина З-0.6. Говорю вслух, чтобы это не выглядело работающей связью + там, где её пока никто не использует. +6. **`prochest_klyuch_ami` пробой не проверена**: проба всегда передаёт тайное + слово прямо, и до чтения файла дело не доходит. Чтение файла — тот же приём, + что у ключа двери, который проверен, но это довод по сходству, а не замер. +7. **Один раз за смену проба упала в чужом, доставшемся мне разделе 9** (живой + HTTP на сокете) с `ConnectionAbortedError` — это Windows и старый код, не мой. + За следующие шестнадцать прогонов не повторилось, но я не могу доказать, что + это не повторится. +8. **Тайное слово служебного входа поедет на машину человеком.** Файла с ним в + хранилище нет и быть не должно; правило прав 600 записано в README и в + эталоне службы, но исполняет его человек, а не сторож.