Appearance
Bot
Bot là một tài khoản chat dành cho phần mềm, không phải cho người. Bot có tên, có ảnh đại diện, được thêm vào nhóm chat như một thành viên. Từ đó phần mềm đứng sau bot có thể đăng tin vào nhóm, nhận tin mọi người gọi tên nó, rồi trả lời lại, mà không cần ai ngồi đăng nhập.
Trang này viết cho ba người đọc:
- Quản trị đọc từ đầu tới hết Bot nghe những tin nào: tạo bot, lấy token, thêm vào nhóm, bật Trợ lý CRM.
- Người muốn nối bot với một hệ thống khác hoặc một AI Agent (ví dụ AI Coworker chạy trên máy tính của bạn) đọc thêm Thiết kế bot hai chiều và Nối AI Agent chạy trên máy của bạn.
- Lập trình viên nhảy thẳng xuống Dành cho lập trình viên: địa chỉ API, dạng tin gửi và nhận, cách kiểm chữ ký, bảng mã lỗi.
Đường dẫn: →
Mở trang Bot ↗ Đặc tả API chat (Swagger) ↗
Bốn thứ hay bị nhầm với nhau.
Bot (trang này): thành viên tự động trong Chat nội bộ giữa nhân viên với nhau.
Trợ lý AI ở Hộp thư: AI trả lời khách hàng nhắn qua Zalo, Facebook, Livechat. Đó là hệ khác, cấu hình ở Hộp thư, không phải trang này.
API Keys: chìa khoá để phần mềm khác đọc hoặc ghi dữ liệu CRM, Hộp thư. Xem API Keys.
Kết nối MCP: nối trợ lý AI (Claude…) vào workspace để hỏi dữ liệu bằng tiếng Việt, không đi qua nhóm chat.
Bot (trang này): thành viên tự động trong Chat nội bộ giữa nhân viên với nhau.
Trợ lý AI ở Hộp thư: AI trả lời khách hàng nhắn qua Zalo, Facebook, Livechat. Đó là hệ khác, cấu hình ở Hộp thư, không phải trang này.
API Keys: chìa khoá để phần mềm khác đọc hoặc ghi dữ liệu CRM, Hộp thư. Xem API Keys.
Kết nối MCP: nối trợ lý AI (Claude…) vào workspace để hỏi dữ liệu bằng tiếng Việt, không đi qua nhóm chat.
Bot làm được bốn việc
| Việc | Ví dụ | Cần gì |
|---|---|---|
| Phần mềm ngoài báo tin vào nhóm | Website có đơn mới thì nhóm «Bán hàng» nhận một thẻ «Đơn hàng mới #SO-1024» | Bot + Incoming Webhook (không cần lập trình) hoặc gọi API gửi tin |
| Nhóm hỏi, hệ thống khác trả lời | Gõ «@Bot kho tồn SP-01?» thì bot hỏi phần mềm kho rồi trả lời ngay trong nhóm | Bot + một chương trình nhỏ («cầu nối») do bạn chạy |
| Hỏi số liệu CRM ngay trong nhóm | «@Bot doanh số tháng này» | Bot + bật Trợ lý CRM (không cần lập trình) |
| Nối một AI Agent làm việc cùng nhóm | «@Bot tóm tắt file báo giá này rồi soạn email trả lời khách» | Bot + cầu nối tới AI Agent, xem mục riêng |
Trước khi bắt đầu: quyền
Mục Bot chỉ hiện trong menu khi workspace có Chat nội bộ và vai trò của bạn có ô Xem của dòng Bot trong Phân quyền. Từng thao tác cần ô riêng:
| Thao tác | Ai làm được |
|---|---|
| Xem trang Bot, chi tiết bot, danh sách nhóm của bot, xem Incoming Webhook | Ô Bot · Xem |
| Tạo bot | Ô Bot · Tạo |
| Sửa tên, ảnh, mô tả · tạo lại token · đặt webhook · bật Trợ lý CRM · tạo hoặc xoay Incoming Webhook | Ô Bot · Sửa |
| Tắt bot · xoá Incoming Webhook | Ô Bot · Xoá |
| Thêm bot vào một nhóm, gỡ bot khỏi nhóm | Chủ hoặc quản trị của nhóm đó (không cần quyền Bot) |
| Bật «Đọc tin nhắn» / «Đọc tất cả» cho bot ở một nhóm | Chủ hoặc quản trị của nhóm đó, hoặc người có Bot · Sửa |
| Xem danh sách bot để thêm vào nhóm | Mọi thành viên workspace. Người không có Bot · Xem không thấy địa chỉ webhook của bot |
Quy tắc: từ 05/10/2026 máy chủ kiểm đúng các ô quyền trên cho mọi thao tác với bot. Trước đó ô quyền chỉ ẩn menu. Ai cần quản lý bot thì quản trị tick ô Bot tương ứng cho vai trò của người đó.
🖼️ Hướng dẫn sử dụng (kèm hình ảnh)
1 · Tạo bot

- Mở Tiện ích → Bot, bấm «Tạo bot» ①
Ô tạo bot hiện ra.
- Điền thông tin
Avatar (tuỳ chọn, ảnh tối đa 5MB) · Tên hiển thị: tên mọi người thấy trong nhóm, ví dụ «Trợ lý Kho» · Username (bắt buộc): 3–32 ký tự, chỉ chữ không dấu, số và dấu gạch dưới, bắt đầu bằng chữ, không trùng trong workspace, ví dụ
kho_bot· Mô tả: bot dùng để làm gì. - Bấm «Tạo bot»
Hệ thống mở trang chi tiết của bot vừa tạo.
💡 Một việc một bot. Mỗi hệ thống nối vào (kho, kế toán, AI Agent, Trợ lý CRM) nên có bot riêng. Lý do kỹ thuật ở mục Một token chỉ một chương trình đọc tin.
2 · Lấy token (mã truy cập của bot)
Phần mềm muốn nói chuyện dưới tên bot phải cầm token của bot. Token có dạng bot_ + 43 ký tự.
- Trên trang chi tiết bot, tìm khung «Token API»
Lần đầu khung này chưa có token, vì trang tạo bot không hiện token. Đây là chủ ý, không phải lỗi.
- Bấm «Tạo lại token»
Token mới hiện trong khung vàng «⚠ Token chỉ hiện 1 lần — lưu lại ngay». Bấm nút chép rồi cất vào nơi an toàn (biến môi trường trên máy chạy phần mềm).
Quy tắc: mỗi lần bấm «Tạo lại token», token cũ ngừng chạy ngay lập tức. Mọi phần mềm đang dùng token cũ phải đổi sang token mới. Đóng khung vàng rồi thì không xem lại được token: tạo token khác.
3 · Thêm bot vào nhóm chat
Bot chỉ nhìn thấy tin của những nhóm nó là thành viên. Nhóm không có bot thì bot không nhận được gì.
- Mở nhóm chat, bấm biểu tượng «Xem thông tin» ở góc phải tiêu đề nhóm
Khung thông tin nhóm mở ra bên phải.
- Ở phần «Trong phòng», bấm «Bot»
Con số bên cạnh là số bot đang ở trong nhóm. Popup «Bot trong phòng» hiện ra.
- Ở phần «Thêm vào phòng», bấm «Thêm» cạnh bot cần thêm
Bot chuyển sang nhãn ● Trong phòng. Muốn gỡ thì bấm biểu tượng thùng rác cạnh bot.
Chỉ chủ hoặc quản trị nhóm mới thấy nút Thêm và Gỡ. Thành viên thường mở popup chỉ xem được danh sách. Danh sách gợi ý thêm thành viên thông thường («Thêm thành viên») không có bot: bot chỉ thêm qua popup «Bot trong phòng».
Nhắn riêng với bot: mở một nhóm có bot, sang tab Thành viên, rê chuột vào dòng của bot rồi bấm biểu tượng «Nhắn DM».
4 · Bot nghe những tin nào
Đây là phần quan trọng nhất khi nối bot với phần mềm khác. Bot nhận tin theo một trong hai đường (chi tiết ở Dành cho lập trình viên):
- Webhook: CNV Chat chủ động gửi tin sang địa chỉ web của bạn. Cần địa chỉ công khai trên internet.
- Bot tự hỏi tin mới (
getUpdates): chương trình của bạn hỏi CNV Chat vài giây một lần. Chạy được cả trên máy tính ở nhà hay văn phòng, không cần mở cổng.
| Loại tin trong nhóm có bot | Webhook | Bot tự hỏi tin mới |
|---|---|---|
Tin gọi tên bot: gõ @, chọn bot trong danh sách gợi ý | ✅ Nhận | ✅ Nhận, có đánh dấu bot được gọi tên |
| Tin gõ tay «@ten_bot» mà không chọn từ danh sách | ❌ Không | ✅ Nhận như tin thường |
| Tin thường, không gọi tên bot | ❌ Không, trừ khi bật «Đọc tất cả» | ✅ Nhận mọi tin: chương trình phải tự lọc |
| Bật «Đọc tin nhắn» / «Đọc tất cả» cho bot ở nhóm đó | ✅ Nhận mọi tin của nhóm | Không ảnh hưởng (vốn đã nhận mọi tin) |
| Tin của bot khác, tin của chính bot | ❌ Không bao giờ (chống hai bot nói qua nói lại mãi) | ❌ Không bao giờ |
| Tin hệ thống («A đã thêm B vào nhóm») · sửa tin · thu hồi · thả cảm xúc · chuyển tiếp | ❌ Không | ❌ Không |
| Tin chỉ có ảnh hoặc tệp | Có báo, nhưng không kèm tệp | Có báo, nhưng không kèm tệp |
Công tắc «Đọc tất cả» nằm ở hai chỗ: khung «Nhóm bot tham gia» trên trang chi tiết bot (người có Bot · Sửa), và công tắc «Đọc tin nhắn» cạnh bot trong popup «Bot trong phòng» (chủ hoặc quản trị nhóm). Công tắc này tính riêng từng nhóm.
Quy tắc: bật «Đọc tất cả» là mọi tin của nhóm được gửi sang phần mềm đứng sau bot. Chỉ bật ở nhóm mà mọi thành viên biết và đồng ý có phần mềm đọc tin.
5 · Webhook: bot chuyển tin ra phần mềm ngoài
Khung «Webhook» trên trang chi tiết bot nhận hai ô:
- Webhook URL: địa chỉ web của phần mềm nhận tin.
- Chuỗi bí mật (tuỳ chọn, nên có): dùng để ký từng tin, giúp bên nhận biết chắc tin đến từ CNV Chat.
Bấm «Lưu webhook». Từ đó, mỗi tin thuộc diện «Nhận» ở cột Webhook (bảng trên) được gửi sang địa chỉ này, thường trong vòng 10 giây. Bên nhận trả lời lỗi thì CNV Chat gửi lại, tối đa 5 lần trong khoảng 6 phút. Xem cách kiểm chữ ký.
6 · Gửi thử
Khung «Gửi thử» chỉ dùng được ngay sau khi bạn vừa bấm «Tạo lại token» trên trang này. Gõ một câu rồi bấm Gửi thử: hệ thống tạo một nhóm tên «Demo <tên bot>» có bot làm thành viên, rồi để bot đăng câu đó vào, đúng như phần mềm ngoài sẽ làm. Mỗi lần mở lại trang rồi gửi thử là một nhóm demo mới.
7 · Incoming Webhook: đường dẫn cho phần mềm ngoài gửi tin vào nhóm
Cách đơn giản nhất để một hệ thống khác báo tin vào nhóm, không cần lập trình bot: tạo một đường dẫn bí mật, dán vào phần mềm kia (nhiều phần mềm có sẵn ô «Webhook URL» kiểu Lark / Feishu).
- Bot phải có token và đang ở trong nhóm đích
Làm bước 2 và bước 3 trước.
- Ở khung «Incoming Webhook», bấm «Tạo webhook»
Chọn Nhóm đích (chỉ hiện các nhóm bot đang ở), đặt Tên gợi nhớ (ví dụ «Đơn website»), chọn Bảo mật: Không (chỉ cần URL bí mật) hoặc Chữ ký HMAC. Mở Tuỳ chọn nâng cao nếu muốn bắt buộc từ khoá hoặc giới hạn địa chỉ IP gửi đến.
- Chép URL (và secret nếu chọn chữ ký) trong khung vàng
Secret chỉ hiện một lần. URL có dạng
https://api.cnvwork.com/api/v1/hook/bot/….
Mỗi dòng webhook có cột Dùng lần cuối để biết đường dẫn nào còn chạy. Nút xoay secret cấp secret mới, và tự chuyển webhook sang chế độ Chữ ký HMAC. Nút xoá thì đường dẫn ngừng nhận tin ngay. Từ khoá và IP không sửa được sau khi tạo: cần đổi thì tạo webhook mới.
Quy tắc: ai cầm URL là đăng được tin dưới tên bot vào nhóm đó. Coi URL như mật khẩu. Muốn chắc chắn thì chọn Chữ ký HMAC.
8 · Đặt tên bot trong câu: gọi tên đúng cách
Trong ô soạn tin, gõ @ rồi chọn bot trong danh sách gợi ý (danh sách có cả người lẫn bot trong nhóm). Chỉ khi chọn từ danh sách, tin mới mang dấu «gọi tên bot» thật. Gõ tay «@kho_bot» thì phần lớn trường hợp bot coi như tin thường.
9 · Trợ lý CRM
Bật trợ lý thì bot trả lời câu hỏi về dữ liệu CRM ngay trong nhóm: lead, cơ hội bán, báo giá, hoá đơn, công nợ, doanh số theo kỳ.
- Bot phải có token
Làm bước 2 trước. Workspace phải có gói CRM và đã khai khoá AI (Claude hoặc ChatGPT) ở Hệ thống → Tích hợp. Thiếu gói thì thẻ «Trợ lý CRM» khoá mờ kèm lý do.
- Ở thẻ «Trợ lý CRM» đầu trang chi tiết bot, bấm «Bật trợ lý»
Cần ô Bot · Sửa. Bot bắt đầu trả lời trong khoảng một phút.
- Thêm bot vào nhóm, rồi gọi tên bot kèm câu hỏi
Ví dụ: «@Trợ lý CRM doanh số tháng này», «@Trợ lý CRM hoá đơn quá hạn của công ty ACME». Kể cả khi nhắn riêng với bot cũng gọi tên bot.
- Bot trả lời theo quyền của người hỏi, không theo quyền của bot. Nhân viên chỉ thấy dữ liệu của mình thì bot cũng chỉ trả lời phần đó, dù hỏi giữa nhóm đông người.
- Mỗi người hỏi tối đa 10 câu mỗi phút. Mọi câu hỏi được ghi nhật ký.
- Bot chỉ đọc dữ liệu, không tạo hay sửa gì.
- Bấm «Tắt trợ lý» thì bot thôi trả lời và thôi đọc tin trong vòng khoảng 30 giây.
Quy tắc: bot đã bật Trợ lý CRM thì đừng dùng token của nó cho phần mềm khác. Hai bên sẽ giành tin của nhau: tin lúc tới chỗ này, lúc tới chỗ kia. Tạo bot riêng cho phần mềm khác.
10 · Tắt bot
Khung «Vùng nguy hiểm», nút «Tắt bot»: token và webhook của bot ngừng chạy ngay. Bot vẫn hiện trong danh sách với nhãn «○ Đã tắt» nhưng không bật lại được, và tên username vẫn bị giữ. Cần lại thì tạo bot mới với username khác.
Thiết kế bot hai chiều: nhóm chat ⇄ hệ thống khác
Phần này trả lời câu hỏi: thêm bot vào một nhóm, cho bot gọi API sang hệ thống khác để lấy và trả thông tin thì nên làm thế nào?
Chọn đường nhận tin
| Webhook | Bot tự hỏi tin mới (getUpdates) | |
|---|---|---|
| Máy chạy cầu nối | Phải có địa chỉ HTTPS công khai | Máy nào có internet cũng được (sau modem, NAT, VPN) |
| Độ trễ | 0–10 giây | Theo nhịp hỏi, nên 1–2 giây |
| Tin nhận được | Chỉ tin gọi tên bot (hoặc mọi tin nếu bật «Đọc tất cả») | Mọi tin trong mọi nhóm có bot: tự lọc |
| Cầu nối tắt máy một lúc | CNV Chat gửi lại tối đa 5 lần trong khoảng 6 phút | Tin chờ sẵn tối đa 7 ngày, bật lại là đọc tiếp |
| Hợp với | Máy chủ công ty, dịch vụ đám mây | Máy tính cá nhân, AI Agent cài trên máy |
Tám quy tắc cho một cầu nối chạy ổn
- Một bot, một chương trình đọc tin. Đừng dùng chung token với Trợ lý CRM hoặc với chương trình khác (xem lý do).
- Chỉ trả lời khi được gọi tên. Đi đường hỏi tin mới thì lọc tin có
entitiesloạiMENTIONmangaccountIdcủa chính bot. Đừng trả lời mọi câu trong nhóm. - Chống xử lý trùng. Ghi nhớ
message_id(hoặc headerX-Chat-Event-Idcủa webhook) đã xử lý. Webhook có thể gửi lại cùng một tin. - Trả lời nhanh, việc lâu thì báo trước. Gửi ngay «⏳ Đang xử lý…» (kèm
replyToIdđể vào đúng luồng), xong việc thìeditMessagethay bằng kết quả. Người hỏi biết bot đã nhận, nhóm không bị nhiều tin rác. - Mỗi nhóm làm một việc một lúc. Xếp hàng theo
room_idđể hai câu hỏi liền nhau không trả lời lộn thứ tự. - Tự quyết ai được hỏi gì. Tin chỉ có
sender_id(mã tài khoản chat), không có tên hay quyền. Hệ thống phía sau phải tự giữ danh sáchsender_idhoặcroom_idđược phép, nhất là khi bot đọc được dữ liệu nhạy cảm. - Giữ token như mật khẩu. Để trong biến môi trường, không dán vào chat, không đưa lên Git. Lộ thì bấm «Tạo lại token».
- Báo lỗi bằng câu dễ hiểu. Hệ thống phía sau chết thì trả «⚠️ Chưa tra được, thử lại sau ít phút», đừng đẩy nguyên dòng lỗi kỹ thuật vào nhóm.
Một token chỉ một chương trình đọc tin
Mỗi lần hỏi tin mới, CNV Chat đánh dấu đã đọc ngay những tin vừa trả. Hai chương trình cùng hỏi bằng một token sẽ chia nhau tin: tin nào đến tay chương trình nào là ngẫu nhiên, và không ai báo lỗi. Đó là lý do:
- Bot đã bật Trợ lý CRM đang tự đọc tin bằng chính token của nó. Đừng nối thêm phần mềm khác vào bot đó.
- Muốn vừa có Trợ lý CRM vừa có AI Agent trong cùng một nhóm: tạo hai bot, thêm cả hai vào nhóm. Người dùng gọi tên bot nào thì bot đó trả lời.
Nối AI Agent chạy trên máy của bạn
Ví dụ dưới đây dùng AI Coworker, một ứng dụng AI Agent chạy trên máy tính (macOS, Windows, Linux) có thể tự lập kế hoạch, dùng trình duyệt, đọc ghi tệp, chạy theo lịch. Cách làm áp dụng được cho mọi AI Agent có API kiểu OpenAI (/v1/chat/completions).
Vì sao không dùng thẳng kênh Telegram có sẵn của AI Coworker? API bot của CNV Chat giống Telegram nhưng không khớp hẳn: mã tin là UUID chứ không phải số, phòng là
roomId chứ không phải chat_id, tên lệnh khác (editMessage thay cho editMessageText). Trỏ kênh Telegram của AI Agent vào CNV Chat sẽ không chạy. Cách gọn và chắc là một cầu nối nhỏ chạy cùng máy với AI Agent. Các bước
- Tạo một bot RIÊNG cho AI Agent
Ví dụ username
ai_coworker_bot, tên hiển thị «AI Coworker». Không dùng bot đang bật Trợ lý CRM. - Bấm «Tạo lại token», chép token
Token này sẽ nằm trong biến môi trường
CNV_BOT_TOKENcủa cầu nối. - Thêm bot vào nhóm sẽ làm việc với AI
Nên bắt đầu bằng một nhóm nhỏ chỉ có người tin cậy (xem phần an toàn bên dưới). Không cần bật «Đọc tất cả»: cầu nối tự lọc tin gọi tên bot.
- Cài AI Coworker và bật Agent API
Cài bản máy tính từ aicoworker.net (hoặc bản chạy nền trên Linux). Từ bản 2026.6.26, AI Coworker có Agent API cho chatbot bên ngoài: có khoá riêng, có chuẩn OpenAI
chat/completions. Trong ứng dụng mở phần Agent API, bấm Open docs hoặc Swagger UI để xem địa chỉ (cổng), cách cấp khoá và tên model / agent của máy bạn. Ba thông tin này điền vàoAI_BASE_URL,AI_API_KEY,AI_MODEL. - Tạo một agent riêng cho việc chat
Trong AI Coworker, tạo agent riêng cho nhóm chat, chỉ bật những công cụ thật sự cần, bật chế độ sandbox nếu có. Đừng dùng agent chính đang có toàn quyền trên máy bạn.
- Chạy cầu nối
Cần Node.js 18 trở lên. Lưu đoạn mã dưới đây thành
cnv-bot-bridge.mjsrồi chạy lệnh ở cuối mục. - Thử trong nhóm
Gõ
@, chọn «AI Coworker» trong danh sách gợi ý, gõ câu hỏi. Bot đáp «⏳ Đang xử lý…» rồi thay bằng câu trả lời của AI. - Cho cầu nối chạy thường trực
Máy phải bật và không ngủ. Dùng
pm2,launchd(macOS) hoặcsystemd(Linux) để cầu nối tự chạy lại khi khởi động máy.
Mã cầu nối (Node.js, không cần cài thêm gói nào)
js
// cnv-bot-bridge.mjs — cầu nối Bot CNV Work ⇄ AI Agent chạy trên máy (API kiểu OpenAI).
// Chỉ GỌI RA ngoài (hỏi tin mới mỗi 1,5 giây) ⇒ chạy được sau modem/NAT, không cần mở cổng.
const CHAT = process.env.CNV_CHAT_API || 'https://chatapi.cnvwork.com';
const TOKEN = process.env.CNV_BOT_TOKEN; // token của bot (bot_…)
const AI_BASE = (process.env.AI_BASE_URL || '').replace(/\/$/, ''); // vd http://127.0.0.1:<cổng Agent API>
const AI_KEY = process.env.AI_API_KEY || '';
const AI_MODEL = process.env.AI_MODEL || 'default';
const SYSTEM = process.env.AI_SYSTEM_PROMPT
|| 'Bạn là trợ lý của công ty trong nhóm chat. Trả lời ngắn gọn, bằng tiếng Việt.';
const ALLOW_ROOMS = (process.env.ALLOW_ROOMS || '').split(',').filter(Boolean); // trống = mọi nhóm có bot
const POLL_MS = Number(process.env.POLL_MS || 1500);
if (!TOKEN || !AI_BASE) { console.error('Thiếu CNV_BOT_TOKEN hoặc AI_BASE_URL'); process.exit(1); }
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function bot(method, name, body) {
const r = await fetch(`${CHAT}/bot/${TOKEN}/${name}`, {
method,
headers: { 'Content-Type': 'application/json' },
body: body ? JSON.stringify(body) : undefined,
});
const j = await r.json().catch(() => ({}));
if (r.status === 401) { console.error('Token bot hết hiệu lực — bấm «Tạo lại token» rồi chạy lại.'); process.exit(1); }
if (!r.ok) throw new Error(`${name}: HTTP ${r.status} ${j?.error?.code || ''}`);
return j.result;
}
const me = await bot('GET', 'getMe'); // { id, accountId, username, displayName }
console.log(`Bot @${me.username} đang nghe…`);
const isMine = (e) => e?.type === 'MENTION' && e.accountId === me.accountId;
/** Bỏ đoạn «@Tên bot» khỏi câu, để AI chỉ thấy câu hỏi. */
function question(p) {
let text = p.text || '';
for (const e of (p.entities || []).filter(isMine).sort((a, b) => b.offset - a.offset)) {
text = text.slice(0, e.offset) + text.slice(e.offset + e.length);
}
return text.trim();
}
async function askAI(roomId, q) {
const r = await fetch(`${AI_BASE}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', ...(AI_KEY ? { Authorization: `Bearer ${AI_KEY}` } : {}) },
body: JSON.stringify({
model: AI_MODEL,
user: `cnv-room:${roomId}`, // mỗi nhóm một phiên hội thoại
messages: [{ role: 'system', content: SYSTEM }, { role: 'user', content: q }],
}),
signal: AbortSignal.timeout(5 * 60_000), // AI Agent làm việc lâu tối đa 5 phút
});
if (!r.ok) throw new Error(`AI: HTTP ${r.status}`);
const j = await r.json();
return j.choices?.[0]?.message?.content?.trim() || '(AI không trả lời)';
}
async function handle(p) {
const q = question(p);
if (!q) return;
const wait = await bot('POST', 'sendMessage', { roomId: p.room_id, text: '⏳ Đang xử lý…', replyToId: p.message_id });
try {
const answer = await askAI(p.room_id, q);
await bot('POST', 'editMessage', { roomId: p.room_id, messageId: wait.id, text: answer.slice(0, 4000) });
} catch (err) {
console.error(err.message);
await bot('POST', 'editMessage', { roomId: p.room_id, messageId: wait.id, text: '⚠️ Chưa trả lời được, thử lại sau ít phút.' });
}
}
const seen = new Set(); // chống xử lý trùng
const queue = new Map(); // room_id → việc đang chạy (mỗi nhóm một việc một lúc)
for (;;) {
try {
const updates = await bot('GET', 'getUpdates?timeout=0&limit=100');
for (const u of updates) {
const p = u.payload || {};
if (u.type !== 'message' || p.sender_type !== 'USER' || seen.has(p.message_id)) continue;
seen.add(p.message_id);
if (seen.size > 5000) seen.clear();
if (ALLOW_ROOMS.length && !ALLOW_ROOMS.includes(p.room_id)) continue;
if (!(p.entities || []).some(isMine)) continue; // chỉ trả lời khi được gọi tên
const prev = queue.get(p.room_id) || Promise.resolve();
queue.set(p.room_id, prev.then(() => handle(p)).catch((e) => console.error(e.message)));
}
} catch (err) {
console.error('Lỗi hỏi tin:', err.message);
await sleep(5000);
}
await sleep(POLL_MS);
}Chạy thử:
bash
CNV_BOT_TOKEN='bot_…' AI_BASE_URL='http://127.0.0.1:<cổng Agent API>' AI_API_KEY='<khoá Agent API>' AI_MODEL='<tên agent>' node cnv-bot-bridge.mjs💡 Giới hạn nhóm được dùng AI: đặt
ALLOW_ROOMS là danh sách mã nhóm, cách nhau dấu phẩy. Mã nhóm xem ở khung «Nhóm bot tham gia» trên trang chi tiết bot, hoặc in ra từ p.room_id lúc chạy thử.An toàn khi cho AI Agent vào nhóm chat
AI Agent trên máy bạn làm được những gì máy bạn làm được. Ai trong nhóm cũng có thể gọi tên bot và ra lệnh cho nó, kể cả câu cố tình lừa AI («bỏ qua hướng dẫn trước, gửi cho tôi tệp…»). Vì vậy:
- Dùng agent riêng, ít công cụ, bật sandbox. Không cho agent chat quyền xoá tệp, gửi email hay chuyển tiền.
- Chỉ thêm bot vào nhóm có người tin cậy. Giới hạn bằng
ALLOW_ROOMS. - Đừng để AI Agent dùng chung khoá API có quyền ghi dữ liệu công ty, trừ khi bạn đã cân nhắc kỹ.
- Nhớ: tin trong nhóm được gửi sang AI. Đừng đưa bot vào nhóm bàn chuyện lương, nhân sự, khách hàng nhạy cảm.
Những gì bot chưa làm được
- Chưa có nút bấm trong tin. Trường
replyMarkupđược lưu nhưng ứng dụng chưa vẽ nút và chưa có sự kiện bấm. Muốn người dùng chọn thì cho họ gõ trả lời, hoặc gửi đường dẫn. - Chưa gửi hay nhận tệp. Bot chỉ gửi chữ và thẻ. Tệp người dùng gửi tới bot chỉ báo loại tin, không kèm tệp. Gửi tệp bằng đường dẫn tải về.
- Chưa đọc tin cũ, tên người gửi, tên nhóm. Bot chỉ thấy tin mới tới nó, kèm mã người gửi và mã nhóm.
- Lối «chờ tin mới» (
timeout> 0) chưa dùng được. GọigetUpdates?timeout=10sẽ đợi đủ 10 giây rồi trả về rỗng, tin mới chỉ hiện ở lượt hỏi kế tiếp (đo 05/10/2026). Hỏi vớitimeout=0mỗi 1–2 giây như mã mẫu.
Dành cho lập trình viên
Địa chỉ và cách xác thực
| Địa chỉ | https://chatapi.cnvwork.com/bot/<token>/<phương thức> (gọi được từ internet) |
| Xác thực | Token nằm ngay trong đường dẫn. Không cần header xác thực |
| Kiểu gọi | getMe, getUpdates, getWebhookInfo: GET. Mọi phương thức khác: POST, thân JSON |
| Tên trường | Thân gửi lên viết kiểu camelCase (roomId, replyToId). Tin nhận về (payload) viết kiểu snake_case (room_id, sender_id) |
| Phản hồi đúng | { "ok": true, "result": … } |
| Phản hồi lỗi | { "error": { "code", "message", "traceId" } } (không có ok) |
| Đặc tả máy đọc | Swagger · https://chatapi.cnvwork.com/v3/api-docs |
Gọi thử cho chắc:
bash
curl -s https://chatapi.cnvwork.com/bot/$CNV_BOT_TOKEN/getMejson
{ "ok": true, "result": { "id": "01a10b36-52de-…", "accountId": "01a10b36-52dd-…", "username": "huongdan_demo_bot", "displayName": "Bot thử hướng dẫn" } }accountId là mã tài khoản chat của bot. Mã này xuất hiện trong entities khi có người gọi tên bot, và trong senderId của tin bot gửi.
Các phương thức
| Phương thức | Dùng để | Thân / tham số |
|---|---|---|
GET getMe | Thông tin bot | — |
GET getUpdates | Hỏi tin mới | ?timeout=0&limit=100 (limit 1–100) |
POST sendMessage | Gửi tin vào nhóm | { roomId, text, parseMode?, replyToId?, metadata? } |
POST editMessage | Sửa tin của chính bot | { roomId, messageId, text, parseMode? } |
POST deleteMessage | Thu hồi tin của chính bot | { roomId, messageId } |
POST setWebhook | Đặt webhook bằng token | { url, secretToken } |
POST deleteWebhook | Gỡ webhook | — |
GET getWebhookInfo | Xem webhook đang đặt | — |
Bot không tự tạo nhóm, tự vào nhóm hay rời nhóm được: người quản lý nhóm thêm bot (mục 3). Bot gửi vào nhóm mình không ở thì nhận lỗi 403 NOT_ROOM_MEMBER.
Hỏi tin mới (getUpdates)
bash
curl -s "https://chatapi.cnvwork.com/bot/$CNV_BOT_TOKEN/getUpdates?timeout=0&limit=100"Kết quả đo thật ngày 05/10/2026: một tin thường và một tin có gọi tên bot.
json
{
"ok": true,
"result": [
{
"update_id": "01a10b36-5411-7451-9d62-af06b533629c",
"type": "message",
"created_at": "2026-10-05T08:37:52.273298Z",
"payload": {
"message_id": "01a10b36-53ff-7088-9baf-729c07425b78",
"room_id": "01a10b36-53d7-7c47-82a8-4a3382812021",
"sender_id": "019e4ef0-df80-7f18-898b-de3a66252b26",
"sender_type": "USER",
"type": "TEXT",
"text": "trưa nay ăn gì",
"entities": [],
"created_at": "2026-10-05T08:37:52.255007064Z"
}
},
{
"update_id": "01a10b36-543d-7e20-bce0-d4af645eec0a",
"type": "message",
"created_at": "2026-10-05T08:37:52.317900Z",
"payload": {
"message_id": "01a10b36-5431-7bb9-96bf-12041245c5f0",
"room_id": "01a10b36-53d7-7c47-82a8-4a3382812021",
"sender_id": "019e4ef0-df80-7f18-898b-de3a66252b26",
"sender_type": "USER",
"type": "TEXT",
"text": "@Bot thử hướng dẫn xin chào, doanh số tuần này?",
"entities": [
{ "type": "MENTION", "offset": 0, "length": 4, "botUsername": "Bot" },
{ "type": "MENTION", "offset": 0, "length": 18, "accountId": "01a10b36-52dd-7802-b53f-65a34cb3463e" }
],
"created_at": "2026-10-05T08:37:52.305705907Z"
}
}
]
}Những điều phải biết:
- Tin được đánh dấu đã đọc ngay khi trả về. Không cần (và không nên) gửi
offset:offsetchỉ lọc theo mã, không phải xác nhận. Chương trình sập giữa lúc xử lý là mất các tin của lượt đó. - Mỗi token chỉ một chương trình hỏi. Hai nơi cùng hỏi sẽ chia nhau tin.
- Nhận mọi tin của mọi nhóm có bot. Nhận diện tin gọi tên bot bằng
entitiescótype: "MENTION"vàaccountIdbằngaccountIdcủa bot. Dòng MENTION cóbotUsernamemà không cóaccountIdlà hệ thống tự đoán từ chữ, bỏ qua được. offsetvàlengthcủa entity tính theo đơn vị UTF-16, giốngString.prototype.slicecủa JavaScript.- Tin chờ đọc giữ tối đa 7 ngày. Tin đã đọc giữ 24 giờ rồi xoá.
- Dùng
timeout=0. Giá trị lớn hơn hiện chưa trả tin sớm (xem giới hạn).
Gửi tin (sendMessage)
bash
curl -s -X POST "https://chatapi.cnvwork.com/bot/$CNV_BOT_TOKEN/sendMessage" \
-H 'Content-Type: application/json' \
-d '{"roomId":"01a10b36-53d7-7c47-82a8-4a3382812021","text":"<b>Tồn kho SP-01</b>: 120 cái<br>Kho: Hà Nội","parseMode":"HTML","replyToId":"01a10b36-5431-7bb9-96bf-12041245c5f0"}'| Trường | Ý nghĩa |
|---|---|
roomId | Bắt buộc. Mã nhóm (lấy từ payload.room_id) |
text | Nội dung. Nên giữ dưới 4.000 ký tự |
parseMode | NONE (mặc định) · HTML · MARKDOWN_V2. Viết hoa đúng như vậy: html hay Markdown trả lỗi 500 |
replyToId | Mã tin cần trả lời. Tin cùng nhóm thì bot trả lời vào luồng của tin đó |
metadata | Thẻ hiển thị đẹp (xem bên dưới) |
- HTML nhận các thẻ
b strong i em u s del code pre blockquote a br(achỉ nhậnhttp,https,mailto,tel), các thẻ khác bị bỏ. Xuống dòng dùng<br>. - MARKDOWN_V2 nhận
*đậm*_nghiêng___gạch chân__~gạch ngang~`mã`[chữ](https://…). Ký tự đặc biệt cần\đứng trước, ví dụ\-. - Bot gõ «@Tên người» thì chỉ là chữ: người đó không nhận thông báo được nhắc tên.
- Dãy số dài trong tin có thể bị hiểu nhầm là số điện thoại và hiện thành liên kết gọi.
Phản hồi trả lại result.id (mã tin của bot) để sau này editMessage hoặc deleteMessage.
Thẻ hiển thị. Gửi metadata dạng phẳng { "card_kind": "notification", "card": { … } }, không bọc thêm lớp nào:
json
{
"roomId": "01a10b36-53d7-7c47-82a8-4a3382812021",
"text": "Đơn hàng mới #SO-1024",
"metadata": {
"card_kind": "notification",
"card": {
"title": "Đơn hàng mới #SO-1024",
"intro": "Khách vừa đặt qua website",
"fields": [{ "label": "Số tiền", "value": "1.200.000đ" }, { "label": "Khách", "value": "Nguyễn Văn A" }],
"actions": [{ "label": "Mở đơn", "url": "https://shop.example.com/orders/1024" }]
}
}
}Thẻ đọc các trường: title, intro, fields[{label,value}], status, priority, tags[], code, columns + rows (bảng), actions[{label,url}] (nút đầu tiên thành nút mở liên kết). text là chữ dự phòng cho nơi không vẽ được thẻ. Thẻ không sửa được bằng editMessage, chỉ chữ sửa được.
Webhook nhận tin (CNV Chat → phần mềm của bạn)
Đặt ở khung «Webhook» trên trang bot (hoặc gọi setWebhook). Mỗi tin thuộc diện nhận (mục 4) được gửi như sau:
http
POST /duong-dan-cua-ban HTTP/1.1
Content-Type: application/json
X-Chat-Event-Id: 0199b2c4-7e2b-7f00-a1b2-c3d4e5f60718
X-Chat-Event-Type: message
X-Chat-Signature: sha256=9c1f…(64 ký tự hex)
{"event":"message","message_id":"01a10b36-5431-…","room_id":"01a10b36-53d7-…","sender_id":"019e4ef0-df80-…","sender_type":"USER","type":"TEXT","text":"@Trợ lý Kho tồn kho SP-01?","created_at":"2026-10-05T08:38:53.7Z"}| Điều | Chi tiết |
|---|---|
| Thời điểm | 0–10 giây sau khi có tin |
| Thành công | Trả mã 2xx trong 10 giây. Nội dung trả về bị bỏ qua: muốn trả lời trong nhóm thì gọi sendMessage |
| Gửi lại | Lỗi hoặc quá giờ thì gửi lại sau khoảng 1 giây, 5 giây, 30 giây, 5 phút. Hỏng 5 lần thì bỏ tin đó |
| Trùng tin | Có thể nhận một tin hai lần: chống trùng bằng X-Chat-Event-Id (giữ nguyên qua các lần gửi lại) |
| Thứ tự | Không bảo đảm đúng thứ tự |
Khác với getUpdates | Thân không có entities, vì webhook vốn chỉ gửi tin gọi tên bot (hoặc mọi tin nếu bật «Đọc tất cả») |
| Chữ ký | Chỉ có khi đặt chuỗi bí mật: sha256= + HMAC-SHA256 (khoá = chuỗi bí mật, nội dung = nguyên văn thân request), dạng hex chữ thường |
Kiểm chữ ký (Node.js). Phải tính trên thân gốc, đừng JSON.parse rồi stringify lại vì thứ tự khoá có thể khác:
js
import crypto from 'node:crypto';
function verifyChatSignature(rawBody, header, secret) {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return typeof header === 'string' && header.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected));
}Incoming Webhook (gửi tin vào nhóm bằng đường dẫn)
URL lấy ở mục 7: https://api.cnvwork.com/api/v1/hook/bot/<khoá>. Gửi POST thân JSON, tối đa 20KB.
Chế độ chỉ cần URL bí mật:
bash
curl -s -X POST 'https://api.cnvwork.com/api/v1/hook/bot/<khoá>' \
-H 'Content-Type: application/json' \
-d '{"msg_type":"text","content":{"text":"Đơn #SO-1024 đã thanh toán"}}'Chế độ Chữ ký HMAC, kiểu Lark: sign = base64( HMAC-SHA256( khoá = "<timestamp>\n<secret>", nội dung rỗng ) ), timestamp tính bằng giây, lệch giờ tối đa 1 giờ. timestamp và sign nằm trong thân JSON:
bash
TS=$(date +%s); SECRET='<secret>'
SIGN=$(printf '' | openssl dgst -sha256 -hmac "$(printf '%s\n%s' "$TS" "$SECRET")" -binary | base64)
curl -s -X POST 'https://api.cnvwork.com/api/v1/hook/bot/<khoá>' -H 'Content-Type: application/json' \
-d "{\"timestamp\":\"$TS\",\"sign\":\"$SIGN\",\"msg_type\":\"interactive\",\"card\":{\"title\":\"Đơn hàng mới\",\"intro\":\"Khách vừa đặt\",\"fields\":[{\"label\":\"Số tiền\",\"value\":\"1.200.000đ\"}]}}"- Chữ:
content.texthoặctextở ngoài cùng. Thẻ:msg_type: "interactive"kèmcard(cùng các trường thẻ như trên). - Từ khoá (nếu đặt): tin phải chứa ít nhất một từ khoá, không phân biệt hoa thường, tính cả
card.title. - IP (nếu đặt): nhận IP cụ thể, dạng
4.5.6.*hoặc dải1.2.3.0/24(IPv4).
Mã code trả về | Nghĩa |
|---|---|
0 | Đã đăng vào nhóm |
19001 (HTTP 404) | Khoá sai, hoặc webhook đã xoá hay tắt |
19021 | Sai chữ ký hoặc timestamp lệch quá 1 giờ |
19022 | IP gửi đến không nằm trong danh sách cho phép |
19024 | Thiếu từ khoá bắt buộc |
9499 | Nội dung rỗng hoặc quá 20KB |
19099 | Không gửi được vào nhóm: bot không còn trong nhóm, bot đã tắt, hoặc token đã đổi |
Bảng mã lỗi của API bot
| HTTP | error.code | Nghĩa và cách xử lý |
|---|---|---|
| 401 | BOT_TOKEN_INVALID | Token sai hoặc đã bị «Tạo lại token». Lấy token mới |
| 404 | BOT_NOT_FOUND | Bot đã bị tắt |
| 403 | NOT_ROOM_MEMBER | Bot không ở trong nhóm đó. Nhờ quản trị nhóm thêm bot |
| 403 | FORBIDDEN | Sửa hoặc thu hồi tin không phải của bot |
| 400 | MESSAGE_RECALLED | Tin đã bị thu hồi |
| 400 | VALIDATION_FAILED | Mã sai dạng (không phải UUID) hoặc thân JSON hỏng |
| 404 | ROOM_NOT_FOUND · MESSAGE_NOT_FOUND | Nhóm hoặc tin không tồn tại |
| 500 | INTERNAL_ERROR | Thường do parseMode viết sai hoặc thiếu roomId. Kiểm lại thân request |
Mọi phản hồi có header X-Trace-Id. Báo sự cố cho CNV thì gửi kèm mã này, đừng gửi token.
Hỏi nhanh — đáp nhanh
| Bạn gặp | Nguyên nhân và cách xử lý |
|---|---|
| Không thấy mục Bot trong menu | Vai trò thiếu ô Bot · Xem, hoặc workspace chưa có Chat nội bộ. |
| Bấm Tạo bot / Lưu webhook thì báo «chưa có quyền «Bot · …»» | Vai trò thiếu đúng ô đó trong Phân quyền. |
| Tạo bot xong không thấy token | Đúng thiết kế. Vào khung «Token API», bấm «Tạo lại token». |
| Gọi tên bot trong nhóm mà phần mềm không nhận được | Kiểm theo thứ tự: bot đã ở trong nhóm chưa (mục 3) · có chọn bot từ danh sách gợi ý sau @ không · webhook đã lưu đúng URL chưa · có chương trình nào khác đang đọc tin bằng cùng token (Trợ lý CRM?). |
| Phần mềm nhận cả những tin không gọi tên bot | Đang dùng đường hỏi tin mới: đường này nhận mọi tin, phải tự lọc theo entities. Hoặc nhóm đang bật «Đọc tất cả». |
| Bot lúc trả lời lúc không | Hai chương trình cùng đọc tin bằng một token. Mỗi chương trình một bot. |
| Bot không gửi được vào nhóm (403 NOT_ROOM_MEMBER) | Bot chưa được thêm vào nhóm đó, hoặc đã bị gỡ. |
| Không bật được «Đọc tin nhắn» cho bot trong nhóm | Chỉ chủ hoặc quản trị nhóm (hoặc người có Bot · Sửa) bật được. |
| Trợ lý CRM không trả lời | Bot chưa có token · workspace chưa có gói CRM hoặc khoá AI · chưa gọi tên bot · người hỏi không có quyền xem dữ liệu đó. |
| Lỡ để lộ token | Bấm «Tạo lại token» ngay. Token cũ chết lập tức. |
| Muốn bot có nút bấm, gửi tệp, đọc tin cũ | Chưa hỗ trợ — xem những gì bot chưa làm được. |
Xem thêm
- Chat nội bộ: nhóm, luồng, nhắc tên, tin của bot hệ thống.
- Tiện ích: Nhóm chat chung, Mẫu thông báo.
- Automation: gửi tin vào nhóm từ bot theo luồng tự động, không cần lập trình.
- API Keys: khi phần mềm khác cần đọc hoặc ghi dữ liệu CRM, không phải nói chuyện trong nhóm chat.
- Hệ thống & Phân quyền: bảng Phân quyền, dòng Bot.
