Чат-міст
Провайдери
Розділ із назвою «Провайдери»| Провайдер | Значення в протоколі | Транспорт | Тип простору |
|---|---|---|---|
| Slack | slack |
Socket Mode (вихідний WSS) | slack |
Slack — єдиний провайдер. ChatProvider — це enum з єдиним значенням, і cc_server реєструє один плагін. Схема, кожне звернення й кожен унікальний індекс прив’язані до provider, тож ідентифікатори ніколи не переходять між провайдерами, а додавання нового — це зміна лише на сервері.
Один застосунок на боці провайдера на кожну пару робочий простір × провайдер.
Чат-команди
Розділ із назвою «Чат-команди»Ім’я команди задається для кожного застосунку (cc за замовчуванням, тобто /cc). Адміністратор може його перейменувати; тоді міст використовує те ім’я, з яким команду викликали востаннє.
| Команда | Дія |
|---|---|
/cc help (або /cc без дієслова) |
Перелічує, що вміє бот |
/cc link <code> |
Погашає одноразовий код і прив’язує чат-особу відправника |
/cc ticket <title> |
Створює локальний тікет, про який повідомив прив’язаний учасник |
/cc ticket <title> | <description> |
Те саме, де описом стає все після першого | |
| Невідоме дієслово | Повідомляє невідоме дієслово разом із текстом довідки |
Способи звернутися до агента:
| Жест | Поведінка |
|---|---|
@bot … у каналі |
Відкриває гілку Slack і відповідний простір Control Center; агент відповідає в гілці Slack |
| Відповідь у прив’язаній гілці Slack | Продовжує той самий простір Control Center — згадка не потрібна |
| Звичайне повідомлення в каналі | Ігнорується (не адресоване боту й не в прив’язаній гілці Slack) |
| Особисте повідомлення боту | Один простір Control Center на кожну розмову в особистих повідомленнях; задає заголовок гілки Slack і статус «is thinking…», поки агент працює |
Поля облікових даних (Slack)
Розділ із назвою «Поля облікових даних (Slack)»Їх оголошує дескриптор провайдера — саме він генерує діалог підключення й перевіряє надіслані дані.
| Ідентифікатор поля | Мітка | Обов’язкове | Префікс | Де знаходиться |
|---|---|---|---|---|
botToken |
Bot user OAuth token | Так | xoxb- |
OAuth & Permissions |
appToken |
App-level token | Так | xapp- |
Basic Information → App-Level Tokens, область connections:write |
configRefreshToken |
App configuration refresh token | Ні | xoxe |
App configuration tokens on api.slack.com/apps |
configRefreshToken — це облікові дані для керування застосунком: без них з’єднання працює, але бота не можна створити чи змінити з Control Center. Вони змінюються за кожного використання, і нове значення зберігається негайно.
Значення обрізаються по краях, неоголошені ключі відкидаються, а невідповідність префікса відхиляється з однаковим повідомленням і на клієнті, і на сервері. Обов’язкові поля мають бути присутні за кожного підключення; необов’язкове поле, залишене порожнім, зберігає те, що вже було збережено.
Вимоги до застосунку Slack
Розділ із назвою «Вимоги до застосунку Slack»Області доступу токена бота:
app_mentions:readassistant:writechannels:historychat:writecommandsgroups:historyim:historyusers:readusers:read.emailПідписки бота на події:
app_home_openedapp_mentionmessage.channelsmessage.groupsmessage.imСтруктурні налаштування, які Control Center задає примусово, коли створює або змінює застосунок: увімкнений Socket Mode, відсутній request URL, увімкнена й доступна для запису вкладка Messages, always_online у бота, потрібні області доступу й події, об’єднані з тим, що застосунок уже має (ніколи не віднімаючи), і одна slash-команда з описом File a ticket or link your Control Center account — маркер, за яким в експортованому маніфесті впізнається власна команда Control Center.
Стани з’єднання
Розділ із назвою «Стани з’єднання»| Стан | Значення в протоколі | Що означає |
|---|---|---|
| Не підключено | disconnected |
Для цієї пари робочий простір × провайдер облікові дані не збережено |
| Підключення… | connecting |
Облікові дані збережено, транспорт установлює або перевстановлює з’єднання |
| Наживо | connected |
Транспорт піднято й приймає події |
| Помилка з’єднання | error |
Дані збережено, але з’єднання не працює; поруч показано власну помилку провайдера |
Перепідключення — це експоненційна затримка з джиттером і верхньою межею 30 секунд. Ротація сокета за ініціативою провайдера перепідключається одразу, не нарощуючи затримку. Відхилені облікові дані (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) або відсутня область доступу — це остаточна відмова: міст припиняє повторювати спроби, доки не з’являться нові облікові дані.
Можливості провайдера (Slack)
Розділ із назвою «Можливості провайдера (Slack)»Міст написано під найбагатшу поверхню, і він відмовляється від можливостей по одній.
| Можливість | Slack | Що відбувається за її відсутності |
|---|---|---|
streaming |
так | Уся відповідь надсилається один раз, наприкінці |
streamingRequiresThread |
так | Ціль відповіді без якоря гілки надсилає відповіді цілком |
ephemeralMessages |
так | Відповідь на slash-команду без придатного дескриптора для відповіді відкидається (відмови завжди публікуються в гілці) |
threadStatus |
так | Немає тимчасового рядка «is thinking…» |
threadTitle |
так | Гілка чату зберігає заголовок за замовчуванням |
slashCommands |
так | Немає поверхні /cc |
taskCards |
так | Лише текст — ні картки над відповіддю, ні посилання «View in Control Center» |
maxMessageLength |
39 000 | Довші відповіді урізаються з указівкою на Control Center |
maxStreamChunkLength |
3 800 | Долучення розбиваються на межі абзацу, рядка або пробілу |
Заявлена потокова передача — це ще не обіцяна: Slack потребує для неї функції Agents & AI Apps на платному тарифі. Відмову під час виклику (streaming_not_supported, feature_not_enabled, paid_only, …) запам’ятовують на весь час життя з’єднання й показують як streamingAvailable: false. Картки завдань Slack їдуть у тому самому виклику, тож Slack, який відмовляє від режиму відображення картки, зберігає свій потік і втрачає лише картку — один раз, на весь час життя з’єднання.
Потік також має назвати читача, для якого він призначений. Щоб відкрити потік у каналі, Slack вимагає і користувача-отримувача, і його команду (в особистих повідомленнях — ні) та інакше відмовляє з missing_recipient_user_id / missing_recipient_team_id — це відмова на кожен виклик, тож міст продовжує спроби, а хід тим часом приходить однією надісланою відповіддю. Отримувач — це той, хто запитав, узятий із вхідного повідомлення (його команда, а не команда застосунку, тож канал Slack Connect визначається правильно). Для ходу, який ніхто не починав із чату — роботу запустили в Control Center у вже прив’язаній розмові, — це учасник, чия прив’язка створила міст; якщо він відтоді відв’язав свій чат-акаунт, такий хід надсилається цілком, а не потоком.
Картки завдань
Розділ із назвою «Картки завдань»Одна згрупована картка на кожен хід агента (task_display_mode: plan) з рядком на кожен крок. Кожен рядок — це task_update з id + title + status, плюс одноразові details, коли думка готова. details та output з однаковим id об’єднуються без розділювача, тож details надсилаються один раз, а output ніколи не передається потоком. Заголовок плану — це запит; відповідь — окреме, пізніше долучення з фрагментом markdown_text.
| Частина | Вміст |
|---|---|
| Заголовок плану | Те, що запитали |
| Рядок підготовки | Working on it…, потім Cloning …, потім Starting the agent… — один рядок, заголовок замінюється |
| Рядок міркування | Thinking…, додається, коли агент починає міркувати; думка — це details цього рядка, надіслані один раз, коли міркування завершується |
| Рядки інструментів | По одному на інструмент (Bash which swift), унікальний id, тож вони стоять поруч |
| Статус | У роботі, поки цей рядок виконується; завершено, коли починається наступний рядок або хід закінчується; помилка для невдалих, перерваних і тих, що вперлися в ліміт ходів |
| Заклик до дії | View in Control Center як замикальний рядок, коли хід завершується — відсутній, поки робота ще триває, і відсутній, коли сервер не має доступного публічного URL |
Чого картка не несе ніколи: виводу інструментів, аргументу поза цим списком дозволених або чогось прочитаного з бази даних. Текст міркувань — це лише вміст рядка Thinking… (не довший за 256 символів), а не тіло повідомлення. Картка, яка не сказала б нічого нового, не надсилається повторно. Згадка відкриває потік одразу, щоб гілка Slack не мовчала; хід, який потім не створив жодного тексту, завершує ту саму картку короткою приміткою, а не другим повідомленням. Хід без слів у просторі, який так і не відкрив потік (карток немає або в потоковій передачі вже відмовлено), усе одно надсилає одне повідомлення із завершеною карткою та приміткою.
Картки в потоці й у надісланому повідомленні — це різне корисне навантаження:
| Форма | Структура | Текст |
|---|---|---|
| У потоці | plan_update плюс фрагменти task_update з ключем id |
Звичайний рядок, не довший за 256 символів на фрагмент |
| У повідомленні | Блок plan, що обгортає task_card із ключем task_id |
Форматований текст |
Обробка збоїв:
| Ситуація | Поведінка |
|---|---|
| Провайдер відмовляє в картці під час долучення | Долучення негайно повторюється без неї; текст усе одно надходить наживо, а картки відкидаються на весь час життя з’єднання |
| Провайдер відмовляє і в тексті | Потік закривається, і відповідь надсилається цілком |
| Потік обривається після того, як частина відповіді вже дійшла | Потік закривається, і нічого не надсилається повторно |
Чому корисне навантаження розділено саме так, пояснює розділ Чат-мости.
Підготовка, поки ходу ще немає
Розділ із назвою «Підготовка, поки ходу ще немає»Картка відкривається, коли згадку прийнято, — до клонування й до першого токена; наступний за нею хід переймає ту саму картку й той самий потік. Подальша розповідь приходить із SpaceProvisioningChanged на шині доменних подій — той самий поступ, який банер підготовки застосунку для комп’ютера читає з рядка простору.
| Стан підготовки | Розповідь |
|---|---|
| Згадку прийнято | Working on it… |
Крок repo |
Cloning <repo>… |
Крок prCheckout |
Checking out the pull request in <repo>… |
Крок script |
Running the setup script for <repo>… |
Крок agent |
Setting up <agent>… |
provisioning, кроку ще немає |
Preparing the workspace… |
ready |
Starting the agent… |
| Агент міркує | Thinking… |
failed |
Workspace setup failed — details are in Control Center. |
cancelled |
Workspace setup was stopped — details are in Control Center. |
Розповідь про підготовку вимагає всіх чотирьох умов:
| Умова |
|---|
| Провайдер відображає картки |
| На цьому з’єднанні доступна потокова передача |
| У просторі є чат-запит у процесі виконання |
| Жодна картка вже не звітує там про хід |
Запит залишається в процесі виконання, доки не почнеться хід або не зазнає невдачі підготовка. Подія ready, коли картку ще не відкрито, — це теплий простір: його залишають у спокої, а підготовка, якої ніхто в чаті не чекає — розмова, відкрита в застосунку, або покинута, відновлена під час запуску сервера, — не створює картки взагалі.
Сторінка перенаправлення /open
Розділ із назвою «Сторінка перенаправлення /open»Чат-продукти приймають у посиланні http(s) і нічого більше, тож заклик до дії не може напряму назвати схему застосунку для комп’ютера.
| Властивість | Значення |
|---|---|
| Маршрут | GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id> |
| Джерело | Виводиться з публічного URL сервера (ws → http, wss → https); loopback зберігається |
| Відповідь | Невелика HTML-сторінка з no-store, яка переходить на control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> і показує кнопку для ручного переходу |
| Перевірка | Ідентифікатори мають відповідати [A-Za-z0-9_.-]{1,128}; усе інше — 404 |
| Доступ | Публічна й інертна — бази даних вона не читає, тож не є оракулом існування; авторизацію виконує застосунок, який вона відкриває |
Поля профілю бота
Розділ із назвою «Поля профілю бота»| Поле | Межа | Примітки |
|---|---|---|
appName |
1–35 символів | За замовчуванням Control Center · <workspace name> |
botDisplayName |
1–80 символів | За замовчуванням control-center |
description |
≤ 140 символів | |
agentDescription |
≤ 300 символів | Показується над особистим повідомленням боту |
command |
≤ 32 символів | Нормалізується: провідні слеші прибираються, регістр знижується, [^a-z0-9_-] видаляється, порожнє значення замінюється на cc |
agentEnabled |
— | Чи відкрито досвід особистих повідомлень з агентом |
Порожнє поле означає «залишити як є», а не «очистити». Межі належать Slack, а вони найжорсткіші серед змодельованих продуктів.
Коди прив’язки
Розділ із назвою «Коди прив’язки»| Властивість | Значення |
|---|---|
| Довжина | 6 символів |
| Алфавіт | 23456789ABCDEFGHJKLMNPQRSTVWXYZ (без 0/O, 1/I, U) |
| Час життя | 15 хвилин |
| Використань | Одне |
| Зберігання | У пам’яті — перезапуск сервера знецінює всі невикористані коди |
| Область | Один робочий простір і один провайдер; створення нового коду знецінює попередній код учасника |
Погашення зіставляється без урахування регістру. Невідомі, прострочені коди, коди з чужого робочого простору та коди не від того провайдера — усі навмисно зазнають невдачі однаково нерозрізненно.
Способи прив’язки
Розділ із назвою «Способи прив’язки»| Спосіб | Значення в протоколі | Як |
|---|---|---|
| Код | code |
Учасник погасив /cc link <code> |
email |
Підтверджена адреса електронної пошти провайдера збіглася з користувачем, який уже є учасником робочого простору |
Операції RPC
Розділ із назвою «Операції RPC»Усі вони прив’язані до робочого простору. Сервер не зберігає стану, тож кожен виклик несе власний workspace_id в аргументах; диспетчер відхиляє операцію з областю робочого простору, у якій його пропущено, ще до відкриття будь-якої бази даних. Облікові дані передаються лише всередину — читання статусу ніколи їх не повертає.
| Операція | Вид | Мінімальна роль | Класи дій |
|---|---|---|---|
chat.providers |
читання | guest |
— |
chat.status |
читання | guest |
— |
chat.connect |
зміна | admin |
secretAccess, networkEgress |
chat.disconnect |
руйнівна | admin |
secretAccess |
chat.listUserLinks |
читання | guest |
— |
chat.beginUserLink |
зміна | member |
— |
chat.unlinkUser |
зміна | member для себе, admin для будь-кого іншого |
— |
chat.createApp |
зміна | admin |
secretAccess, networkEgress |
chat.setupLink |
читання | admin |
— |
chat.botProfile |
читання | admin |
networkEgress |
chat.updateBotProfile |
зміна | admin |
networkEgress |
Операції читання успадковують нижню межу guest від свого виду; операції зміни без явної межі вимагають member.
chat.setupLink збирає URL створення застосунку у провайдера з профілю бота. Він не оголошує жодного класу дій, бо не читає секретів і нікуди не звертається — єдиний виклик робить власний браузер користувача.
Поверхню завершує один реактивний запит:
| Запит | Вид | Надсилає |
|---|---|---|
chat.watchUserLinks |
стеження | Список, що пересилається за кожної прив’язки або відв’язки |
Він існує тому, що прив’язка відбувається на іншому боці: учасник вводить одноразовий код у чат із ботом, тож жодна відповідь клієнта не може принести результат назад. І список у налаштуваннях, і відкритий діалог «прив’язати мій обліковий запис» стежать за ним, і діалог перемикається на підтвердження тієї самої миті, коли надходить прив’язка самого учасника.
chat.beginUserLink завжди створює код для власної ідентичності того, хто викликає. chat.updateBotProfile повертає remaining_step, лише коли залишився крок (зазвичай перевстановлення); його відсутність означає, що зміна вже чинна.
chat.providers повертає по одному запису на кожного запропонованого провайдера — дескриптор плюс, коли він є, статус з’єднання. Усе, що відображає інтерфейс налаштувань, береться з цього корисного навантаження, і саме тому додавання провайдера — це зміна лише на сервері.
Зберігання
Розділ із назвою «Зберігання»| Що | Де |
|---|---|
| Облікові дані з’єднання | <dataDir>/<workspaceId>/chat_credentials/<provider>.json |
| Незавершене покрокове налаштування | <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json |
| Застарілі файли Slack | <dataDir>/<workspaceId>/slack_credentials.json і slack_app_setup.json, читаються один раз і переносяться на місці |
Облікові дані лежать поруч із базою даних робочого простору, а не всередині неї: видалення робочого простору забирає з собою його токени чату, а workspace.export чи резервна копія копіює workspace.db — тож робочий простір, переданий комусь іншому, не несе всередині живих токенів бота. Записи атомарні (тимчасовий файл плюс перейменування) і доступні лише власнику.
Таблиці бази даних
Розділ із назвою «Таблиці бази даних»Обидві живуть у базі даних робочого простору, де базовий набір міграцій зведено до v1, — тому читання з чужого робочого простору не компілюється.
chat_space_links — одна зовнішня розмова ↔ один простір Control Center:
| Стовпець | Примітки |
|---|---|
id |
UUID v4 |
workspace_id |
Область робочого простору |
provider |
ChatProvider.wire |
external_team_id |
Команда Slack T…, порожньо для провайдерів без цього поняття |
external_channel_id |
Канал Slack C…, група G…, особисте повідомлення D… |
external_thread_id |
Якір гілки Slack або null, коли якорем є вся розмова (особисті повідомлення боту) |
cc_space_id |
REFERENCES spaces(id) ON DELETE CASCADE |
created_by_user_id, created_at, last_activity_at |
Унікальність за (workspace_id, provider, external_channel_id, external_thread_id), а частковий індекс забезпечує те саме для ідентифікаторів гілок, що дорівнюють null (SQLite вважає NULL різними).
chat_user_links — один зовнішній учасник ↔ один користувач Control Center:
| Стовпець | Примітки |
|---|---|
id |
UUID v4 |
workspace_id |
Область робочого простору |
provider |
ChatProvider.wire |
external_team_id, external_user_id |
Учасник Slack U… |
user_id |
Користувач Control Center (глобальний, тож без зовнішнього ключа) |
method |
code або email |
linked_at |
Унікальність за (workspace_id, provider, external_team_id, external_user_id) і за (workspace_id, provider, user_id) — однозначна атрибуція в обидва боки, при цьому одна людина може бути прив’язана в кількох провайдерів.
Прив’язка — це зіставлення особи, а не надання доступу: членство перевіряється заново на кожній вхідній події, тож прив’язка до учасника, якого відтоді вилучили, приводить до відмови.
Тип простору й метадані повідомлень
Розділ із назвою «Тип простору й метадані повідомлень»Прив’язаний простір створюється з SpaceKind провайдера (slack). Він поводиться як звичайний простір для людей — сповіщення й розділи як у topic — а isExternalChat — це перевірка, крізь яку проходить кожна поведінка (дзеркалення, значки, зберігання).
Кожне повідомлення, яке приносить міст, несе позначку metadata['chat']:
{ "chat": { "provider": "slack", "teamId": "T024BE7LD", "channelId": "C0G9QF9GW", "threadId": "1700000000.000100", "messageId": "1700000000.000200", "userId": "U024BE7LH", "userLabel": "sam" }}Ця сама позначка слугує захистом від відлуння: вихідне дзеркало пропускає будь-яке повідомлення, що її несе, і саме це не дає вхідному повідомленню з чату надіслатися прямо назад.
Поведінка ретрансляції
Розділ із назвою «Поведінка ретрансляції»| Поведінка | Подробиці |
|---|---|
| Що ретранслюється назовні | Лише текст агента плюс рядок статусу картки завдання. Текст міркувань і вивід інструментів залишаються в Control Center |
| Ритм потокової передачі | Обмежений одним скиданням на секунду, поки триває хід; картка й текст, який вона пояснює, їдуть в одному виклику |
| Повідомлення людей, набрані в Control Center | Дзеркаляться в прив’язану гілку з іменем автора на початку |
| Порожні ходи | Повідомляються короткою приміткою (запуск зазнав невдачі, був перерваний або зупинився на своєму ліміті ходів), а не мовчанням |
| Дедуплікація | За власним ідентифікатором події провайдера; пам’ятаються останні 512 |
| Агент на вході | Нова прив’язана розмова будить CEO-агента робочого простору, інакше — його першого агента, інакше — нікого |
| Іменування просторів | #channel · <request> для гілки в каналі Slack, Slack DM · <request> для особистого повідомлення, заголовки не довші за 48 символів |
| Ізоляція збоїв | Одна погана подія ніколи не валить міст; з’єднання продовжує обслуговувати всі інші розмови |
Див. також
Розділ із назвою «Див. також»- Налаштувати інтеграцію Slack
- Прив’язати обліковий запис Slack
- Налаштувати чат-бота
- Чат-мости
- Обмеження — про класи дій, які оголошують операції