@warriorteam/redai-zalo-sdk
Version:
Comprehensive TypeScript/JavaScript SDK for Zalo APIs - Official Account v3.0, ZNS with Full Type Safety, Consultation Service, Broadcast Service, Group Messaging with List APIs, Social APIs, Enhanced Article Management, Promotion Service v3.0 with Multip
513 lines (417 loc) • 15.7 kB
Markdown
`ConsultationService` là dịch vụ chuyên dụng để gửi tin nhắn tư vấn (Customer Service) qua Zalo Official Account. Tin nhắn tư vấn cho phép OA gửi tin nhắn chủ động đến người dùng trong khung thời gian nhất định để hỗ trợ và tư vấn khách hàng.
**Endpoint API**: `https://openapi.zalo.me/v3.0/oa/message/cs`
- **Khung thời gian**: Chỉ được gửi trong vòng **48 giờ** kể từ khi người dùng tương tác cuối cùng với OA
- **Tương tác bao gồm**:
- Gửi tin nhắn đến OA
- Nhấn button/quick reply
- Gọi điện thoại từ OA
- Truy cập website từ OA
- **Mục đích**: Phải liên quan đến tư vấn, hỗ trợ khách hàng
- **Bao gồm**: Trả lời câu hỏi, hướng dẫn sử dụng, hỗ trợ kỹ thuật
- **Không được**: Chứa nội dung quảng cáo trực tiếp
- Người dùng phải đã follow OA
- Người dùng không được block OA
- Người dùng phải có tương tác gần đây với OA
- Không giới hạn số lượng tin nhắn tư vấn trong ngày
- Cần tuân thủ nguyên tắc không spam
```typescript
import { ConsultationService } from "@warriorteam/redai-zalo-sdk";
import { ZaloClient } from "@warriorteam/redai-zalo-sdk";
const client = new ZaloClient();
const consultationService = new ConsultationService(client);
```
```typescript
// Gửi tin nhắn văn bản tư vấn
const response = await consultationService.sendTextMessage(
accessToken,
"user-id-here",
"Xin chào! Tôi có thể hỗ trợ gì cho bạn?"
);
console.log("Message ID:", response.message_id);
console.log("User ID:", response.user_id);
console.log("Quota info:", response.quota);
```
**Tham số:**
- `accessToken`: Access token của OA
- `userId`: ID người nhận
- `text`: Nội dung tin nhắn (tối đa 2000 ký tự)
**Giới hạn:**
- Nội dung không được để trống
- Tối đa 2000 ký tự
```typescript
// Gửi hình ảnh tư vấn bằng URL
const response = await consultationService.sendImageMessage(
accessToken,
"user-id-here",
"https://example.com/support-image.jpg",
"Hướng dẫn sử dụng sản phẩm" // Text tùy chọn
);
```
**Tham số:**
- `accessToken`: Access token của OA
- `userId`: ID người nhận
- `imageUrl`: URL hình ảnh (jpg, png, tối đa 1MB)
- `text`: Tiêu đề ảnh (tùy chọn, tối đa 2000 ký tự)
```typescript
// Gửi hình ảnh đã upload trước đó
const response = await consultationService.sendImageByAttachmentId(
accessToken,
"user-id-here",
"attachment-id-from-upload-api",
"Hướng dẫn chi tiết"
);
```
**Tham số:**
- `accessToken`: Access token của OA
- `userId`: ID người nhận
- `attachmentId`: ID ảnh đã upload
- `text`: Tiêu đề ảnh (tùy chọn)
**Lưu ý:**
- Chỉ sử dụng 1 trong 2: `url` hoặc `attachment_id`
- Định dạng hỗ trợ: jpg, png
- Dung lượng tối đa: 1MB
- Kích thước tối ưu: 16:9, vùng safe zone 14:9
```typescript
// Gửi ảnh GIF động
const response = await consultationService.sendGifMessage(
accessToken,
"user-id-here",
"https://example.com/animation.gif",
400, // width
300, // height
"Hướng dẫn động" // text tùy chọn
);
```
**Tham số:**
- `accessToken`: Access token của OA
- `userId`: ID người nhận
- `gifUrl`: URL file GIF
- `width`: Chiều rộng (bắt buộc cho GIF)
- `height`: Chiều cao (bắt buộc cho GIF)
- `text`: Tiêu đề (tùy chọn)
### 4. Gửi File Đính Kèm
```typescript
// Gửi file đính kèm (cần upload file trước)
const response = await consultationService.sendFileMessage(
accessToken,
"user-id-here",
"file-token-from-upload-api"
);
```
**Tham số:**
- `accessToken`: Access token của OA
- `userId`: ID người nhận
- `fileToken`: Token file đã upload
**Lưu ý:** Cần sử dụng API upload file trước để lấy token
```typescript
// Gửi sticker
const response = await consultationService.sendStickerMessage(
accessToken,
"user-id-here",
"bfe458bf64fa8da4d4eb"
);
```
**Tham số:**
- `accessToken`: Access token của OA
- `userId`: ID người nhận
- `stickerAttachmentId`: ID sticker
**Nguồn sticker:**
- Website: https://stickers.zaloapp.com/
- Video hướng dẫn: https://vimeo.com/649330161
```typescript
// Gửi template yêu cầu thông tin
const response = await consultationService.sendRequestUserInfoMessage(
accessToken,
"user-id-here",
"OA Chatbot (Testing)",
"Đang yêu cầu thông tin từ bạn",
"https://developers.zalo.me/web/static/zalo.png"
);
```
**Tham số:**
- `accessToken`: Access token của OA
- `userId`: ID người nhận
- `title`: Tiêu đề (tối đa 100 ký tự)
- `subtitle`: Tiêu đề phụ (tối đa 500 ký tự)
- `imageUrl`: URL hình ảnh
Tất cả các API đều trả về response theo format sau:
```typescript
interface SendMessageResponse {
message_id: string;
user_id: string;
quota?: {
quota_type: "reply" | "sub_quota" | "purchase_quota" | "reward_quota";
remain?: string;
total?: string;
expired_date?: string;
owner_type?: "OA" | "App";
owner_id?: string;
};
}
```
**Các loại quota:**
1. **reply**: Tin trong khung 8 tin 48h
2. **sub_quota**: Tin miễn phí theo gói
3. **purchase_quota**: Tin trong gói tính năng lẻ
4. **reward_quota**: Tin trong hạn mức Redeem code
5. **Không có quota**: Tin tính phí
```typescript
import { ZaloSDKError } from "@warriorteam/redai-zalo-sdk";
try {
const response = await consultationService.sendTextMessage(
accessToken,
"user-id",
"Hello!"
);
console.log("Sent successfully:", response.message_id);
} catch (error) {
if (error instanceof ZaloSDKError) {
console.error("Zalo API Error:", error.message);
console.error("Error Code:", error.code);
// Xử lý các lỗi phổ biến
switch (error.code) {
case -216:
console.log("Access token không hợp lệ");
break;
case -201:
console.log("Tham số không hợp lệ");
break;
case -223:
console.log("Đã vượt quá quota");
break;
default:
console.log("Lỗi khác:", error.message);
}
} else {
console.error("Unexpected error:", error);
}
}
```
```typescript
class CustomerSupportBot {
constructor(private consultationService: ConsultationService) {}
async handleUserQuestion(accessToken: string, userId: string, question: string) {
try {
// Phân tích câu hỏi và tạo phản hồi
const response = this.generateResponse(question);
// Gửi tin nhắn tư vấn
await this.consultationService.sendTextMessage(
accessToken,
userId,
response
);
// Gửi thêm hình ảnh hướng dẫn nếu cần
if (this.needsVisualGuide(question)) {
await this.consultationService.sendImageMessage(
accessToken,
userId,
"https://example.com/guide-image.jpg",
"Hướng dẫn chi tiết"
);
}
// Gửi file hướng dẫn nếu cần
if (this.needsDetailedGuide(question)) {
// Giả sử đã upload file và có token
const fileToken = "uploaded-file-token";
await this.consultationService.sendFileMessage(
accessToken,
userId,
fileToken
);
}
} catch (error) {
console.error("Failed to send consultation message:", error);
// Gửi tin nhắn lỗi cho user
try {
await this.consultationService.sendTextMessage(
accessToken,
userId,
"Xin lỗi, hệ thống đang gặp sự cố. Vui lòng thử lại sau."
);
} catch (fallbackError) {
console.error("Failed to send fallback message:", fallbackError);
}
}
}
private generateResponse(question: string): string {
// Logic tạo phản hồi dựa trên câu hỏi
if (question.includes("đăng nhập")) {
return "Để đăng nhập, bạn vui lòng làm theo các bước sau:\n1. Mở ứng dụng\n2. Nhấn 'Đăng nhập'\n3. Nhập thông tin tài khoản";
}
if (question.includes("thanh toán")) {
return "Về vấn đề thanh toán, chúng tôi hỗ trợ các phương thức:\n• Thẻ tín dụng\n• Chuyển khoản\n• Ví điện tử";
}
if (question.includes("hỗ trợ") || question.includes("help")) {
return "Tôi có thể hỗ trợ bạn về:\n1. Đăng nhập tài khoản\n2. Thanh toán\n3. Sử dụng sản phẩm\n4. Khắc phục sự cố\n\nBạn cần hỗ trợ về vấn đề nào?";
}
return "Cảm ơn bạn đã liên hệ. Chúng tôi sẽ hỗ trợ bạn ngay! Vui lòng mô tả chi tiết vấn đề bạn gặp phải.";
}
private needsVisualGuide(question: string): boolean {
return question.includes("hướng dẫn") ||
question.includes("cách làm") ||
question.includes("how to");
}
private needsDetailedGuide(question: string): boolean {
return question.includes("tài liệu") ||
question.includes("manual") ||
question.includes("chi tiết");
}
}
```
```typescript
import express from 'express';
import { ConsultationService } from '@warriorteam/redai-zalo-sdk';
const app = express();
app.use(express.json());
// Xử lý tin nhắn từ webhook
app.post('/webhook', async (req, res) => {
const event = req.body;
if (event.event_name === 'user_send_text') {
const userId = event.sender.id;
const userMessage = event.message.text;
// Kiểm tra xem có phải câu hỏi cần hỗ trợ không
if (userMessage.includes('help') || userMessage.includes('hỗ trợ')) {
// Gửi tin nhắn tư vấn
await consultationService.sendTextMessage(
accessToken,
userId,
"Tôi có thể hỗ trợ bạn về các vấn đề sau:\n1. Đăng nhập\n2. Thanh toán\n3. Sử dụng sản phẩm\nBạn cần hỗ trợ về vấn đề nào?"
);
// Gửi sticker thân thiện
await consultationService.sendStickerMessage(
accessToken,
userId,
"friendly-sticker-id"
);
}
// Xử lý yêu cầu thông tin
if (userMessage.includes('thông tin') || userMessage.includes('profile')) {
await consultationService.sendRequestUserInfoMessage(
accessToken,
userId,
"Yêu cầu thông tin",
"Để hỗ trợ bạn tốt hơn, vui lòng chia sẻ thông tin",
"https://example.com/info-request.png"
);
}
}
res.status(200).send('OK');
});
```
```typescript
// Kiểm tra thời gian tương tác cuối cùng trước khi gửi
const lastInteraction = await getUserLastInteraction(userId);
const hoursSinceLastInteraction = (Date.now() - lastInteraction) / (1000 * 60 * 60);
if (hoursSinceLastInteraction > 48) {
console.log("Không thể gửi tin nhắn tư vấn - quá 48 giờ");
return;
}
```
```typescript
// Kiểm tra quota từ response
const response = await consultationService.sendTextMessage(
accessToken,
userId,
"Hello!"
);
if (response.quota) {
console.log(`Quota type: ${response.quota.quota_type}`);
console.log(`Quota remaining: ${response.quota.remain}/${response.quota.total}`);
// Cảnh báo khi quota sắp hết
if (response.quota.remain && parseInt(response.quota.remain) < 5) {
console.warn("Quota is running low!");
}
// Kiểm tra ngày hết hạn
if (response.quota.expired_date) {
console.log(`Quota expires: ${response.quota.expired_date}`);
}
}
```
```typescript
async function sendWithRetry(
consultationService: ConsultationService,
accessToken: string,
userId: string,
text: string,
maxRetries = 3
) {
for (let i = 0; i < maxRetries; i++) {
try {
return await consultationService.sendTextMessage(accessToken, userId, text);
} catch (error) {
console.log(`Attempt ${i + 1} failed:`, error.message);
if (i === maxRetries - 1) throw error;
// Đợi trước khi retry (exponential backoff)
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
```
```typescript
class RateLimitedConsultationService {
private lastSent = new Map<string, number>();
private readonly minInterval = 1000; // 1 giây giữa các tin nhắn
constructor(private consultationService: ConsultationService) {}
async sendTextMessage(accessToken: string, userId: string, text: string) {
const now = Date.now();
const lastSentTime = this.lastSent.get(userId) || 0;
if (now - lastSentTime < this.minInterval) {
throw new Error(`Rate limit exceeded for user ${userId}`);
}
const result = await this.consultationService.sendTextMessage(accessToken, userId, text);
this.lastSent.set(userId, now);
return result;
}
}
```
| Method | Description | Parameters |
|--------|-------------|------------|
| `sendTextMessage` | Gửi tin nhắn văn bản | `accessToken`, `userId`, `text` |
| `sendImageMessage` | Gửi ảnh bằng URL | `accessToken`, `userId`, `imageUrl`, `text?` |
| `sendImageByAttachmentId` | Gửi ảnh bằng attachment ID | `accessToken`, `userId`, `attachmentId`, `text?` |
| `sendGifMessage` | Gửi ảnh GIF | `accessToken`, `userId`, `gifUrl`, `width`, `height`, `text?` |
| `sendFileMessage` | Gửi file đính kèm | `accessToken`, `userId`, `fileToken` |
| `sendStickerMessage` | Gửi sticker | `accessToken`, `userId`, `stickerAttachmentId` |
| `sendRequestUserInfoMessage` | Yêu cầu thông tin người dùng | `accessToken`, `userId`, `title`, `subtitle`, `imageUrl` |
1. **Tuân thủ chính sách**: Chỉ gửi tin nhắn tư vấn thực sự, không spam
2. **Theo dõi quota**: Kiểm tra quota thường xuyên để tránh vượt giới hạn
3. **Xử lý lỗi**: Luôn có cơ chế xử lý lỗi phù hợp
4. **Logging**: Ghi log để theo dõi và debug
5. **Rate limiting**: Tránh gửi quá nhiều tin nhắn trong thời gian ngắn
6. **48h rule**: Chỉ gửi trong vòng 48h sau tương tác cuối của user
7. **File upload**: Cần upload file trước khi gửi file message
8. **Sticker source**: Lấy sticker ID từ https://stickers.zaloapp.com/
- [Zalo Official Account API Documentation](https://developers.zalo.me/docs/api/official-account-api)
- [Consultation Message API](https://developers.zalo.me/docs/api/official-account-api/gui-tin-nhan-tu-van)
- [Upload File API](https://developers.zalo.me/docs/api/official-account-api/upload-file)
- [Sticker Collection](https://stickers.zaloapp.com/)
- [Video Guide](https://vimeo.com/649330161)