@warriorteam/redai-zalo-sdk 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +265 -0
- package/CHANGELOG.md +154 -0
- package/README.md +104 -2
- package/SERVICES_ADDED.md +540 -0
- package/docs/ARTICLE_MANAGEMENT.md +336 -0
- package/docs/CONSULTATION_SERVICE.md +330 -0
- package/docs/GROUP_MANAGEMENT.md +232 -0
- package/docs/WEBHOOK_EVENTS.md +806 -0
- package/examples/consultation-service-example.ts +390 -0
- package/package.json +18 -5
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# Article Management Service
|
|
2
|
+
|
|
3
|
+
The `ArticleService` and `VideoUploadService` provide comprehensive article management capabilities for Zalo Official Account (OA).
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
### ArticleService
|
|
8
|
+
- ✅ Create normal articles with rich content
|
|
9
|
+
- ✅ Create video articles
|
|
10
|
+
- ✅ Update existing articles
|
|
11
|
+
- ✅ Get article details and lists
|
|
12
|
+
- ✅ Remove articles
|
|
13
|
+
- ✅ Track article creation/update progress
|
|
14
|
+
- ✅ Comprehensive validation
|
|
15
|
+
|
|
16
|
+
### VideoUploadService
|
|
17
|
+
- ✅ Upload video files for articles
|
|
18
|
+
- ✅ Check video processing status
|
|
19
|
+
- ✅ Wait for upload completion with polling
|
|
20
|
+
- ✅ Upload videos from URLs
|
|
21
|
+
- ✅ Get video information
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
1. **OA Requirements:**
|
|
26
|
+
- OA must have permission to create articles
|
|
27
|
+
- Access token must have "manage_article" scope
|
|
28
|
+
- OA must have active status and be verified
|
|
29
|
+
|
|
30
|
+
2. **Content Limits:**
|
|
31
|
+
- Title: max 150 characters
|
|
32
|
+
- Author: max 50 characters (normal articles only)
|
|
33
|
+
- Description: max 300 characters
|
|
34
|
+
- Image size: max 1MB per image
|
|
35
|
+
- Video size: max 50MB per video
|
|
36
|
+
- Video formats: MP4, AVI only
|
|
37
|
+
|
|
38
|
+
## Usage
|
|
39
|
+
|
|
40
|
+
### Initialize SDK
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { ZaloSDK } from 'redai-zalo-sdk';
|
|
44
|
+
|
|
45
|
+
const sdk = new ZaloSDK({
|
|
46
|
+
appId: 'your_app_id',
|
|
47
|
+
appSecret: 'your_app_secret',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const article = sdk.article;
|
|
51
|
+
const videoUpload = sdk.videoUpload;
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 1. Create Normal Article
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { ArticleType, ArticleStatus, CommentStatus, CoverType, BodyItemType } from 'redai-zalo-sdk';
|
|
58
|
+
|
|
59
|
+
const normalArticle = {
|
|
60
|
+
type: ArticleType.NORMAL,
|
|
61
|
+
title: "Welcome to Our Service",
|
|
62
|
+
author: "RedAI Team",
|
|
63
|
+
description: "This is a comprehensive guide to our new features",
|
|
64
|
+
cover: {
|
|
65
|
+
cover_type: CoverType.PHOTO,
|
|
66
|
+
photo_url: "https://example.com/cover.jpg",
|
|
67
|
+
status: ArticleStatus.SHOW
|
|
68
|
+
},
|
|
69
|
+
body: [
|
|
70
|
+
{
|
|
71
|
+
type: BodyItemType.TEXT,
|
|
72
|
+
content: "Welcome to our new service! Here's what you need to know..."
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: BodyItemType.IMAGE,
|
|
76
|
+
url: "https://example.com/image1.jpg"
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: BodyItemType.TEXT,
|
|
80
|
+
content: "For more information, please contact us."
|
|
81
|
+
}
|
|
82
|
+
],
|
|
83
|
+
tracking_link: "https://your-website.com/tracking",
|
|
84
|
+
status: ArticleStatus.SHOW,
|
|
85
|
+
comment: CommentStatus.SHOW
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const result = await article.createNormalArticle(accessToken, normalArticle);
|
|
89
|
+
console.log(`Article creation token: ${result.token}`);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. Create Video Article
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
const videoArticle = {
|
|
96
|
+
type: ArticleType.VIDEO,
|
|
97
|
+
title: "Product Demo Video",
|
|
98
|
+
description: "Watch our latest product demonstration",
|
|
99
|
+
video_id: "your_video_id", // Get from video upload
|
|
100
|
+
avatar: "https://example.com/thumbnail.jpg",
|
|
101
|
+
status: ArticleStatus.SHOW,
|
|
102
|
+
comment: CommentStatus.SHOW
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const result = await article.createVideoArticle(accessToken, videoArticle);
|
|
106
|
+
console.log(`Video article creation token: ${result.token}`);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 3. Upload Video for Articles
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Upload video file
|
|
113
|
+
const videoFile = new File([videoBuffer], 'demo.mp4', { type: 'video/mp4' });
|
|
114
|
+
const uploadResult = await videoUpload.uploadVideo(accessToken, videoFile);
|
|
115
|
+
|
|
116
|
+
// Wait for processing completion
|
|
117
|
+
const finalResult = await videoUpload.waitForUploadCompletion(
|
|
118
|
+
accessToken,
|
|
119
|
+
uploadResult.token,
|
|
120
|
+
5 * 60 * 1000, // 5 minutes timeout
|
|
121
|
+
5 * 1000 // 5 seconds polling interval
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
console.log(`Video ID: ${finalResult.video_id}`);
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 4. Upload Video from URL
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
const uploadResult = await videoUpload.uploadVideoFromUrl(
|
|
131
|
+
accessToken,
|
|
132
|
+
'https://example.com/video.mp4'
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const finalResult = await videoUpload.waitForUploadCompletion(
|
|
136
|
+
accessToken,
|
|
137
|
+
uploadResult.token
|
|
138
|
+
);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### 5. Check Article Creation Progress
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
const progress = await article.checkArticleProcess(accessToken, token);
|
|
145
|
+
|
|
146
|
+
switch (progress.status) {
|
|
147
|
+
case 'processing':
|
|
148
|
+
console.log('Article is still being processed...');
|
|
149
|
+
break;
|
|
150
|
+
case 'success':
|
|
151
|
+
console.log(`Article created with ID: ${progress.article_id}`);
|
|
152
|
+
break;
|
|
153
|
+
case 'failed':
|
|
154
|
+
console.error(`Article creation failed: ${progress.error_message}`);
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 6. Get Article Details
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const articleDetail = await article.getArticleDetail(accessToken, articleId);
|
|
163
|
+
console.log('Article details:', articleDetail);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### 7. Get Article List
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
const articleList = await article.getArticleList(accessToken, {
|
|
170
|
+
offset: 0,
|
|
171
|
+
limit: 20,
|
|
172
|
+
type: 'normal' // or 'video'
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
console.log(`Found ${articleList.data.total} articles`);
|
|
176
|
+
articleList.data.articles.forEach(article => {
|
|
177
|
+
console.log(`- ${article.title} (${article.total_view} views)`);
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### 8. Update Article
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
const updateData = {
|
|
185
|
+
id: articleId,
|
|
186
|
+
type: ArticleType.NORMAL,
|
|
187
|
+
title: "Updated Article Title",
|
|
188
|
+
author: "Updated Author",
|
|
189
|
+
description: "Updated description",
|
|
190
|
+
cover: {
|
|
191
|
+
cover_type: CoverType.PHOTO,
|
|
192
|
+
photo_url: "https://example.com/new-cover.jpg",
|
|
193
|
+
status: ArticleStatus.SHOW
|
|
194
|
+
},
|
|
195
|
+
body: [
|
|
196
|
+
{
|
|
197
|
+
type: BodyItemType.TEXT,
|
|
198
|
+
content: "Updated content..."
|
|
199
|
+
}
|
|
200
|
+
],
|
|
201
|
+
status: ArticleStatus.SHOW
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const updateResult = await article.updateNormalArticle(accessToken, updateData);
|
|
205
|
+
console.log(`Update token: ${updateResult.token}`);
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### 9. Remove Article
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
const removeResult = await article.removeArticle(accessToken, articleId);
|
|
212
|
+
console.log(removeResult.message);
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 10. Check Video Status
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const videoStatus = await videoUpload.checkVideoStatus(accessToken, token);
|
|
219
|
+
|
|
220
|
+
console.log(`Status: ${videoStatus.status}`);
|
|
221
|
+
console.log(`Progress: ${videoStatus.convert_percent}%`);
|
|
222
|
+
|
|
223
|
+
if (videoStatus.video_id) {
|
|
224
|
+
console.log(`Video ID: ${videoStatus.video_id}`);
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Error Handling
|
|
229
|
+
|
|
230
|
+
```typescript
|
|
231
|
+
import { ZaloSDKError } from 'redai-zalo-sdk';
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const result = await article.createNormalArticle(accessToken, articleData);
|
|
235
|
+
console.log('Success:', result);
|
|
236
|
+
} catch (error) {
|
|
237
|
+
if (error instanceof ZaloSDKError) {
|
|
238
|
+
console.error('Zalo API Error:', error.message, error.code);
|
|
239
|
+
console.error('Details:', error.details);
|
|
240
|
+
} else {
|
|
241
|
+
console.error('Unexpected error:', error);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Complete Example
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { ZaloSDK, ArticleType, CoverType, BodyItemType, ArticleStatus } from 'redai-zalo-sdk';
|
|
250
|
+
|
|
251
|
+
class ArticleManager {
|
|
252
|
+
private sdk: ZaloSDK;
|
|
253
|
+
|
|
254
|
+
constructor() {
|
|
255
|
+
this.sdk = new ZaloSDK({
|
|
256
|
+
appId: process.env.ZALO_APP_ID!,
|
|
257
|
+
appSecret: process.env.ZALO_APP_SECRET!,
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async createCompleteArticle(accessToken: string) {
|
|
262
|
+
try {
|
|
263
|
+
// 1. Upload video first (if needed)
|
|
264
|
+
const videoFile = new File([videoBuffer], 'demo.mp4');
|
|
265
|
+
const videoUpload = await this.sdk.videoUpload.uploadVideo(accessToken, videoFile);
|
|
266
|
+
const videoResult = await this.sdk.videoUpload.waitForUploadCompletion(
|
|
267
|
+
accessToken,
|
|
268
|
+
videoUpload.token
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// 2. Create article with video
|
|
272
|
+
const article = await this.sdk.article.createNormalArticle(accessToken, {
|
|
273
|
+
type: ArticleType.NORMAL,
|
|
274
|
+
title: "Complete Guide",
|
|
275
|
+
author: "RedAI Team",
|
|
276
|
+
description: "A complete guide with video content",
|
|
277
|
+
cover: {
|
|
278
|
+
cover_type: CoverType.PHOTO,
|
|
279
|
+
photo_url: "https://example.com/cover.jpg",
|
|
280
|
+
status: ArticleStatus.SHOW
|
|
281
|
+
},
|
|
282
|
+
body: [
|
|
283
|
+
{
|
|
284
|
+
type: BodyItemType.TEXT,
|
|
285
|
+
content: "Introduction to our service..."
|
|
286
|
+
},
|
|
287
|
+
{
|
|
288
|
+
type: BodyItemType.VIDEO,
|
|
289
|
+
video_id: videoResult.video_id
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
type: BodyItemType.TEXT,
|
|
293
|
+
content: "Thank you for watching!"
|
|
294
|
+
}
|
|
295
|
+
],
|
|
296
|
+
status: ArticleStatus.SHOW
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
// 3. Wait for article creation
|
|
300
|
+
const articleProgress = await this.sdk.article.checkArticleProcess(
|
|
301
|
+
accessToken,
|
|
302
|
+
article.token
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
return articleProgress;
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error('Article creation error:', error);
|
|
308
|
+
throw error;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Validation Rules
|
|
315
|
+
|
|
316
|
+
### Normal Articles
|
|
317
|
+
- Title: required, max 150 characters
|
|
318
|
+
- Author: required, max 50 characters
|
|
319
|
+
- Description: required, max 300 characters
|
|
320
|
+
- Cover: required, must specify type and corresponding URL/ID
|
|
321
|
+
- Body: required, at least one item
|
|
322
|
+
|
|
323
|
+
### Video Articles
|
|
324
|
+
- Title: required, max 150 characters
|
|
325
|
+
- Description: required, max 300 characters
|
|
326
|
+
- Video ID: required, must be valid uploaded video
|
|
327
|
+
- Avatar: required, thumbnail URL
|
|
328
|
+
|
|
329
|
+
### Video Files
|
|
330
|
+
- Formats: MP4, AVI only
|
|
331
|
+
- Size: max 50MB
|
|
332
|
+
- Processing: asynchronous, use polling to check status
|
|
333
|
+
|
|
334
|
+
## API Reference
|
|
335
|
+
|
|
336
|
+
For detailed API documentation, see the TypeScript definitions in the source code. All methods include comprehensive JSDoc comments with parameter descriptions and return types.
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# Consultation Service - Hướng Dẫn Sử Dụng
|
|
2
|
+
|
|
3
|
+
## Tổng Quan
|
|
4
|
+
|
|
5
|
+
`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.
|
|
6
|
+
|
|
7
|
+
## Điều Kiện Gửi Tin Nhắn Tư Vấn
|
|
8
|
+
|
|
9
|
+
### 1. Thời Gian Gửi
|
|
10
|
+
- **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
|
|
11
|
+
- **Tương tác bao gồm**:
|
|
12
|
+
- Gửi tin nhắn đến OA
|
|
13
|
+
- Nhấn button/quick reply
|
|
14
|
+
- Gọi điện thoại từ OA
|
|
15
|
+
- Truy cập website từ OA
|
|
16
|
+
|
|
17
|
+
### 2. Nội Dung Tin Nhắn
|
|
18
|
+
- **Mục đích**: Phải liên quan đến tư vấn, hỗ trợ khách hàng
|
|
19
|
+
- **Bao gồm**: Trả lời câu hỏi, hướng dẫn sử dụng, hỗ trợ kỹ thuật
|
|
20
|
+
- **Không được**: Chứa nội dung quảng cáo trực tiếp
|
|
21
|
+
|
|
22
|
+
### 3. Điều Kiện Người Dùng
|
|
23
|
+
- Người dùng phải đã follow OA
|
|
24
|
+
- Người dùng không được block OA
|
|
25
|
+
- Người dùng phải có tương tác gần đây với OA
|
|
26
|
+
|
|
27
|
+
### 4. Tần Suất Gửi
|
|
28
|
+
- Không giới hạn số lượng tin nhắn tư vấn trong ngày
|
|
29
|
+
- Cần tuân thủ nguyên tắc không spam
|
|
30
|
+
|
|
31
|
+
## Khởi Tạo Service
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { ZaloSDK } from "@warriorteam/redai-zalo-sdk";
|
|
35
|
+
|
|
36
|
+
const zalo = new ZaloSDK({
|
|
37
|
+
appId: "your-app-id",
|
|
38
|
+
appSecret: "your-app-secret",
|
|
39
|
+
debug: true
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Lấy consultation service
|
|
43
|
+
const consultationService = zalo.consultation;
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Các Phương Thức Chính
|
|
47
|
+
|
|
48
|
+
### 1. Gửi Tin Nhắn Văn Bản
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// Gửi tin nhắn văn bản tư vấn
|
|
52
|
+
const response = await consultationService.sendTextMessage(
|
|
53
|
+
accessToken,
|
|
54
|
+
{ user_id: "user-id-here" },
|
|
55
|
+
{
|
|
56
|
+
type: "text",
|
|
57
|
+
text: "Xin chào! Tôi có thể hỗ trợ gì cho bạn?"
|
|
58
|
+
}
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
console.log("Message ID:", response.message_id);
|
|
62
|
+
console.log("Quota remaining:", response.quota?.remain);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Giới hạn văn bản:**
|
|
66
|
+
- Nội dung không được để trống
|
|
67
|
+
- Tối đa 2000 ký tự
|
|
68
|
+
|
|
69
|
+
### 2. Gửi Tin Nhắn Hình Ảnh
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
// Gửi hình ảnh tư vấn
|
|
73
|
+
const response = await consultationService.sendImageMessage(
|
|
74
|
+
accessToken,
|
|
75
|
+
{ user_id: "user-id-here" },
|
|
76
|
+
{
|
|
77
|
+
type: "image",
|
|
78
|
+
attachment: {
|
|
79
|
+
type: "image",
|
|
80
|
+
payload: {
|
|
81
|
+
url: "https://example.com/support-image.jpg"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 3. Gửi File Đính Kèm
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
// Gửi file hướng dẫn
|
|
92
|
+
const response = await consultationService.sendFileMessage(
|
|
93
|
+
accessToken,
|
|
94
|
+
{ user_id: "user-id-here" },
|
|
95
|
+
{
|
|
96
|
+
type: "file",
|
|
97
|
+
url: "https://example.com/user-manual.pdf",
|
|
98
|
+
filename: "Hướng dẫn sử dụng.pdf",
|
|
99
|
+
attachment: {
|
|
100
|
+
type: "file",
|
|
101
|
+
payload: {
|
|
102
|
+
url: "https://example.com/user-manual.pdf"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### 4. Gửi Sticker
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// Gửi sticker thân thiện
|
|
113
|
+
const response = await consultationService.sendStickerMessage(
|
|
114
|
+
accessToken,
|
|
115
|
+
{ user_id: "user-id-here" },
|
|
116
|
+
{
|
|
117
|
+
type: "sticker",
|
|
118
|
+
sticker_id: "sticker-id",
|
|
119
|
+
attachment: {
|
|
120
|
+
type: "sticker",
|
|
121
|
+
payload: {
|
|
122
|
+
id: "sticker-id"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 5. Gửi Tin Nhắn Tổng Quát
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Gửi bất kỳ loại tin nhắn nào
|
|
133
|
+
const response = await consultationService.sendMessage(
|
|
134
|
+
accessToken,
|
|
135
|
+
{ user_id: "user-id-here" },
|
|
136
|
+
{
|
|
137
|
+
type: "text",
|
|
138
|
+
text: "Cảm ơn bạn đã liên hệ. Chúng tôi sẽ hỗ trợ bạn ngay!"
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Xử Lý Lỗi
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
import { ZaloSDKError } from "@warriorteam/redai-zalo-sdk";
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
const response = await consultationService.sendTextMessage(
|
|
150
|
+
accessToken,
|
|
151
|
+
{ user_id: "user-id" },
|
|
152
|
+
{ type: "text", text: "Hello!" }
|
|
153
|
+
);
|
|
154
|
+
} catch (error) {
|
|
155
|
+
if (error instanceof ZaloSDKError) {
|
|
156
|
+
console.error("Zalo API Error:", error.message);
|
|
157
|
+
console.error("Error Code:", error.code);
|
|
158
|
+
|
|
159
|
+
// Xử lý các lỗi phổ biến
|
|
160
|
+
switch (error.code) {
|
|
161
|
+
case -216:
|
|
162
|
+
console.log("Access token không hợp lệ");
|
|
163
|
+
break;
|
|
164
|
+
case -201:
|
|
165
|
+
console.log("Tham số không hợp lệ");
|
|
166
|
+
break;
|
|
167
|
+
case -223:
|
|
168
|
+
console.log("Đã vượt quá quota");
|
|
169
|
+
break;
|
|
170
|
+
default:
|
|
171
|
+
console.log("Lỗi khác:", error.message);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Ví Dụ Thực Tế
|
|
178
|
+
|
|
179
|
+
### Hệ Thống Hỗ Trợ Khách Hàng
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
class CustomerSupportBot {
|
|
183
|
+
constructor(private consultationService: ConsultationService) {}
|
|
184
|
+
|
|
185
|
+
async handleUserQuestion(accessToken: string, userId: string, question: string) {
|
|
186
|
+
try {
|
|
187
|
+
// Phân tích câu hỏi và tạo phản hồi
|
|
188
|
+
const response = this.generateResponse(question);
|
|
189
|
+
|
|
190
|
+
// Gửi tin nhắn tư vấn
|
|
191
|
+
await this.consultationService.sendTextMessage(
|
|
192
|
+
accessToken,
|
|
193
|
+
{ user_id: userId },
|
|
194
|
+
{
|
|
195
|
+
type: "text",
|
|
196
|
+
text: response
|
|
197
|
+
}
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
// Gửi thêm hình ảnh hướng dẫn nếu cần
|
|
201
|
+
if (this.needsVisualGuide(question)) {
|
|
202
|
+
await this.consultationService.sendImageMessage(
|
|
203
|
+
accessToken,
|
|
204
|
+
{ user_id: userId },
|
|
205
|
+
{
|
|
206
|
+
type: "image",
|
|
207
|
+
attachment: {
|
|
208
|
+
type: "image",
|
|
209
|
+
payload: {
|
|
210
|
+
url: "https://example.com/guide-image.jpg"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
} catch (error) {
|
|
218
|
+
console.error("Failed to send consultation message:", error);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private generateResponse(question: string): string {
|
|
223
|
+
// Logic tạo phản hồi dựa trên câu hỏi
|
|
224
|
+
if (question.includes("đăng nhập")) {
|
|
225
|
+
return "Để đăng nhập, bạn vui lòng làm theo các bước sau...";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (question.includes("thanh toán")) {
|
|
229
|
+
return "Về vấn đề thanh toán, chúng tôi hỗ trợ các phương thức...";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return "Cảm ơn bạn đã liên hệ. Chúng tôi sẽ hỗ trợ bạn ngay!";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private needsVisualGuide(question: string): boolean {
|
|
236
|
+
return question.includes("hướng dẫn") || question.includes("cách làm");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Tích Hợp Với Webhook
|
|
242
|
+
|
|
243
|
+
```typescript
|
|
244
|
+
// Xử lý tin nhắn từ webhook
|
|
245
|
+
app.post('/webhook', async (req, res) => {
|
|
246
|
+
const event = req.body;
|
|
247
|
+
|
|
248
|
+
if (event.event_name === 'user_send_text') {
|
|
249
|
+
const userId = event.sender.id;
|
|
250
|
+
const userMessage = event.message.text;
|
|
251
|
+
|
|
252
|
+
// Kiểm tra xem có phải câu hỏi cần hỗ trợ không
|
|
253
|
+
if (userMessage.includes('help') || userMessage.includes('hỗ trợ')) {
|
|
254
|
+
|
|
255
|
+
// Gửi tin nhắn tư vấn
|
|
256
|
+
await consultationService.sendTextMessage(
|
|
257
|
+
accessToken,
|
|
258
|
+
{ user_id: userId },
|
|
259
|
+
{
|
|
260
|
+
type: "text",
|
|
261
|
+
text: "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?"
|
|
262
|
+
}
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
res.status(200).send('OK');
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## Best Practices
|
|
272
|
+
|
|
273
|
+
### 1. Kiểm Tra Thời Gian Tương Tác
|
|
274
|
+
```typescript
|
|
275
|
+
// Kiểm tra thời gian tương tác cuối cùng trước khi gửi
|
|
276
|
+
const lastInteraction = await getUserLastInteraction(userId);
|
|
277
|
+
const hoursSinceLastInteraction = (Date.now() - lastInteraction) / (1000 * 60 * 60);
|
|
278
|
+
|
|
279
|
+
if (hoursSinceLastInteraction > 48) {
|
|
280
|
+
console.log("Không thể gửi tin nhắn tư vấn - quá 48 giờ");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### 2. Quản Lý Quota
|
|
286
|
+
```typescript
|
|
287
|
+
// Kiểm tra quota trước khi gửi
|
|
288
|
+
const quota = await zalo.oa.getQuotaSummary(accessToken);
|
|
289
|
+
if (quota.consultation.remaining <= 0) {
|
|
290
|
+
console.log("Đã hết quota tin nhắn tư vấn");
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### 3. Retry Logic
|
|
296
|
+
```typescript
|
|
297
|
+
async function sendWithRetry(
|
|
298
|
+
consultationService: ConsultationService,
|
|
299
|
+
accessToken: string,
|
|
300
|
+
recipient: MessageRecipient,
|
|
301
|
+
message: Message,
|
|
302
|
+
maxRetries = 3
|
|
303
|
+
) {
|
|
304
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
305
|
+
try {
|
|
306
|
+
return await consultationService.sendMessage(accessToken, recipient, message);
|
|
307
|
+
} catch (error) {
|
|
308
|
+
if (i === maxRetries - 1) throw error;
|
|
309
|
+
|
|
310
|
+
// Đợi trước khi retry
|
|
311
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
## Lưu Ý Quan Trọng
|
|
318
|
+
|
|
319
|
+
1. **Tuân thủ chính sách**: Chỉ gửi tin nhắn tư vấn thực sự, không spam
|
|
320
|
+
2. **Theo dõi quota**: Kiểm tra quota thường xuyên để tránh vượt giới hạn
|
|
321
|
+
3. **Xử lý lỗi**: Luôn có cơ chế xử lý lỗi phù hợp
|
|
322
|
+
4. **Logging**: Ghi log để theo dõi và debug
|
|
323
|
+
5. **Rate limiting**: Tránh gửi quá nhiều tin nhắn trong thời gian ngắn
|
|
324
|
+
|
|
325
|
+
## Tài Liệu Liên Quan
|
|
326
|
+
|
|
327
|
+
- [Message Types](./MESSAGE_TYPES.md) - Các loại tin nhắn được hỗ trợ
|
|
328
|
+
- [Webhook Events](./WEBHOOK_EVENTS.md) - Xử lý sự kiện webhook
|
|
329
|
+
- [Error Handling](./ERROR_HANDLING.md) - Xử lý lỗi chi tiết
|
|
330
|
+
- [Quota Management](./QUOTA_MANAGEMENT.md) - Quản lý quota tin nhắn
|