Punte de chat
Provideri
Secțiune intitulată „Provideri”| Provider | Valoare serializată | Transport | Tip de spațiu |
|---|---|---|---|
| Slack | slack |
Socket Mode (WSS spre exterior) | slack |
Slack este singurul provider. ChatProvider este un enum cu o singură valoare, iar cc_server înregistrează un singur plugin. Schema, fiecare căutare și fiecare index unic au cheia provider, așa că id-urile nu trec niciodată de la un provider la altul, iar adăugarea unuia este o schimbare doar pe server.
O aplicație de partea providerului per spațiu de lucru × provider.
Comenzi de chat
Secțiune intitulată „Comenzi de chat”Numele comenzii este propriu fiecărei aplicații (cc implicit, deci /cc). Un admin îl poate redenumi; puntea folosește apoi numele cu care a fost invocată ultima dată.
| Comandă | Efect |
|---|---|
/cc help (sau /cc fără verb) |
Listează ce poate face botul |
/cc link <code> |
Valorifică un cod de unică folosință și leagă identitatea de chat a expeditorului |
/cc ticket <title> |
Înregistrează un tichet local raportat de membrul legat |
/cc ticket <title> | <description> |
La fel, cu tot ce urmează după primul | ca descriere |
| Verb necunoscut | Raportează verbul necunoscut plus textul de ajutor |
Modalități de a ajunge la un agent:
| Gest | Comportament |
|---|---|
@bot … într-un canal |
Deschide un fir Slack și un spațiu Control Center corespunzător; agentul răspunde în firul Slack |
| Răspuns într-un fir Slack legat | Continuă același spațiu Control Center — nu este nevoie de mențiune |
| Mesaj obișnuit într-un canal | Ignorat (nu este adresat botului și nu se află într-un fir Slack legat) |
| DM către bot | Un spațiu Control Center per conversație DM; setează un titlu de fir Slack și un status „is thinking…” cât timp lucrează agentul |
Câmpuri de credențiale (Slack)
Secțiune intitulată „Câmpuri de credențiale (Slack)”Declarate de descriptorul providerului, care este cel ce generează dialogul de conectare și validează trimiterea.
| Id de câmp | Etichetă | Obligatoriu | Prefix | Unde se află |
|---|---|---|---|---|
botToken |
Token OAuth al utilizatorului bot | Da | xoxb- |
OAuth & Permissions |
appToken |
Token la nivel de aplicație | Da | xapp- |
Basic Information → App-Level Tokens, scope connections:write |
configRefreshToken |
Token de reîmprospătare a configurației aplicației | Nu | xoxe |
App configuration tokens on api.slack.com/apps |
configRefreshToken este credențialul de administrare a aplicației: fără el conexiunea funcționează, dar botul nu poate fi creat sau editat din Control Center. Se rotește la fiecare utilizare, iar valoarea rotită este stocată imediat.
Valorilor li se taie spațiile albe de la capete, cheile nedeclarate sunt eliminate, iar un prefix care nu se potrivește este refuzat cu aceeași frază pe client și pe server. Câmpurile obligatorii trebuie să fie prezente la fiecare conectare; un câmp opțional lăsat gol păstrează ce era deja stocat.
Cerințe pentru aplicația Slack
Secțiune intitulată „Cerințe pentru aplicația Slack”Scope-urile tokenului de bot:
app_mentions:readassistant:writechannels:historychat:writecommandsgroups:historyim:historyusers:readusers:read.emailAbonamentele la evenimente ale botului:
app_home_openedapp_mentionmessage.channelsmessage.groupsmessage.imSetările structurale pe care Control Center le impune când creează sau editează aplicația: Socket Mode activat, niciun URL de cerere, fila Messages activată și cu drept de scriere, botul always_online, scope-urile și evenimentele necesare fuzionate cu tot ce are deja aplicația (niciodată scăzute) și o comandă slash care poartă descrierea File a ticket or link your Control Center account — markerul ce identifică propria comandă a Control Center într-un manifest exportat.
Stări de conexiune
Secțiune intitulată „Stări de conexiune”| Stare | Valoare serializată | Semnificație |
|---|---|---|
| Neconectat | disconnected |
Nu există credențiale stocate pentru acest spațiu de lucru × provider |
| Se conectează… | connecting |
Credențiale stocate, transportul se conectează sau se reconectează |
| Activ | connected |
Transportul este pornit și primește evenimente |
| Eroare de conexiune | error |
Stocate, dar în eșec; eroarea proprie a providerului este afișată alături |
Reconectarea folosește un backoff exponențial cu jitter, plafonat la 30 de secunde. O rotire de socket inițiată de provider se reconectează prompt, fără să escaladeze backoff-ul. Un credențial respins (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) sau un scope lipsă este definitiv: puntea nu mai reîncearcă până când nu sosesc credențiale noi.
Capabilități ale providerului (Slack)
Secțiune intitulată „Capabilități ale providerului (Slack)”Puntea este scrisă pentru cea mai bogată suprafață și degradează o capabilitate pe rând.
| Capabilitate | Slack | Efectul în lipsa ei |
|---|---|---|
streaming |
da | Întregul răspuns este postat o dată, la final |
streamingRequiresThread |
da | O țintă de răspuns fără punct de ancorare în fir postează răspunsuri întregi |
ephemeralMessages |
da | Un răspuns la o comandă slash fără un identificator de răspuns utilizabil este aruncat (refuzurile sunt postate mereu în fir) |
threadStatus |
da | Nicio linie temporară „is thinking…” |
threadTitle |
da | Firul de chat își păstrează titlul implicit |
slashCommands |
da | Nicio suprafață /cc |
taskCards |
da | Doar text — niciun card deasupra răspunsului și niciun link „View in Control Center” |
maxMessageLength |
39.000 | Răspunsurile mai lungi sunt trunchiate, cu o trimitere către Control Center |
maxStreamChunkLength |
3.800 | Adăugările sunt împărțite la granița dintre paragrafe, linii sau spații |
A anunța streaming nu înseamnă a-l promite: cel al Slack are nevoie de funcția Agents & AI Apps pe un plan plătit. Un refuz din momentul apelului (streaming_not_supported, feature_not_enabled, paid_only, …) este reținut pentru întreaga viață a conexiunii și expus ca streamingAvailable: false. Cardurile de sarcină ale Slack călătoresc în același apel, așa că un Slack care refuză modul de afișare al cardului își păstrează fluxul și pierde doar cardul, o singură dată, pentru întreaga viață a conexiunii.
Un flux trebuie să numească și cititorul căruia îi este destinat. Slack cere atât un utilizator destinatar, cât și echipa acestuia pentru a deschide unul într-un canal (un DM nu este suficient) și refuză cu missing_recipient_user_id / missing_recipient_team_id în caz contrar — un refuz per apel, așa că puntea continuă să încerce, iar între timp tura sosește ca un singur răspuns postat. Destinatarul este cine a întrebat, luat din mesajul de intrare (echipa acestuia, nu a aplicației, așa că un canal Slack Connect se rezolvă corect). Pentru o tură pe care nimeni nu a pornit-o din chat — muncă lansată în Control Center pe o conversație deja legată — acesta este membrul a cărui legătură a creat puntea; dacă între timp și-a dezlegat contul de chat, acea tură este postată întreagă, nu transmisă în flux.
Carduri de sarcină
Secțiune intitulată „Carduri de sarcină”Un card grupat per tură de agent (task_display_mode: plan), cu un rând per pas. Fiecare rând este un task_update cu id + title + status, plus un details trimis o singură dată când gândul este gata. details și output cu același id se concatenează fără separator, așa că details sunt trimise o singură dată, iar output nu este niciodată transmis în flux. Titlul planului este cererea; răspunsul este o adăugare separată, ulterioară, care poartă un fragment markdown_text.
| Parte | Conținut |
|---|---|
| Titlul planului | Ce s-a cerut |
| Rândul de configurare | Working on it…, apoi Cloning …, apoi Starting the agent… — un singur rând, cu titlul înlocuit |
| Rândul de gândire | Thinking…, adăugat când agentul începe să raționeze; gândul sunt details ale acestui rând, trimise o singură dată când raționamentul se termină |
| Rândurile de instrumente | Unul per instrument (Bash which swift), cu id unic, așa că stau unul lângă altul |
| Stare | În curs cât rulează acel rând; complet când începe rândul următor sau se termină tura; eroare pentru eșec, întrerupere și limita de ture |
| Îndemn la acțiune | View in Control Center ca rând final când se termină tura — absent cât timp munca încă rulează și absent când serverul nu are un URL public accesibil |
Ce nu poartă niciodată un card: ieșirea instrumentelor, un argument din afara acelei liste de valori permise sau orice citit din baza de date. Textul raționamentului sunt doar details ale rândului Thinking… (plafonate la 256 de caractere), nu corpul mesajului. Un card care nu ar spune nimic nou nu este retrimis. O mențiune deschide fluxul imediat, ca firul Slack să nu rămână mut; o tură care apoi nu a produs niciun text încheie același card cu o notă scurtă, în loc să posteze un al doilea mesaj. O tură fără cuvinte pe un spațiu care nu a deschis niciodată un flux (fără carduri sau cu streamingul deja refuzat) postează totuși un mesaj care poartă cardul încheiat și nota.
Cardurile transmise în flux și cele postate sunt încărcături utile diferite:
| Formă | Structură | Text |
|---|---|---|
| Transmis în flux | plan_update plus fragmente task_update indexate după id |
Șir simplu, plafonat la 256 de caractere per fragment |
| Postat | Un bloc plan care înfășoară task_card-uri indexate după task_id |
Text îmbogățit |
Gestionarea eșecurilor:
| Situație | Comportament |
|---|---|
| Providerul refuză cardul la o adăugare | Adăugarea este reîncercată imediat fără el; textul sosește în continuare în direct, iar cardurile sunt eliminate pentru întreaga viață a conexiunii |
| Providerul refuză și textul | Fluxul este închis, iar răspunsul este postat întreg |
| Fluxul se rupe după ce o parte a răspunsului a ajuns | Fluxul este închis și nimic nu este repostat |
Vezi Punți de chat pentru motivul pentru care încărcătura utilă este împărțită astfel.
Configurarea inițială, înainte de a exista o tură
Secțiune intitulată „Configurarea inițială, înainte de a exista o tură”Cardul se deschide când o mențiune este acceptată, înainte de clonare și înainte de primul token; tura care urmează adoptă același card și același flux. Narațiunea de după vine de la SpaceProvisioningChanged de pe bus-ul de evenimente de domeniu — exact același progres pe care bannerul de pregătire al aplicației de desktop îl citește de pe rândul spațiului.
| Stare de pregătire | Narațiune |
|---|---|
| Mențiune acceptată | Working on it… |
Pasul repo |
Cloning <repo>… |
Pasul prCheckout |
Checking out the pull request in <repo>… |
Pasul script |
Running the setup script for <repo>… |
Pasul agent |
Setting up <agent>… |
provisioning, încă niciun pas |
Preparing the workspace… |
ready |
Starting the agent… |
| Agentul raționează | Thinking… |
failed |
Workspace setup failed — details are in Control Center. |
cancelled |
Workspace setup was stopped — details are in Control Center. |
Narațiunea de configurare cere toate cele patru condiții:
| Condiție |
|---|
| Providerul afișează carduri |
| Streamingul este disponibil pe această conexiune |
| Spațiul are o cerere de chat în curs |
| Niciun card nu raportează deja o tură acolo |
Cererea rămâne în curs până când începe o tură sau configurarea eșuează. Un eveniment ready fără niciun card deja deschis este un spațiu deja cald și este lăsat în tăcere, iar o pregătire pe care nimeni din chat nu o așteaptă — o conversație deschisă în aplicație, una rămasă în pană, reluată la pornire — nu produce niciun card.
Pagina de redirecționare /open
Secțiune intitulată „Pagina de redirecționare /open”Produsele de chat acceptă http(s) într-un link și nimic altceva, așa că un îndemn la acțiune nu poate numi direct schema aplicației de desktop.
| Proprietate | Valoare |
|---|---|
| Rută | GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id> |
| Origine | Derivată din URL-ul public al serverului (ws → http, wss → https); adresa de loopback este păstrată |
| Răspuns | O pagină HTML mică, fără stocare, care navighează către control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> și afișează un buton manual |
| Validare | Id-urile trebuie să corespundă lui [A-Za-z0-9_.-]{1,128}; orice altceva este un 404 |
| Acces | Publică și inertă — nu citește nicio bază de date, deci nu este un oracol de existență; aplicația pe care o deschide face autorizarea |
Câmpuri de profil ale botului
Secțiune intitulată „Câmpuri de profil ale botului”| Câmp | Limită | Note |
|---|---|---|
appName |
1–35 caractere | Implicit: Control Center · <workspace name> |
botDisplayName |
1–80 caractere | Implicit: control-center |
description |
≤ 140 caractere | |
agentDescription |
≤ 300 caractere | Afișat deasupra unui DM cu botul |
command |
≤ 32 caractere | Normalizat: barele oblice de la început sunt eliminate, textul este trecut în litere mici, [^a-z0-9_-] este eliminat, iar dacă rămâne gol se folosește cc |
agentEnabled |
— | Dacă experiența de DM/agent este expusă |
O casetă goală înseamnă „las-o așa”, nu „golește-o”. Limitele sunt ale Slack, cele mai strânse dintre produsele modelate.
Coduri de legare
Secțiune intitulată „Coduri de legare”| Proprietate | Valoare |
|---|---|
| Lungime | 6 caractere |
| Alfabet | 23456789ABCDEFGHJKLMNPQRSTVWXYZ (fără 0/O, 1/I, U) |
| Durată de viață | 15 minute |
| Utilizări | Una |
| Stocare | În memorie — o repornire a serverului invalidează codurile încă nefolosite |
| Domeniu de aplicare | Un spațiu de lucru și un provider; generarea unui cod nou îl invalidează pe cel anterior al membrului |
Utilizarea codului este potrivită fără a ține cont de majuscule. Codurile necunoscute, expirate, din alt spațiu de lucru și pentru alt provider eșuează toate identic, intenționat.
Metode de legare
Secțiune intitulată „Metode de legare”| Metodă | Valoare serializată | Cum |
|---|---|---|
| Cod | code |
Membrul a utilizat /cc link <code> |
email |
Emailul verificat al providerului s-a potrivit cu un utilizator care este deja membru al spațiului de lucru |
Operații RPC
Secțiune intitulată „Operații RPC”Toate sunt limitate la spațiul de lucru. Serverul este fără stare, așa că fiecare apel își poartă propriul workspace_id în argumente; dispecerul refuză o operație limitată la spațiul de lucru care îl omite, înainte de a se deschide vreo bază de date. Credențialele circulă doar spre interior — citirile de stare nu le returnează niciodată.
| Operație | Tip | Rol minim | Clase de acțiuni |
|---|---|---|---|
chat.providers |
citire | guest |
— |
chat.status |
citire | guest |
— |
chat.connect |
mutare | admin |
secretAccess, networkEgress |
chat.disconnect |
distructivă | admin |
secretAccess |
chat.listUserLinks |
citire | guest |
— |
chat.beginUserLink |
mutare | member |
— |
chat.unlinkUser |
mutare | member pentru tine, admin pentru oricine altcineva |
— |
chat.createApp |
mutare | admin |
secretAccess, networkEgress |
chat.setupLink |
citire | admin |
— |
chat.botProfile |
citire | admin |
networkEgress |
chat.updateBotProfile |
mutare | admin |
networkEgress |
Operațiile de citire moștenesc pragul guest de la tipul lor; mutările fără un prag explicit cer member.
chat.setupLink compune un URL de creare a aplicației providerului pornind de la profilul botului. Nu declară nicio clasă de acțiuni pentru că nu citește niciun secret și nu apelează nimic — singurul apel este făcut de browserul utilizatorului.
O interogare reactivă completează suprafața:
| Interogare | Tip | Emite |
|---|---|---|
chat.watchUserLinks |
watch | Conturile legate, retrimise ori de câte ori se creează sau se elimină o legătură |
Există pentru că legarea se întâmplă de cealaltă parte: un membru tastează codul de unică folosință în bot, așa că niciun răspuns al clientului nu poate duce răspunsul înapoi. Rosterul din setări și dialogul deschis „leagă-mi contul” o urmăresc amândouă, iar dialogul trece într-o confirmare în momentul în care legătura membrului sosește.
chat.beginUserLink generează întotdeauna un cod pentru identitatea celui care apelează. chat.updateBotProfile returnează un remaining_step doar când mai rămâne un pas (de obicei o reinstalare); lipsa lui înseamnă că modificarea este deja activă.
chat.providers returnează o intrare per provider oferit — descriptorul plus, când există, starea conexiunii. Tot ce afișează interfața de setări vine din această încărcătură utilă, motiv pentru care adăugarea unui provider este o schimbare doar pe server.
| Ce | Unde |
|---|---|
| Credențialele conexiunii | <dataDir>/<workspaceId>/chat_credentials/<provider>.json |
| Configurarea asistată neterminată | <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json |
| Fișierele Slack vechi | <dataDir>/<workspaceId>/slack_credentials.json și slack_app_setup.json, citite o dată și migrate pe loc |
Credențialele stau lângă baza de date a spațiului de lucru, nu în ea: ștergerea unui spațiu de lucru ia cu el și tokenurile de chat, iar workspace.export sau o copie de siguranță copiază workspace.db — așa că un spațiu de lucru înmânat altcuiva nu duce cu el tokenuri de bot active. Scrierile sunt atomice (fișier temporar plus redenumire) și restrânse la doar proprietar.
Tabele de bază de date
Secțiune intitulată „Tabele de bază de date”Ambele se află în baza de date a spațiului de lucru, care are o bază v1 comprimată — așa că o citire din alt spațiu de lucru nu se compilează.
chat_space_links — o conversație externă ↔ un spațiu Control Center:
| Coloană | Note |
|---|---|
id |
UUID v4 |
workspace_id |
Domeniul spațiului de lucru |
provider |
ChatProvider.wire |
external_team_id |
Echipa Slack T…, gol pentru providerii fără acest concept |
external_channel_id |
Canal Slack C…, grup G…, DM D… |
external_thread_id |
Punctul de ancorare al firului Slack sau null când întreaga conversație este ancorajul (DM-urile botului) |
cc_space_id |
REFERENCES spaces(id) ON DELETE CASCADE |
created_by_user_id, created_at, last_activity_at |
Unic pe (workspace_id, provider, external_channel_id, external_thread_id), cu un index parțial care impune același lucru pentru id-urile de fir null (SQLite tratează NULL-urile ca distincte).
chat_user_links — un membru extern ↔ un utilizator Control Center:
| Coloană | Note |
|---|---|
id |
UUID v4 |
workspace_id |
Domeniul spațiului de lucru |
provider |
ChatProvider.wire |
external_team_id, external_user_id |
Membrul Slack U… |
user_id |
Utilizator Control Center (global, deci fără cheie străină) |
method |
code sau email |
linked_at |
Unic pe (workspace_id, provider, external_team_id, external_user_id) și pe (workspace_id, provider, user_id) — atribuire cu o singură valoare în ambele direcții, în timp ce o persoană poate fi legată la mai mulți provideri.
O legătură este o rezolvare de identitate, nu o acordare de acces: apartenența este reverificată la fiecare eveniment de intrare, așa că o legătură către un membru care a fost între timp eliminat se rezolvă într-un refuz.
Tipul de spațiu și metadatele mesajelor
Secțiune intitulată „Tipul de spațiu și metadatele mesajelor”Un spațiu legat este creat cu SpaceKind al providerului (slack). Se comportă ca un spațiu obișnuit pentru oameni — notificat și încadrat la fel ca topic — iar isExternalChat este verificarea prin care trece fiecare comportament (oglindire, ecusoane, păstrare).
Fiecare mesaj pe care puntea îl aduce poartă o marcă metadata['chat']:
{ "chat": { "provider": "slack", "teamId": "T024BE7LD", "channelId": "C0G9QF9GW", "threadId": "1700000000.000100", "messageId": "1700000000.000200", "userId": "U024BE7LH", "userLabel": "sam" }}Marca este și paza anti-ecou: oglinda spre exterior sare peste orice mesaj care o poartă, și exact asta împiedică un mesaj de chat sosit din exterior să fie postat imediat înapoi.
Comportamentul releului
Secțiune intitulată „Comportamentul releului”| Comportament | Detaliu |
|---|---|
| Ce este transmis spre exterior | Doar textul agentului, plus linia de stare a cardului de sarcină. Textul raționamentului și ieșirea instrumentelor rămân în Control Center |
| Cadența streamingului | Limitată la o singură golire pe secundă cât rulează o tură; cardul și textul pe care îl explică călătoresc în același apel |
| Mesajele scrise de oameni în Control Center | Oglindite în firul legat, cu numele afișat al autorului ca prefix |
| Turele fără conținut | Raportate ca o notă scurtă (rularea a eșuat, a fost întreruptă sau s-a oprit la limita de ture), nu ca tăcere |
| Deduplicare | Indexată după id-ul de eveniment propriu al providerului; ultimele 512 sunt reținute |
| Agentul de întâmpinare | O conversație legată nouă trezește agentul CEO al spațiului de lucru, altfel primul său agent, altfel nimeni |
| Denumirea spațiului | #channel · <request> pentru un fir de canal Slack, Slack DM · <request> pentru un DM, titluri plafonate la 48 de caractere |
| Izolarea eșecurilor | Un singur eveniment defect nu doboară niciodată puntea; conexiunea continuă să deservească toate celelalte conversații |
- Configurează integrarea Slack
- Leagă contul tău Slack
- Personalizează botul de chat
- Punți de chat
- Guardrail-uri pentru clasele de acțiuni pe care le declară operațiile