Чат-мости
Чат-міст робить зовнішній чат-продукт — сьогодні це Slack — повноцінним шляхом до робочого простору Control Center. Згадайте бота — і прокинеться агент; дайте відповідь у гілці Slack — і розмова продовжиться; введіть slash-команду — і створиться тікет. А під цим — ті самі простори, транскрипти, тікети й атрибуція, якими користується застосунок для комп’ютера. Чат — це поверхня робочого простору, а не його копія.
Єдиний провайдер, який постачається, — Slack. Реалізацій Discord, Telegram, Teams чи Matrix у продукті немає ніде. Цю функцію навмисно написано без прив’язки до провайдера: додати другий продукт — це одне значення enum, один адаптер і один плагін, — але сьогодні «чат» означає Slack.
Цю можливість охоплюють дві поверхні, і належать вони різним людям. Підключення робочого простору до Slack і налаштування бота — це дії адміністратора в розділі Налаштування → Робочий простір → Загальні. Прив’язка власного облікового запису в чаті до вашої особи в Control Center — дія учасника в розділі Налаштування → Робочий простір → Профіль і ідентичність.
Усю цю функцію визначають три рішення: у якому напрямку встановлюється з’єднання, що дає право повідомленню, яке надходить ззовні, і де дозволено зберігати знання, специфічні для продукту.
Вихідне з’єднання за задумом
Розділ із назвою «Вихідне з’єднання за задумом»Міст ніколи не слухає провайдера. Зі Slack міст з’єднується через Socket Mode: сервер запитує у Slack URL сокета й сам встановлює вихідне з’єднання, а потім отримує через нього згадки, особисті повідомлення та slash-команди.
Саме наслідок тут і є суттю. Ноутбук за NAT, домашній сервер без домену, машина, що переміщається між мережами, — усі вони можуть працювати з мостом без вхідного порту, без тунелю, без сертифіката TLS і без публічного URL, який доводилося б оновлювати, коли він змінюється. Немає ендпоінта webhook, який треба захищати, бо ендпоінта webhook немає взагалі. Коли Control Center формує визначення застосунку, він жорстко задає Socket Mode і прибирає будь-який request URL, бо застосунок, налаштований на обидва варіанти, працював би наполовину.
Ціна цього — з’єднання, яке треба підтримувати живим, і це робота сервера: перепідключення виконується з експоненційним збільшенням затримки та джиттером і верхньою межею 30 секунд, ротація сокета за ініціативою провайдера перепідключається, не нарощуючи цю затримку, а відхилені облікові дані — це остаточна відмова: нескінченні спроби з відкликаним токеном дали б лише обмеження частоти, тож міст зупиняється й чекає на нові токени, дослівно показуючи в налаштуваннях власну помилку провайдера.
Робочий простір без чат-застосунку не коштує нічого: ні сокета, ні таймерів, ні трафіку. І все це запускається після банера готовності сервера, бо чат-застосунок, який повільно відповідає, ніколи не повинен затримувати завантаження.
Членство дає право, а не володіння токеном
Розділ із назвою «Членство дає право, а не володіння токеном»Той, хто встановив застосунок, — не той, хто надсилає повідомлення. Тому кожна вхідна подія, перш ніж щось станеться, зводиться до Principal, і за найменшої невідповідності запит відхиляється з поясненням:
- Неприв’язаному обліковому запису в чаті пояснюють, як виконати прив’язку, у тій гілці Slack, де він написав.
- Прив’язаному обліковому запису, користувач якого не є учасником цього робочого простору, радять попросити в адміністратора запрошення.
- Учаснику, чия роль доступна лише для читання (
viewer,guest), повідомляють, що його роль не дозволяє запускати роботу.
Жоден із цих випадків не продовжується. Повідомлення з чату ніколи не приписується тому, хто його не надсилав, — саме це зберігає чесними журнал аудиту, обмеження частоти для кожного Principal і авторів тікетів. Модель членства, на яку все це спирається, описано в розділі Спільна робота — ідентичність, членство та присутність.
Прив’язка — це зіставлення особи, а не надання доступу. Членство перевіряється заново на кожній події, тож прив’язка того, кого згодом вилучили, дає відмову, а не його колишні привілеї. Прив’язку можна встановити двома шляхами: короткоживучий одноразовий код, який учасник вводить у чаті, або автоматичне зіставлення за підтвердженою адресою електронної пошти в провайдера — і шлях через пошту все одно вимагає, щоб зіставлений користувач уже був учасником, тож стороння особа, чия адреса випадково є на сервері, не прив’язується ні до чого.
Коди навмисно тримають у пам’яті з часом життя 15 хвилин. Збереження такого коду створило б довговічний рядок, схожий на облікові дані, для того, що має зникнути, а перезапуск сервера, який знецінює всі невикористані коди, — це правильна поведінка, а не регресія: учасник просто натискає «прив’язати» ще раз.
На відмову відповідають у тій гілці Slack, яку відкрило повідомлення, справжньою відповіддю. Відповідь у гілці позначає повідомлення, на яке відповідає, доходить до єдиної людини, якій це потрібно, і при цьому не виносить «ви не прив’язані» в сам канал. До того ж надсилання повідомлення — це єдина можливість, яка є в кожного провайдера, тож пояснення ніколи не залежить від функції, якої застосунок може не мати.
Походження та проблема відлуння
Розділ із назвою «Походження та проблема відлуння»Міст, який дзеркалить в обидва боки, зациклиться, якщо щось не розрізнятиме ці
два напрямки. Кожне повідомлення, яке міст приймає, позначається
metadata['chat'] — провайдер, команда, розмова, гілка, повідомлення й
відправник. Вихідне дзеркало пропускає будь-яке повідомлення з цією позначкою.
Саме це єдине правило робить двостороннє дзеркалення безпечним: повідомлення людини, набране в Control Center, передається назовні в прив’язану гілку (з іменем автора на початку), а повідомлення, що надійшло з чату, зупиняється там, де увійшло.
Назовні проходить навмисно вузький набір. Відповідь агента передається як
звичайний текст чату. Коротка думка може з’явитися в рядку Thinking… картки.
Вивід інструментів — це стіна дифів, і він залишається в Control Center, куди ж
веде й урізана відповідь.
Картка звітує про роботу; текст залишається повідомленням
Розділ із назвою «Картка звітує про роботу; текст залишається повідомленням»Там, де провайдер їх відображає, хід несе одну картку завдання, яку редагують на місці, поки триває хід, і яка стоїть над відповіддю, що її пояснює.
Картка — це один згрупований план, а не зліплений абзац. Slack об’єднує
details і output в одному завданні, тож деталі рядка надсилають один раз
(думку в рядку Thinking…), а output ніколи не передається потоком.
Підготовка — це один рядок, заголовок якого замінюється (Working on it…,
Cloning …). Міркування додає рядок Thinking…. Кожен інструмент — окремий
рядок (Read lib/main.dart). Відповідь — це звичайний текст під карткою:
подання плану в Slack читає його з пізнішого долучення markdown, а не з
фрагмента, домішаного до завдань. І більше нічого: жодного виводу інструментів
і лише аргумент інструмента з короткого списку дозволених, бо аргументи
невідомого інструмента можуть бути цілим тілом файлу, а рядок — це один рядок.
Картка охоплює й очікування перед ходом. Перша згадка створює розмову, а її
створення клонує кожен репозиторій, у якому працюватиме агент, — і на реальному
репозиторії це хвилини, коли агента ще ні про що не спитали, а гілка виглядає
так, ніби бот проігнорував запит. Тож картка відкривається тієї ж миті, коли
згадку прийнято (Working on it…), потім рядок підготовки оновлюється тими
самими словами, які показує застосунок для комп’ютера, — Cloning acme/widgets…,
Setting up Reviewer…, Starting the agent… — далі рядок Thinking…, щойно
агент починає міркувати, а потім по рядку на кожен інструмент. Одна картка, одне
повідомлення — від «я вас почув» до відповіді звичайним текстом під нею. Про
підготовку повідомляють лише там, де її хтось чекає в чаті: розмова, яку готують
у застосунку, або застрягла розмова, відновлена під час запуску сервера,
розгортається взагалі без картки.
Вона несе єдине, чого чат не може дати читачеві: посилання View in Control
Center на розмову за відповіддю. Чат-продукти приймають у посиланні http(s)
і нічого більше, тож кнопка вказує на невелику сторінку, яку віддає цей сервер
і яка перенаправляє браузер на посилання control-center:// застосунку для
комп’ютера. Ця сторінка нічого не читає й нічого не вирішує — ідентифікатори з
її URL передаються в глибоке посилання і більше нікуди, — а сервер без
доступного публічного URL просто створює картку без кнопки, а не зламану.
Картки — така сама можливість, як і будь-яка інша. Провайдер без них поводиться
точно як раніше, лише текстом, а провайдер, який відмовляє від картки під час
виклику, зберігає свій потік: відповідь завжди важливіша за картку, що її
описує. Коли потокову передачу вимкнено повністю, готова картка їде разом з
єдиним надісланим повідомленням. Створений тікет отримує таке саме ставлення:
підтвердження /cc ticket — це завершена картка, єдиний сенс якої — посилання
на щойно створений тікет.
Можливості узгоджуються, а потім запам’ятовуються
Розділ із назвою «Можливості узгоджуються, а потім запам’ятовуються»Чат-продукти різняться тим, що вони вміють відображати, тож міст написано під найбагатшу поверхню — відповідь, що росте наживо в гілці, тимчасовий статус «is thinking…», гілку із заголовком, ефемерну відповідь на slash-команду — і відмовляється від можливостей по одній. Провайдер, у якого немає нічого з цього, все одно працює: відповіді просто приходять одним повідомленням.
Заявлена можливість — це ще не обіцянка. Потокові відповіді Slack потребують функції платного тарифу, і Slack відмовляє лише під час виклику. Відмову, позначену як постійна, запам’ятовують на весь час життя з’єднання: міст перестає питати й відтоді надсилає відповіді цілком, а налаштування прямо про це повідомляють, бо на «мої відповіді перестали йти потоком» має бути відповідь і без читання журналу. Картки завдань відкидаються так само, якщо Slack відмовляє від однієї з них.
Кожна межа належить провайдеру, а не ретранслятору: стеля довжини повідомлення, стеля фрагмента потоку й те, чи потрібен потокові якір гілки, оголошуються адаптером, а довга відповідь розбивається за межею абзацу, рядка чи слова, а не посеред слова.
Один міст, один адаптер на продукт
Розділ із назвою «Один міст, один адаптер на продукт»Усе, що має форму продукту, живе за портом. Загальне ядро знає про markdown,
розмови в чаті, гілки чату й учасників; воно нічого не знає про конверти Slack,
mrkdwn чи gateway intents. Це забезпечують три частини:
- Адаптер нормалізує один продукт: його транспорт, його виклики API, його
текстовий кодек і його особливості конвертів. Особливості Slack повчальні:
згадка в каналі надходить двічі (як
app_mentionі якmessage), а більшість підтипів повідомлень — це оформлення, а не нові запити. І те й те вирішується в адаптері, тож ядро ніколи не дізнається, що Slack доставляє двічі. - Дескриптор — це публічна форма провайдера: які облікові дані йому потрібні, які в них формати, які URL допомагають користувачеві їх знайти, що він уміє і чи можна створити або змінити його застосунок звідси. Картка налаштувань, діалог підключення й перевірка аргументів — усе це генерується з нього: клієнт надсилає назад провайдера та карту з ключами за ідентифікаторами полів.
- Плагін — це серверна композиція: як облікові дані стають перевіреним з’єднанням, як збирається адаптер і як керується застосунок на боці провайдера.
Отже, додавання продукту — це одне значення enum, один адаптер і один плагін. Жодної нової операції RPC, жодної зміни схеми, жодної зміни клієнта — і клієнт залишається чесним, бо поле, про яке він ніколи не чув, усе одно відображається з міткою, яку надав сервер.
Дві деталі не дають абстракції протекти нагору. Кроки керування застосунком, для яких у провайдера немає API (створити токен, установити застосунок, перевстановити після зміни дозволів), повертаються як іменовані кроки з власними посиланнями, тож екран завершення налаштування показує їх, не знаючи, про якого провайдера йдеться. А кожна інструкція, яку пише міст, називає провайдера й ту команду, яку застосунок насправді використовує, тож перейменована команда або другий провайдер читаються правильно без нового рядка.
Де зберігаються облікові дані
Розділ із назвою «Де зберігаються облікові дані»Чат-токени робочого простору лежать у його власному каталозі, поруч із його
базою даних — як звичайний JSON за шляхом
<dataDir>/<workspaceId>/chat_credentials/<provider>.json, не в базі даних і
навмисно не у зв’язці ключів ОС. Два наслідки легко згодом упустити: видалення
робочого простору відв’язує каталог, і токени зникають разом із ним, а експорт
чи резервне копіювання робочого простору копіює workspace.db, тож робочий
простір, переданий комусь іншому, не несе всередині живих токенів бота. Файл
успадковує межу довіри файлової системи хост-сервера: усе, що має доступ на
читання до каталогу даних, може прочитати токен бота, а це та сама межа, за
якою лежать бази даних.
Один файл на провайдера, який записують атомарно й лише для власника: відкликаний застосунок видаляється відв’язуванням одного файлу, а пошкоджений файл коштує одного провайдера, а не всіх. Облікові дані заходять усередину через операцію підключення й ніколи не повертаються назад: читання статусу відповідає лише метаданими з’єднання.
Що навмисно не пропонується
Розділ із назвою «Що навмисно не пропонується»- Структурні налаштування застосунку. Транспорт, області доступу й підписки на події задаються примусово, а не виставляються назовні. Застосунок, який можна налаштувати до повного мовчання з екрана налаштувань, — це звернення до підтримки, а не функція.
- Розмова та slash-команди. Поверхня Slack — це повідомлення, відповіді в гілках і slash-команди.
- Друге джерело істини. Чат — не місце, де зберігається робота. Гілка Slack — це простір, відповідь — транскрипт, тікет — тікет, і все це в робочому просторі, доступне з будь-якого клієнта.
Див. також
Розділ із назвою «Див. також»- Спільна робота — ідентичність, членство та присутність: Principal і ролі, крізь які міст визначає особу
- Робочі простори та ізоляція: чому прив’язки й облікові дані належать робочому простору
- Модель агента: що прокидається, коли надходить згадка
- Тікети та делегування: що створює
/cc ticket - Архітектура: де міст стоїть між сервером і клієнтами
- Підключіть Slack до робочого простору
- Налаштувати інтеграцію Slack
- Прив’язати обліковий запис Slack
- Налаштувати чат-бота
- Розмови в PR: ті самі три рішення, ухвалені для хоста коду