Cầu chat
Một cầu chat biến một sản phẩm chat bên ngoài — hiện nay là Slack — thành một con đường hạng nhất để bước vào không gian làm việc của Control Center. Nhắc tới bot là một agent thức dậy; trả lời trong luồng Slack là cuộc hội thoại tiếp diễn; gõ một lệnh slash là một phiếu được tạo. Bên dưới, tất cả vẫn là những space, bản ghi hội thoại, phiếu và quy kết mà ứng dụng desktop dùng. Chat là một bề mặt trên không gian làm việc, chứ không phải một bản sao của nó.
Slack là nhà cung cấp duy nhất được phát hành. Không hề có bản triển khai Discord, Telegram, Teams hay Matrix ở bất kỳ đâu trong sản phẩm. Tính năng này được viết theo kiểu không phụ thuộc nhà cung cấp một cách có chủ đích — thêm một sản phẩm thứ hai chỉ tốn một giá trị enum, một adapter và một plugin — nhưng hiện tại “chat” nghĩa là Slack.
Hai bề mặt bao trùm tính năng này và chúng thuộc về những người khác nhau. Kết nối một không gian làm việc với Slack và tùy chỉnh bot là hành động của quản trị viên trong Cài đặt → Không gian làm việc → Chung. Liên kết tài khoản chat của bạn với danh tính Control Center của bạn là hành động của thành viên trong Cài đặt → Không gian làm việc → Hồ sơ & danh tính.
Ba quyết định định hình toàn bộ tính năng: kết nối chạy theo hướng nào, điều gì ủy quyền cho một tin nhắn đến từ bên ngoài và kiến thức đặc thù theo sản phẩm được phép nằm ở đâu.
Gọi ra theo thiết kế
Phần tiêu đề “Gọi ra theo thiết kế”Cầu nối không bao giờ lắng nghe nhà cung cấp. Slack được truy cập qua Socket Mode: máy chủ hỏi Slack một URL socket rồi gọi ra, sau đó nhận các lượt nhắc, DM và lệnh slash qua kết nối đó.
Hệ quả chính là mục đích. Một laptop sau NAT, một máy chủ tại nhà không có tên miền, một cỗ máy di chuyển giữa các mạng — tất cả đều chạy được cầu nối mà không cần cổng vào, không tunnel, không chứng chỉ TLS và không URL công khai phải xoay vòng mỗi khi nó thay đổi. Không có endpoint webhook nào để bảo vệ vì đơn giản là không có endpoint webhook nào. Khi Control Center soạn định nghĩa ứng dụng, nó khẳng định Socket Mode và loại bỏ mọi request URL, bởi một ứng dụng được cấu hình cho cả hai sẽ là một ứng dụng chỉ hoạt động nửa vời.
Cái giá phải trả là một kết nối cần được duy trì, và đó là việc của máy chủ: việc kết nối lại dùng backoff lũy thừa kèm jitter, giới hạn ở 30 giây; một lần nhà cung cấp khởi tạo xoay socket sẽ kết nối lại mà không làm tăng backoff đó; còn một thông tin xác thực bị từ chối là dấu chấm hết — thử lại mãi một token đã bị thu hồi thì chỉ chuốc lấy giới hạn tần suất, nên cầu nối dừng lại, chờ token mới, đồng thời báo nguyên văn lỗi của chính nhà cung cấp trong phần cài đặt.
Một không gian làm việc không có ứng dụng chat chẳng tốn gì cả: không socket, không timer, không lưu lượng. Và toàn bộ tiến trình chỉ khởi động sau thông báo sẵn sàng của máy chủ, bởi một ứng dụng chat phản hồi chậm không bao giờ được phép làm chậm quá trình khởi động.
Tư cách thành viên ủy quyền, không phải việc nắm giữ token
Phần tiêu đề “Tư cách thành viên ủy quyền, không phải việc nắm giữ token”Người cài đặt ứng dụng không phải người gửi tin nhắn. Vì vậy mọi sự kiện đi vào đều được phân giải thành một Principal trước khi bất cứ điều gì xảy ra, và nó từ chối theo mặc định kèm một lời giải thích:
- Một tài khoản chat chưa liên kết được hướng dẫn cách liên kết, ngay trong luồng Slack mà nó đã viết.
- Một tài khoản đã liên kết nhưng người dùng không phải thành viên của không gian làm việc này được yêu cầu xin quản trị viên một lời mời.
- Một thành viên có vai trò chỉ đọc (
viewer,guest) được thông báo rằng vai trò của họ không thể bắt đầu công việc.
Không trường hợp nào trong số đó được tiếp tục. Một tin nhắn chat không bao giờ bị quy cho người không gửi nó — chính điều đó giữ cho dấu vết kiểm toán, các giới hạn tần suất theo từng principal và người báo cáo phiếu luôn trung thực. Xem Đa người dùng — danh tính, tư cách thành viên và hiện diện để biết mô hình tư cách thành viên mà phần này dựa vào.
Việc liên kết là phân giải danh tính, không phải một sự cấp quyền truy cập. Tư cách thành viên được kiểm tra lại ở mọi sự kiện, nên một liên kết thuộc về người đã bị gỡ sẽ dẫn tới sự từ chối chứ không phải những đặc quyền cũ của họ. Có hai đường để thiết lập liên kết: một mã dùng một lần, tồn tại trong thời gian ngắn mà thành viên gõ trong chat, hoặc khớp tự động theo email đã xác minh của nhà cung cấp — và đường email vẫn đòi hỏi người dùng khớp phải đã là thành viên, nên một người lạ có địa chỉ tình cờ tồn tại trên máy chủ sẽ chẳng liên kết được với gì.
Các mã này được cố ý giữ trong bộ nhớ, với thời gian sống 15 phút. Việc lưu bền một mã sẽ tạo ra một dòng mang hình dạng thông tin xác thực lâu dài cho một thứ vốn được sinh ra để biến mất, và việc máy chủ khởi động lại làm vô hiệu mọi mã còn hiệu lực là hành vi đúng đắn chứ không phải một lỗi thoái bộ — thành viên chỉ cần bấm “liên kết” lần nữa.
Một lời từ chối được trả lời ngay trong luồng Slack mà tin nhắn đã mở, dưới dạng một phản hồi thật. Một phản hồi trong luồng gắn nhãn cho tin nhắn mà nó trả lời, đến đúng người cần đến nó và vẫn giữ câu “bạn chưa được liên kết” khỏi chính kênh. Đăng một tin nhắn cũng là năng lực duy nhất mà mọi nhà cung cấp đều có, nên lời giải thích không bao giờ phụ thuộc vào một tính năng mà ứng dụng có thể không có.
Nguồn gốc và vấn đề tiếng vọng
Phần tiêu đề “Nguồn gốc và vấn đề tiếng vọng”Một cầu nối phản chiếu cả hai chiều sẽ lặp vô tận trừ khi có thứ gì đó phân biệt
hai hướng. Mọi tin nhắn mà cầu nối đưa vào đều được đóng dấu bằng
metadata['chat'] — nhà cung cấp, team, cuộc hội thoại, luồng, tin nhắn và
người gửi. Bản phản chiếu hướng ra bỏ qua mọi tin nhắn mang dấu đó.
Chính quy tắc duy nhất đó khiến việc phản chiếu hai chiều trở nên an toàn: một tin nhắn do con người gõ trong Control Center được chuyển ra luồng đã liên kết (có tiền tố là tên tác giả), còn một tin nhắn đến từ chat thì dừng lại ngay tại nơi nó đi vào.
Những gì đi ra ngoài được giới hạn hẹp một cách có chủ đích. Câu trả lời của
agent được chuyển đi như văn bản chat thông thường. Một suy nghĩ ngắn có thể xuất
hiện trên dòng Thinking… của thẻ. Đầu ra công cụ là một bức tường diff và ở
lại Control Center, nơi cũng là đích mà một câu trả lời bị cắt ngắn trỏ tới.
Thẻ báo cáo công việc; văn bản vẫn là tin nhắn
Phần tiêu đề “Thẻ báo cáo công việc; văn bản vẫn là tin nhắn”Ở nơi nhà cung cấp hiển thị được chúng, một lượt mang một thẻ công việc, được sửa tại chỗ trong suốt thời gian lượt đó chạy và nằm phía trên câu trả lời mà nó giải thích.
Thẻ là một kế hoạch được nhóm lại, chứ không phải một đoạn văn trộn lẫn. Slack
nối details và output trên cùng một task, nên phần chi tiết của một dòng chỉ
được gửi một lần (suy nghĩ trên Thinking…) và output không bao giờ được
truyền theo luồng. Phần thiết lập là một dòng có tiêu đề được thay thế (Working on it…, Cloning …). Việc suy luận thêm một dòng Thinking…. Mỗi công cụ có
một dòng riêng (Read lib/main.dart). Câu trả lời là văn bản thông thường nằm
dưới thẻ — chế độ xem kế hoạch của Slack đọc nó từ một lần nối markdown sau đó,
chứ không phải từ một khối bị trộn vào các task. Không có gì khác: không có đầu
ra công cụ và chỉ có một đối số công cụ từ một danh sách cho phép ngắn, vì đối
số của một công cụ chưa biết có thể là cả thân một tệp, mà một dòng chỉ là một
dòng.
Thẻ cũng bao trùm khoảng chờ trước lượt. Lượt nhắc đầu tiên tạo ra cuộc hội
thoại, và việc tạo nó sẽ clone mọi repo mà agent sẽ làm việc trong đó — với một
kho mã thật thì đó là mấy phút mà agent vẫn chưa được yêu cầu gì, còn luồng chat
trông như bot đã phớt lờ yêu cầu. Vì vậy thẻ mở ra ngay khi lượt nhắc được chấp
nhận (Working on it…), rồi dòng thiết lập đó cập nhật bằng đúng những từ mà
desktop hiển thị — Cloning acme/widgets…, Setting up Reviewer…, Starting the agent… — sau đó là một dòng Thinking… ngay khi agent bắt đầu suy luận, rồi mỗi
công cụ một dòng. Một thẻ, một tin nhắn, từ “tôi đã nghe thấy bạn” cho tới câu
trả lời dưới dạng văn bản thông thường bên dưới. Phần thiết lập chỉ được báo ở
nơi có ai đó trong chat đang chờ nó: một cuộc hội thoại đang được chuẩn bị trong
ứng dụng, hay một cuộc hội thoại bị bỏ dở được tiếp tục lúc máy chủ khởi động,
thì được cấp phát mà không có thẻ nào cả.
Nó mang theo thứ duy nhất mà chat không thể cho người đọc: một liên kết View in
Control Center trỏ tới cuộc hội thoại đằng sau phản hồi. Các sản phẩm chat chỉ
chấp nhận http(s) trong một liên kết và không gì khác, nên nút này trỏ tới một
trang nhỏ do máy chủ phục vụ, trang đó đưa trình duyệt tới liên kết
control-center:// của ứng dụng desktop. Trang đó không đọc gì và không phân
giải gì — các id trong URL của nó chỉ được đưa nguyên vào một deep link, không
đâu khác — còn một máy chủ không có URL công khai truy cập được thì chỉ tạo ra
một thẻ không có nút, chứ không phải một nút hỏng.
Thẻ là một năng lực như mọi năng lực khác. Một nhà cung cấp không có thẻ vẫn hoạt
động y như trước, chỉ có văn bản; và một nhà cung cấp từ chối thẻ vào lúc gọi vẫn
giữ được luồng của mình: câu trả lời luôn quan trọng hơn thẻ mô tả nó. Khi tính
năng truyền theo luồng bị tắt hoàn toàn, thẻ đã hoàn tất sẽ đi kèm tin nhắn được
đăng duy nhất thay vào đó. Một phiếu được tạo cũng được đối xử như vậy — xác nhận
/cc ticket là một thẻ đã hoàn tất, mà lý do tồn tại của nó là liên kết tới
phiếu vừa được tạo.
Các năng lực được thương lượng, rồi được ghi nhớ
Phần tiêu đề “Các năng lực được thương lượng, rồi được ghi nhớ”Các sản phẩm chat khác nhau ở những gì chúng hiển thị được, nên cầu nối được viết dựa trên bề mặt giàu tính năng nhất — một phản hồi lớn dần theo thời gian thực bên trong luồng, một trạng thái tạm thời “is thinking…”, một luồng có tiêu đề, một câu trả lời chỉ hiện một lần cho lệnh slash — rồi hạ cấp từng năng lực một. Một nhà cung cấp không có năng lực nào trong số đó vẫn hoạt động; các phản hồi chỉ đơn giản đến dưới dạng một tin nhắn.
Quảng cáo một năng lực không phải là cam kết nó. Phản hồi theo luồng của Slack cần một tính năng của gói trả phí, và Slack chỉ từ chối vào lúc gọi. Một lời từ chối bị đánh dấu là vĩnh viễn sẽ được ghi nhớ cho toàn bộ vòng đời của kết nối: cầu nối ngừng hỏi và đăng nguyên phản hồi từ đó về sau, còn phần cài đặt nói rõ điều đó, bởi câu “các phản hồi của tôi thôi không được truyền theo luồng nữa” phải trả lời được mà không cần đọc log. Thẻ công việc cũng bị gỡ theo cách tương tự nếu Slack từ chối một thẻ.
Mọi giới hạn đều thuộc về nhà cung cấp chứ không thuộc về bộ chuyển tiếp: trần kích thước tin nhắn, trần kích thước khối truyền và việc một luồng có cần điểm neo luồng hay không đều do adapter khai báo, và một phản hồi dài được cắt tại ranh giới đoạn, dòng hoặc từ chứ không cắt giữa từ.
Một cầu nối, một adapter cho mỗi sản phẩm
Phần tiêu đề “Một cầu nối, một adapter cho mỗi sản phẩm”Mọi thứ mang hình dạng sản phẩm đều nằm sau một cổng. Phần lõi chung biết về
markdown, các cuộc hội thoại chat, các luồng chat và thành viên; nó không biết
gì về envelope của Slack, mrkdwn hay gateway intent. Ba mảnh ghép làm nên điều
đó:
- Adapter chuẩn hóa một sản phẩm: transport, các lệnh gọi API, codec văn bản
và những nét dị biệt trong envelope của nó. Trường hợp của Slack rất đáng học
— một lượt nhắc trong kênh đến hai lần (dưới dạng
app_mentionvàmessage) và phần lớn các kiểu con của tin nhắn chỉ là chi tiết trang trí chứ không phải yêu cầu mới. Cả hai đều được xử lý trong adapter, nên lõi không bao giờ biết rằng Slack gửi trùng hai lần. - Descriptor là hình dạng công khai của nhà cung cấp: nó cần những thông tin xác thực nào, định dạng của chúng ra sao, những URL nào giúp người dùng tìm thấy chúng, nó làm được gì và ứng dụng của nó có thể được tạo hay chỉnh sửa từ đây hay không. Thẻ cài đặt, hộp thoại kết nối và việc kiểm tra đối số đều được sinh ra từ nó — client gửi trả về một nhà cung cấp cùng một map có khóa là id trường.
- Plugin là phần tổ hợp phía máy chủ: thông tin xác thực biến thành một kết nối đã xác minh như thế nào, một adapter được dựng ra sao và ứng dụng phía nhà cung cấp được quản lý thế nào.
Vậy nên thêm một sản phẩm chỉ là một giá trị enum, một adapter và một plugin. Không có thao tác RPC mới, không thay đổi schema, không thay đổi client — và client vẫn trung thực, bởi một trường nó chưa từng nghe tới vẫn hiển thị với nhãn mà máy chủ cung cấp.
Hai chi tiết giữ cho lớp trừu tượng không rò rỉ lên trên. Những bước quản lý ứng dụng mà nhà cung cấp không có API cho chúng (tạo token, cài ứng dụng, cài đặt lại sau khi thay đổi quyền) được trả về dưới dạng các bước có tên kèm liên kết riêng, để màn hình hoàn tất thiết lập hiển thị chúng mà không cần biết đó là nhà cung cấp nào. Và mọi hướng dẫn mà cầu nối viết ra đều nêu tên nhà cung cấp cùng lệnh mà ứng dụng thực sự dùng, nên một lệnh được đổi tên hay một nhà cung cấp thứ hai vẫn đọc đúng mà không cần chuỗi mới.
Thông tin xác thực nằm ở đâu
Phần tiêu đề “Thông tin xác thực nằm ở đâu”Token chat của một không gian làm việc nằm trong thư mục riêng của chính không
gian làm việc đó, bên cạnh cơ sở dữ liệu — dưới dạng JSON thuần tại
<dataDir>/<workspaceId>/chat_credentials/<provider>.json, không nằm trong cơ sở
dữ liệu và cố ý không nằm trong keychain của hệ điều hành. Hai hệ quả dễ bị bỏ
quên về sau: xóa một không gian làm việc là gỡ liên kết thư mục đó, và token đi
theo nó; còn xuất hay sao lưu một không gian làm việc thì sao chép workspace.db,
nên một không gian làm việc được chuyển cho người khác không mang theo token bot
còn sống bên trong. Tệp này thừa hưởng ranh giới tin cậy của hệ thống tệp trên
máy chủ — bất cứ thứ gì có quyền đọc thư mục dữ liệu đều đọc được token bot, đúng
ranh giới mà các cơ sở dữ liệu nằm sau.
Mỗi nhà cung cấp một tệp, được ghi theo kiểu nguyên tử và chỉ chủ sở hữu truy cập được: một ứng dụng bị thu hồi sẽ bị xóa bằng cách gỡ liên kết một tệp, và một tệp hỏng chỉ khiến mất một nhà cung cấp thay vì tất cả. Thông tin xác thực chỉ đi vào qua thao tác kết nối và không bao giờ đi ra — các lần đọc trạng thái chỉ trả lời bằng metadata của kết nối.
Những gì cố ý không được cung cấp
Phần tiêu đề “Những gì cố ý không được cung cấp”- Cài đặt cấu trúc của ứng dụng. Transport, scope và đăng ký sự kiện được khẳng định chứ không phơi ra. Một ứng dụng có thể bị cấu hình đến mức im lặng từ màn hình cài đặt là một phiếu hỗ trợ, chứ không phải một tính năng.
- Hội thoại cộng với lệnh slash. Bề mặt Slack gồm tin nhắn, phản hồi trong luồng và lệnh slash.
- Một nguồn sự thật thứ hai. Chat không phải nơi lưu trữ công việc. Luồng Slack là một space, phản hồi là một bản ghi, phiếu là phiếu — tất cả đều nằm trong không gian làm việc, và mọi client đều truy cập được.
Xem thêm
Phần tiêu đề “Xem thêm”- Đa người dùng — danh tính, tư cách thành viên và hiện diện: những Principal và vai trò mà cầu nối phân giải qua
- Không gian làm việc và cô lập: vì sao liên kết và thông tin xác thực thuộc về từng không gian làm việc
- Mô hình agent: điều gì thức dậy khi một lượt nhắc đến
- Phiếu và ủy thác:
/cc tickettạo ra thứ gì - Kiến trúc: cầu nối nằm ở đâu giữa máy chủ và các client
- Nối Slack với không gian làm việc của bạn
- Thiết lập tích hợp Slack
- Liên kết tài khoản Slack của bạn
- Tùy chỉnh chatbot
- Hội thoại PR: ba quyết định tương tự, được đưa ra cho một máy chủ mã