Lewati ke konten

Jembatan chat

Penyedia Nilai wire Transport Jenis space
Slack slack Socket Mode (outbound WSS) slack

Slack adalah satu-satunya penyedia. ChatProvider adalah enum bernilai tunggal dan cc_server mendaftarkan satu plugin. Skema, setiap pencarian, dan setiap indeks unik dikunci berdasarkan provider, sehingga id tidak pernah melintasi penyedia dan menambahkan penyedia baru hanyalah perubahan di sisi server.

Satu aplikasi di sisi penyedia per ruang kerja × penyedia.

Nama perintah bersifat per aplikasi (cc secara bawaan, jadi /cc). Admin dapat menggantinya; jembatan lalu memakai nama terakhir yang dipakai untuk memanggilnya.

Perintah Efek
/cc help (atau /cc tanpa verba) Menampilkan daftar yang bisa dilakukan bot
/cc link <code> Menukarkan kode sekali pakai dan menautkan identitas chat pengirim
/cc ticket <title> Membuat tiket lokal yang dilaporkan oleh anggota yang tertaut
/cc ticket <title> | <description> Sama, dengan semua teks setelah | pertama sebagai deskripsi
Verba tidak dikenal Melaporkan verba yang tidak dikenal beserta teks bantuan

Cara menjangkau agen:

Tindakan Perilaku
@bot … di sebuah kanal Membuka thread Slack dan space Control Center yang cocok; agen membalas di thread Slack
Balasan di thread Slack yang dijembatani Melanjutkan space Control Center yang sama — tidak perlu sebutan
Pesan biasa di sebuah kanal Diabaikan (tidak ditujukan ke bot dan bukan di thread Slack yang dijembatani)
DM ke bot Satu space Control Center per percakapan DM; menetapkan judul thread Slack dan status “is thinking…” selama agen bekerja

Dideklarasikan oleh deskriptor penyedia, yang menghasilkan dialog koneksi dan memvalidasi kiriman.

Id kolom Label Wajib Prefiks Letaknya
botToken Token bot Ya xoxb- OAuth & Permissions
appToken Token tingkat aplikasi Ya xapp- Basic Information → App-Level Tokens, scope connections:write
configRefreshToken Token konfigurasi aplikasi Tidak xoxe App configuration tokens on api.slack.com/apps

configRefreshToken adalah kredensial pengelolaan aplikasi: tanpa itu koneksi tetap berfungsi, tetapi bot tidak dapat dibuat atau diedit dari Control Center. Nilainya berotasi setiap kali dipakai dan nilai hasil rotasinya langsung disimpan.

Nilai dipangkas, kunci yang tidak dideklarasikan dibuang, dan ketidakcocokan prefiks ditolak dengan kalimat yang sama di klien dan di server. Kolom wajib harus ada pada setiap koneksi; kolom opsional yang dibiarkan kosong mempertahankan nilai yang sudah tersimpan.

Cakupan token bot:

app_mentions:read
assistant:write
channels:history
chat:write
commands
groups:history
im:history
users:read
users:read.email

Langganan event bot:

app_home_opened
app_mention
message.channels
message.groups
message.im

Pengaturan struktural yang dipaksakan Control Center saat membuat atau mengedit aplikasi: Socket Mode aktif, tanpa URL permintaan, tab Messages aktif dan dapat ditulis, bot always_online, cakupan dan event yang diperlukan digabungkan ke apa pun yang sudah dimiliki aplikasi (tidak pernah dikurangi), dan satu perintah slash yang membawa deskripsi File a ticket or link your Control Center account — penanda yang mengidentifikasi perintah milik Control Center dalam manifes yang diekspor.

Status Wire Arti
Tidak terhubung disconnected Tidak ada kredensial tersimpan untuk ruang kerja × penyedia ini
Menghubungkan… connecting Kredensial tersimpan, transport sedang menghubungi atau menghubungi ulang
Aktif connected Transport aktif dan menerima event
Kesalahan koneksi error Tersimpan tetapi gagal; galat dari penyedia itu sendiri ditampilkan berdampingan

Koneksi ulang memakai backoff eksponensial dengan jitter, dibatasi maksimum 30 detik. Rotasi socket yang diprakarsai penyedia akan segera tersambung ulang tanpa menaikkan backoff. Kredensial yang ditolak (invalid_auth, not_authed, token_revoked, account_inactive, token_expired, invalid_app_id) atau cakupan yang hilang bersifat final: jembatan berhenti mencoba ulang sampai kredensial baru datang.

Jembatan ini ditulis terhadap permukaan fitur yang terkaya dan menurunkan perilakunya sesuai kemampuan yang tersedia.

Kemampuan Slack Efek saat tidak tersedia
streaming ya Seluruh balasan diposkan sekali, di akhir
streamingRequiresThread ya Target balasan tanpa jangkar thread memposkan balasan secara utuh
ephemeralMessages ya Jawaban perintah slash tanpa pegangan balasan yang dapat dipakai akan dibuang (penolakan selalu diposkan di dalam thread)
threadStatus ya Tidak ada baris sementara “is thinking…”
threadTitle ya Thread chat mempertahankan judul bawaannya
slashCommands ya Tidak ada permukaan /cc
taskCards ya Hanya teks — tidak ada kartu di atas balasan dan tidak ada tautan “View in Control Center”
maxMessageLength 39,000 Balasan yang lebih panjang dipotong dengan penunjuk ke Control Center
maxStreamChunkLength 3,800 Penambahan dipisah pada batas paragraf, baris, atau spasi

Streaming yang diiklankan belum tentu streaming yang dijanjikan: streaming Slack memerlukan fitur Agents & AI Apps pada paket berbayar. Penolakan pada saat pemanggilan (streaming_not_supported, feature_not_enabled, paid_only, …) diingat selama koneksi itu hidup dan dimunculkan sebagai streamingAvailable: false. Kartu tugas Slack menumpang pemanggilan yang sama, sehingga Slack yang menolak mode tampilan kartu tetap mempertahankan streamnya dan hanya kehilangan kartunya, sekali saja, selama koneksi itu hidup.

Sebuah stream juga harus menyebutkan pembaca yang dituju. Slack mensyaratkan adanya pengguna penerima sekaligus timnya untuk membukanya di sebuah kanal (DM tidak bisa), dan jika tidak ada akan menolak dengan missing_recipient_user_id / missing_recipient_team_id — penolakan per pemanggilan, sehingga jembatan terus mencoba dan sementara itu giliran tersebut tiba sebagai satu balasan yang diposkan. Penerimanya adalah siapa pun yang bertanya, diambil dari pesan masuk (tim mereka, bukan tim aplikasi, sehingga kanal Slack Connect terselesaikan dengan benar). Untuk giliran yang tidak dimulai siapa pun dari chat — pekerjaan yang dijalankan di Control Center pada percakapan yang sudah dijembatani — penerimanya adalah anggota yang tautannya membuat jembatan itu; jika sejak itu mereka membatalkan tautan akun chatnya, giliran tersebut diposkan secara utuh alih-alih distreaming.

Satu kartu terkelompok per giliran agen (task_display_mode: plan), dengan satu baris per langkah. Setiap baris adalah task_update dari id + title + status, ditambah details sekali jalan saat pemikirannya siap. details dan output dengan id yang sama disambung tanpa pemisah, jadi details dikirim sekali dan output tidak pernah distreaming. Judul rencananya adalah permintaan itu; jawabannya adalah penambahan terpisah di kemudian hari yang membawa potongan markdown_text.

Bagian Isi
Judul rencana Apa yang diminta
Baris penyiapan Working on it…, lalu Cloning …, lalu Starting the agent… — satu baris, judulnya diganti
Baris berpikir Thinking…, ditambahkan saat agen mulai bernalar; pemikirannya adalah details baris ini, dikirim sekali saat penalaran selesai
Baris alat Satu per alat (Bash which swift), id unik agar duduk berdampingan
Status Sedang berlangsung selama baris itu berjalan; selesai saat baris berikutnya mulai atau giliran berakhir; error untuk gagal, terputus, dan batas giliran
Ajakan bertindak View in Control Center sebagai baris penutup saat giliran berakhir — tidak ada selama pekerjaan masih berjalan dan tidak ada saat server tidak punya URL publik yang dapat dijangkau

Yang tidak pernah dibawa kartu: keluaran alat, argumen di luar daftar izin itu, atau apa pun yang dibaca dari basis data. Prosa penalaran hanyalah details pada baris Thinking… (dibatasi 256 karakter), bukan isi pesannya. Kartu yang tidak akan mengatakan hal baru tidak dikirim ulang. Sebutan langsung membuka stream agar thread Slack tidak sepi; giliran yang kemudian tidak menghasilkan teks sama sekali akan menutup kartu yang sama dengan catatan singkat alih-alih memposkan pesan kedua. Giliran tanpa kata di space yang tidak pernah membuka stream (tanpa kartu, atau streaming sudah ditolak) tetap memposkan satu pesan yang membawa kartu yang sudah selesai dan catatannya.

Kartu yang distreaming dan yang diposkan adalah payload yang berbeda:

Bentuk Wujud Prosa
Distreaming plan_update plus potongan task_update dengan kunci id String biasa, dibatasi 256 karakter per potongan
Diposkan Blok plan yang membungkus task_card dengan kunci task_id Teks kaya

Penanganan kegagalan:

Situasi Perilaku
Penyedia menolak kartu pada sebuah penambahan Penambahan itu langsung dicoba ulang tanpa kartu; prosanya tetap tiba secara live dan kartu dibuang selama koneksi itu hidup
Penyedia juga menolak prosanya Stream ditutup dan balasannya diposkan secara utuh
Stream terputus setelah sebagian balasan tiba Stream ditutup dan tidak ada yang diposkan ulang

Lihat Jembatan chat untuk alasan payload dipisah seperti ini.

Kartu terbuka saat sebuah sebutan diterima, sebelum kloning dan sebelum token pertama; giliran berikutnya memakai kartu dan stream yang sama. Narasi setelahnya berasal dari SpaceProvisioningChanged di bus peristiwa domain — kemajuan yang sama yang dibaca banner penyiapan desktop dari baris space.

Status penyiapan Narasi
Sebutan diterima Working on it…
Langkah repo Cloning <repo>…
Langkah prCheckout Checking out the pull request in <repo>…
Langkah script Running the setup script for <repo>…
Langkah agent Setting up <agent>…
provisioning, belum ada langkah Preparing the workspace…
ready Starting the agent…
Agen bernalar Thinking…
failed Workspace setup failed — details are in Control Center.
cancelled Workspace setup was stopped — details are in Control Center.

Narasi penyiapan memerlukan keempat syarat berikut:

Syarat
Penyedia merender kartu
Streaming tersedia pada koneksi ini
Space memiliki permintaan chat yang sedang berjalan
Belum ada kartu yang melaporkan giliran di sana

Permintaan itu tetap berjalan sampai giliran dimulai atau penyiapan gagal. Event ready tanpa kartu yang sudah terbuka adalah space hangat dan dibiarkan sepi, dan penyiapan yang tidak ditunggu siapa pun di chat — percakapan yang dibuka di aplikasi, percakapan telantar yang dilanjutkan saat boot — tidak menghasilkan kartu sama sekali.

Produk chat hanya menerima http(s) di dalam tautan dan tidak yang lain, jadi ajakan bertindak tidak bisa menyebut skema desktop secara langsung.

Properti Nilai
Rute GET /open/workspaces/<workspaceId>/(spaces|tickets)/<id>
Origin Diturunkan dari URL publik server (ws → http, wss → https); loopback dipertahankan
Respons Halaman HTML kecil tanpa cache yang menavigasi ke control-center://workspaces/<workspaceId>/(spaces|tickets)/<id> dan menampilkan tombol manual
Validasi Id harus cocok dengan [A-Za-z0-9_.-]{1,128}; selain itu adalah 404
Akses Publik dan inert — tidak membaca basis data apa pun, jadi bukan oracle keberadaan; aplikasi yang dibukanya yang melakukan otorisasi
Kolom Batas Catatan
appName 1–35 karakter Bawaannya Control Center · <workspace name>
botDisplayName 1–80 karakter Bawaannya control-center
description ≤ 140 karakter
agentDescription ≤ 300 karakter Ditampilkan di atas DM dengan bot
command ≤ 32 karakter Dinormalisasi: garis miring di depan dihapus, dijadikan huruf kecil, [^a-z0-9_-] dibuang, jika kosong kembali ke cc
agentEnabled — Apakah pengalaman DM/agen tersedia

Kotak kosong berarti “biarkan apa adanya”, bukan “kosongkan”. Batasnya adalah batas milik Slack, yang paling ketat di antara produk yang dimodelkan.

Properti Nilai
Panjang 6 karakter
Alfabet 23456789ABCDEFGHJKLMNPQRSTVWXYZ (tanpa 0/O, 1/I, U)
Masa berlaku 15 menit
Penggunaan Sekali
Penyimpanan Di memori — mulai ulang server membatalkan kode yang masih berlaku
Cakupan Satu ruang kerja dan satu penyedia; membuat kode baru membatalkan kode anggota sebelumnya

Penukaran dicocokkan tanpa membedakan huruf besar/kecil. Kode yang tidak dikenal, kedaluwarsa, milik ruang kerja lain, dan penyedia yang salah semuanya gagal dengan cara yang sama secara sengaja.

Metode Wire Cara
Kode code Anggota menukarkan /cc link <code>
Email email Email terverifikasi dari penyedia cocok dengan pengguna yang sudah menjadi anggota ruang kerja

Semuanya bercakupan ruang kerja. Server bersifat stateless, jadi setiap panggilan membawa workspace_id-nya sendiri di args; dispatcher menolak operasi bercakupan ruang kerja yang menghilangkannya, sebelum basis data apa pun dibuka. Kredensial hanya bergerak masuk — pembacaan status tidak pernah mengembalikannya.

Operasi Jenis Peran minimum Kelas tindakan
chat.providers read guest —
chat.status read guest —
chat.connect mutate admin secretAccess, networkEgress
chat.disconnect destructive admin secretAccess
chat.listUserLinks read guest —
chat.beginUserLink mutate member —
chat.unlinkUser mutate member untuk diri sendiri, admin untuk siapa pun yang lain —
chat.createApp mutate admin secretAccess, networkEgress
chat.setupLink read admin —
chat.botProfile read admin networkEgress
chat.updateBotProfile mutate admin networkEgress

Operasi baca mewarisi batas bawah guest dari jenisnya; mutasi tanpa batas bawah eksplisit memerlukan member.

chat.setupLink menyusun URL pembuatan aplikasi penyedia dari profil bot. Operasi ini tidak mendeklarasikan kelas tindakan karena tidak membaca rahasia apa pun dan tidak menghubungi apa pun — hanya browser pengguna sendiri yang melakukan panggilan.

Satu kueri reaktif melengkapi permukaan ini:

Kueri Jenis Memancarkan
chat.watchUserLinks watch Daftar anggota, dikirim ulang setiap kali tautan dibuat atau dihapus

Kueri ini ada karena penautan terjadi di sisi lain: anggota mengetikkan kode sekali pakai ke dalam bot, sehingga tidak ada respons klien yang dapat membawa jawabannya kembali. Daftar di pengaturan dan dialog “hubungkan akun saya” yang terbuka keduanya mengikuti kueri ini, dan dialognya beralih ke konfirmasi begitu tautan milik anggota itu tiba.

chat.beginUserLink selalu membuat kode untuk identitas pemanggil itu sendiri. chat.updateBotProfile mengembalikan remaining_step hanya ketika masih ada langkah tersisa (biasanya instal ulang); tidak adanya nilai itu berarti editan sudah berlaku.

chat.providers mengembalikan satu entri per penyedia yang ditawarkan — deskriptornya ditambah, bila ada, status koneksi. Semua yang dirender UI pengaturan berasal dari payload itu, itulah sebabnya menambahkan penyedia hanyalah perubahan di sisi server.

Apa Di mana
Kredensial koneksi <dataDir>/<workspaceId>/chat_credentials/<provider>.json
Penyiapan terpandu yang belum selesai <dataDir>/<workspaceId>/chat_credentials/<provider>_setup.json
Berkas Slack lama <dataDir>/<workspaceId>/slack_credentials.json dan slack_app_setup.json, dibaca sekali dan dimigrasikan di tempat

Kredensial berada di samping basis data ruang kerja, bukan di dalamnya: menghapus ruang kerja sekaligus menghapus token chatnya dan workspace.export atau cadangan menyalin workspace.db — jadi ruang kerja yang diserahkan kepada orang lain tidak membawa token bot yang masih hidup. Penulisan bersifat atomik (berkas sementara plus rename) dan diperketat hanya untuk pemiliknya.

Keduanya berada di basis data ruang kerja, yang membawa baseline v1 yang sudah dipadatkan — sehingga pembacaan lintas ruang kerja tidak dapat dikompilasi.

chat_space_links — satu percakapan eksternal ↔ satu space Control Center:

Kolom Catatan
id UUID v4
workspace_id Cakupan ruang kerja
provider ChatProvider.wire
external_team_id Tim Slack T…, kosong untuk penyedia yang tidak mengenal konsep ini
external_channel_id Kanal Slack C…, grup G…, DM D…
external_thread_id Jangkar thread Slack, atau null ketika seluruh percakapan menjadi jangkarnya (DM bot)
cc_space_id REFERENCES spaces(id) ON DELETE CASCADE
created_by_user_id, created_at, last_activity_at

Unik pada (workspace_id, provider, external_channel_id, external_thread_id), dengan indeks parsial yang menegakkan hal yang sama untuk thread id null (SQLite memperlakukan NULL sebagai berbeda).

chat_user_links — satu anggota eksternal ↔ satu pengguna Control Center:

Kolom Catatan
id UUID v4
workspace_id Cakupan ruang kerja
provider ChatProvider.wire
external_team_id, external_user_id Anggota Slack U…
user_id Pengguna Control Center (global, jadi tanpa kunci asing)
method code atau email
linked_at

Unik pada (workspace_id, provider, external_team_id, external_user_id) dan pada (workspace_id, provider, user_id) — atribusi bernilai tunggal di kedua arah, sementara satu orang dapat tertaut di beberapa penyedia.

Tautan adalah resolusi identitas, bukan pemberian akses: keanggotaan diperiksa ulang pada setiap event yang masuk, jadi tautan ke anggota yang sejak itu dihapus berujung pada penolakan.

Space yang dijembatani dibuat dengan SpaceKind milik penyedia (slack). Perilakunya seperti space biasa yang menghadap manusia — dinotifikasi dan dikelompokkan seperti topic — dan isExternalChat adalah pemeriksaan yang dilalui setiap perilaku (pencerminan, pemberian badge, retensi).

Setiap pesan yang dibawa masuk jembatan membawa stempel metadata['chat']:

{
"chat": {
"provider": "slack",
"teamId": "T024BE7LD",
"channelId": "C0G9QF9GW",
"threadId": "1700000000.000100",
"messageId": "1700000000.000200",
"userId": "U024BE7LH",
"userLabel": "sam"
}
}

Stempel itu juga berfungsi sebagai penjaga gema: cermin keluar melewati setiap pesan yang membawanya, itulah yang mencegah pesan chat yang masuk diposkan kembali secara langsung.

Perilaku Rincian
Apa yang diteruskan keluar Hanya teks agen, ditambah baris status kartu tugasnya. Prosa penalaran dan keluaran alat tetap di Control Center
Irama streaming Dibatasi menjadi satu pengiriman per detik selama giliran berjalan; kartu dan teks yang dijelaskannya berjalan dalam panggilan yang sama
Pesan manusia yang diketik di Control Center Dicerminkan ke thread yang tertaut, diawali dengan nama tampilan penulisnya
Giliran kosong Dilaporkan sebagai catatan singkat (Run gagal, terputus, atau berhenti di batas gilirannya) alih-alih diam
Deduplikasi Dikunci berdasarkan id event milik penyedia sendiri; 512 yang terakhir diingat
Agen pintu depan Percakapan terjembatani yang baru membangunkan agen CEO ruang kerja, jika tidak ada maka agen pertamanya, jika tidak ada maka tidak ada siapa pun
Penamaan space #channel · <request> untuk thread kanal Slack, Slack DM · <request> untuk DM, judul dibatasi 48 karakter
Isolasi kegagalan Satu event buruk tidak pernah menjatuhkan jembatan; koneksi itu tetap melayani semua percakapan lain