docs обзвон: отчёт помощника — постройка, сторожа красными, вырезание, ошибки

Записано по ходу, пока полный прогон идёт. Внутри: почему станцию просим не
ждать конца звонка, почему список знакомых отказов белый, шесть поломок с
дословными словами покрасневших сторожей и слепком возврата, перепись
зовущих глазами с четырьмя ловушками счётчика, и три моих ошибки первыми —
включая ту, где мой же сторож остался бы зелёным при живой дыре.

Раздел про полный прогон дописывается отдельно, когда прогон кончится.
This commit is contained in:
Дмитрий
2026-08-07 13:02:37 +03:00
parent 7fd6b4bb76
commit 1855fe126e
@@ -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 и в
эталоне службы, но исполняет его человек, а не сторож.