Сохранить порядок дополнительных Compose-файлов при обновлении и подключать их помощнику на чтение. Добавить проверки override, постоянных каталогов dev-данных и документацию проекта. Явно задать русский язык в тестах русских подписей.
config.appearance подключения: цвет #RRGGBB, положение, размер 48/56/64,
форма и иконки кнопки, свой CSS до 10 КБ. Из CSS вырезаются @import,
внешние url() и expression. Публичная конфигурация отдаёт appearance,
config.accent продолжает работать; конфигурация не кэшируется, лоадер —
не дольше 5 минут.
config.fields WEB-подключения проверяется при сохранении: формат и
уникальность ключа, запрет смены ключа (по служебному id поля), название
до 60 символов, тип из списка, значения только у списка, не больше 30
полей, ключи name/email/phone заняты. Ошибки — через t(). Публичная
конфигурация виджета отдаёт схему без aiVisible.
POST /api/v1/organizations/{id}/integrations/{id}/assets/ принимает SVG или
PNG до 256 КБ (PNG не меньше 96×96) и возвращает { url }. SVG очищается до
сохранения; файл лежит в хранилище организации на диске или в S3 и
отдаётся виджету без сессии по непредсказуемому UUID.
На экранах до 480px лоадер раскрывает панель на весь экран без
скруглений и прячет круглую кнопку, пока панель открыта: иначе она
висит поверх поля ввода. Кнопку «Развернуть окно» панель не показывает —
разворачивать некуда. Ширину экрана iframe не видит, поэтому режим
сообщает лоадер (chatballs-chat-layout), а панель после монтирования
сама его запрашивает.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Раздел «Настройки → Шаблоны ответов»: список по образцу «Групп», создание
кнопкой в шапке раздела, правка и удаление окнами, как у интеграций. Без права
на настройки список только читается.
В тексте шаблона можно использовать {{client_name}}, {{operator_name}} и
{{company}}; кнопка «Вставить переменную» ставит код в позицию курсора.
Композер подставляет значения при выборе шаблона. Переменная без значения
остаётся в тексте, над полем — предупреждение, отправка заблокирована. Гость
виджета распознаётся по подписи сессии (isGuest в карточке диалога), его
«Гость · код» за имя не считается. Сервер отклоняет неизвестные переменные.
Переименование шаблона в занятое название теперь отвечает 409, а не падает
на ограничении базы. Логика шаблонов вынесена из Composer в
useComposerTemplates и ComposerTemplatesMenu.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Повторы одного вызова теперь считаются одним логическим сбоем, поэтому предохранитель не открывается посреди второго сообщения. Изменение runtime-конфигурации и успешная проверка увеличивают ревизию интеграции: каждый event-worker заменяет открытый breaker при следующем запросе.
Удаление используемого провайдера возвращает 409 с понятной причиной и не пишет ложное событие об успешном удалении. Добавлены миграция и регрессионные тесты.
Первый кадр силуэта рисовался раньше, чем создавался слой, на котором он
живёт. Обращение к нему падало, и остаток обработчика не выполнялся: окно
появлялось рывком, а кнопка оставалась со знаком агента вместо шеврона —
свернуть чат было нечем. Со второго раза слой уже существовал, и всё
работало, поэтому дефект был виден только при первом открытии.
Слой создаётся вместе с панелью, до первого кадра.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Иконка приложения и два hero-изображения ни на что не ссылались: ни сборка,
ни README, ни документация их не читают. Удалены владельцем.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Виджет переложен по образцу, который дал владелец.
Лента: имя и аватар агента стоят над пузырём, у пузыря не скруглён верхний
угол со стороны автора — «хвостик». Аватар агента — знак робота на круге
цвета акцента; он же стоит в шапке вместо буквы, пока у виджета нет способа
получить логотип компании, и он же на кнопке чата вместо прежней подписи.
Пока ответ считается, в ленте висит «печатает» с бегущими точками.
Поле ввода: без рамки, скрепка слева, справа запись голосового, которая
сменяется отправкой, как только клиент начал печатать. При росте поля кнопки
держатся верхней строки. Подсказки убраны. Подвал — одного тона с лентой,
внизу «Работает на Chatballs» со ссылкой на сайт.
В шапке появилась кнопка разворота: окно становится почти вдвое шире и
немного выше. Размер держит лоадер на сайте-хосте — панель живёт в iframe и
сама себя не растянет.
Появление и исчезновение — эффект джина, как в доке macOS: окно вытягивается,
изгибается горловиной и втягивается в кнопку. Аффинными преобразованиями
такое не делается, поэтому силуэт считается контуром SVG покадрово, тем же
контуром режется контейнер панели, а само окно внутри получает матрицу,
построенную по этому силуэту, — вместе с окном деформируется и переписка.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ход AI выполнялся прямо в приёме сообщения: цикл опроса мессенджеров и
HTTP-запрос виджета ждали провайдера, держа открытой транзакцию организации.
Один ход — это два обращения к модели (эмбеддинг и чат) по тридцать секунд с
двумя повторами, то есть до трёх минут, и всё это время ни одно входящее по
всей установке не забиралось. Владелец видел это как «бот залипает»: сайт и
MAX на одном агенте отвечали с задержками или молчали.
Приём теперь доводит дело до записи сообщения и ставит событие
`conversation.ai_turn_requested`. Ход считает роль событий воркера
(`run_worker --role=events`) короткими транзакциями, между которыми остаются
походы к провайдеру и в мессенджер. Туда же уехала расшифровка голосовых —
последнее обращение наружу из цикла опроса.
Воркер разделён на роли: `poller` опрашивает подключения и ведёт периодические
работы (один экземпляр — курсоры и паузы после сбоя живут в его памяти),
`events` разбирает outbox и масштабируется репликами (`CHATBALLS_EVENT_WORKERS`,
по умолчанию две). Роль `all` осталась для разработки.
Очередь событий научилась двум вещам: события одного диалога не выдаются
параллельно (иначе два ответа приезжают клиенту вперемешку) и событие,
взятое упавшим процессом, возвращается в очередь по истечении аренды.
Попутно убраны мины, которые тот же залип и продлевали:
- ход клиенту ограничен своим таймаутом (CHATBALLS_AI_TURN_TIMEOUT, 20 с)
и сроком годности (CHATBALLS_AI_TURN_DEADLINE_SECONDS, 120 с) — просроченный
ход не зовёт модель, а передаёт диалог оператору;
- отказ провайдера по существу запроса (4xx, кроме 429) больше не повторяется
трижды по таймауту;
- предохранитель провайдера считает сбои по ключу «организация + интеграция»,
а не один на процесс: отозванный ключ одной организации гасил AI у всех;
- потолок паузы после сбоя опроса — минута вместо четверти часа: он был
компромиссом ради журнала однопоточного воркера.
Виджет узнаёт, что ответ считается, по признаку `thinking` в ленте.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Мастер первого запуска и «Настройки» отбрасывали порт из адреса. На
установке, опубликованной как ip:8081, ссылки на файлы, звонки, приглашения
и сброс пароля уходили без порта и вели в пустоту.
Порт хранится отдельно от хоста (InstanceSettings.public_port): по хосту
проверяются входящие Host, строятся домены порталов и адреса TURN, и порт им
чужой. В ссылки он добавляется, когда не совпадает с портом схемы. В поле
адреса его вписывают через двоеточие; смена одного порта прежний адрес не
сдвигает.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Модель получала последние 20 сообщений диалога — число было зашито в
ingest.py, и поменять его можно было только правкой кода, которую стирало
первое же обновление. Длинному разговору 20 мало, локальной модели с
маленьким окном — бывает много.
Теперь это поле «Контекст диалога, сообщений» в блоке «Модель» карточки
агента: от 1 до 200, по умолчанию 20. Проверочный чат карточки видит то же
окно, что и живой диалог.
Попутно история выбирается в базе с конца и с ограничением: раньше диалог
целиком поднимался в память ради последних сообщений.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Шлюз передавал дальше Host без порта ({host} в Caddy, $host в nginx). Браузер
при этом шлёт Origin с портом, и CSRF-проверка Django отвергала любой POST на
установке, опубликованной как ip:8081: «Origin checking failed».
Теперь Host уходит с портом. X-Forwarded-Proto и X-Forwarded-For Caddy
принимает от прокси из частных сетей — установку часто ставят за прокси
панели, который снимает TLS и ходит к шлюзу по http; без этого CSRF падал бы
снова, как только перед установкой появлялся https.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Прочтением считалось только нажатие на строку в шторке. Сотрудник мог уже
отвечать клиенту, а оклик «клиент ждёт» по этому же диалогу продолжал висеть
непрочитанным и держать счётчик в шапке. Счётчик переставал что-либо значить:
он звал туда, где человек уже был.
Теперь при открытии диалога гасятся все уведомления о нём — не только те,
что клиент успел загрузить в шторку: в неё помещается полсотни последних, и
гасить по загруженному списку означало бы оставлять хвост. Отбор идёт по
адресу уведомления, включая маршруты, которыми на диалог ссылались старые
строки в базе.
Отметка прочтения теперь шлёт сотруднику событие: счётчик живёт в шапке
каждой открытой вкладки, и без события они разъезжались до следующего опроса.
Проверено: тесты уведомлений (гасится только свой диалог, гасятся все его
уведомления, старый маршрут узнаётся, нечисловой идентификатор отклоняется),
рассылка событий, каталоги переводов, ruff, проверка типов рабочего места.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Строка списка — flex, и её ячейки по умолчанию растягиваются на всю высоту.
Обёртка аватара растягивалась вместе с ними, а точка режима привязана к её
низу — в строке с метками или таймером ожидания обёртка вырастала с 38 до 62
пикселей, и точка уезжала под аватар на четверть сантиметра. Чем больше меток
у диалога, тем дальше.
Обёртка прижата к началу поперечной оси и снова ростом с аватар, поэтому
точка стоит в его углу независимо от того, что показано в строке ниже.
Проверено на короткой строке и на строке с меткой группы и таймером: высота
обёртки 38 в обеих, точка на 1 пиксель за краем аватара.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Канал ВКонтакте появился, а марки у него не было: в ленте диалогов и в
карточке контакта рисовался общий пузырь сообщения, а в строке логина —
значок отправки, который для Telegram выглядел уместно только по
совпадению. Клиент из ВКонтакте выглядел пришедшим непонятно откуда.
Теперь `ChannelGlyph` знает ВКонтакте и отдаёт тот же официальный
монохромный глиф, что стоит на плитке подключения, а строка логина берёт
марку своего канала вместо общего значка: Telegram, MAX и ВКонтакте —
каждый свой.
Проверено: глифы на 13-18px в тёмной теме рядом с Telegram и MAX, лента
диалогов, строка логина и плитки каналов в карточке контакта; tsc.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>