Module 01: Chatbot Service (API)
Module 01 • API Gateway
Lắng Nghe Realtime Qua Firebase Firestore
Sau khi gửi câu hỏi qua mutation SendAiMessage, ứng dụng nhận được assistantMessageId ngay lập tức. Client sẽ mở kết nối trực tiếp đến Cloud Firestore Realtime để lắng nghe từng đoạn văn bản do AI sinh ra (Streaming Response) mà không cần polling liên tục.
1. Đường Dẫn Document Firestore (Document Path)
Đường dẫn document mà ứng dụng cần lắng nghe được định dạng chuẩn xác theo cấu trúc cây:
organizations/{orgId}/users/{senderId}/chatSessions/{sessionId}/messages/{assistantMessageId}Bảng Giải Thích Các Thành Phần Trong Path
| Thành Phần | Ví Dụ | Nguồn Lấy Dữ Liệu |
|---|---|---|
{orgId} | org_9928123 | Lấy từ biến môi trường VITE_ORG_ID |
{senderId} | 737a445da12ddbd3d5d40a88ee314864 | platformUserId của người dùng (hoặc account.id) |
{sessionId} | 6a7d45b5aa2c04f1151bee2b | chatSessionId trả về từ mutation SendAiMessage |
{assistantMessageId} | 6a8913448201e708713c6e48 | assistantMessageId trả về từ mutation SendAiMessage |
2. Cấu Trúc Dữ Liệu Realtime Event & Terminal Statuses
Dữ liệu document Firestore được cập nhật liên tục khi AI đang sinh văn bản.
Firestore Document JSON Payload:
{
"messageId": "6a8913448201e708713c6e48",
"ctxRequestId": "req-1740200010000-abc1234",
"message": "Dạ, VUS hiện có các cơ sở tại Đà Nẵng như sau:\n1. VUS Nguyễn Văn Linh...",
"requestStatus": "COMPLETED",
"timestamp": 1740200015000
}Các Trạng Thái requestStatus:
| Trạng Thái | Phân Loại | Ý Nghĩa / Hành Động Của Client |
|---|---|---|
PROCESSING | Tiếp tục stream | AI đang sinh nội dung. Cập nhật message lên UI và tiếp tục lắng nghe. |
COMPLETED | Kết thúc (Terminal) | AI đã trả lời xong toàn bộ. Tự động gọi unsubscribe() và mở khóa nhập liệu. |
FAILED | Kết thúc (Terminal) | Xảy ra lỗi trong quá trình suy luận. Hiển thị thông báo lỗi và gọi unsubscribe(). |
CANCELLED | Kết thúc (Terminal) | Yêu cầu bị hủy (do người dùng hủy hoặc đóng phiên). Gọi unsubscribe(). |
3. Code Triển Khai Firebase SDK Listener (TypeScript)
Dưới đây là hàm helper hoàn chỉnh sử dụng Firebase v9/v10 Modular SDK, tích hợp tính năng tự động ngắt kết nối (unsubscribe) khi nhận được trạng thái kết thúc hoặc khi bị timeout quá 180s:
import { initializeApp, getApps, FirebaseApp } from 'firebase/app';
import { getFirestore, doc, onSnapshot, Unsubscribe, Firestore } from 'firebase/firestore';
export interface FirebaseStreamParams {
orgId: string;
senderId: string;
sessionId: string;
assistantMessageId: string;
firebaseConfig: {
apiKey: string;
authDomain: string;
projectId: string;
storageBucket: string;
messagingSenderId: string;
appId: string;
databaseId?: string;
};
}
export interface StreamUpdatePayload {
message: string;
requestStatus: 'PROCESSING' | 'COMPLETED' | 'FAILED' | 'CANCELLED';
timestamp?: number;
}
/**
* Lắng nghe stream câu trả lời từ Firebase Firestore
* @returns Hàm unsubscribe để hủy lắng nghe khi unmount component
*/
export function listenToConversation(
params: FirebaseStreamParams,
onUpdate: (data: StreamUpdatePayload) => void,
onError?: (err: any) => void
): () => void {
// 1. Khởi tạo Singleton Firebase App
const app: FirebaseApp =
getApps().length === 0 ? initializeApp(params.firebaseConfig) : getApps()[0];
// 2. Lấy Firestore instance (hỗ trợ databaseId tùy biến nếu có)
const db: Firestore = params.firebaseConfig.databaseId
? getFirestore(app, params.firebaseConfig.databaseId)
: getFirestore(app);
const docPath = `organizations/${params.orgId}/users/${params.senderId}/chatSessions/${params.sessionId}/messages/${params.assistantMessageId}`;
const docRef = doc(db, docPath);
console.log('[PangoAI Firebase] Đang lắng nghe:', docPath);
let isCleanedUp = false;
// 3. Cơ chế Timeout an toàn (180 giây) chống treo loading
const safetyTimeout = setTimeout(() => {
if (!isCleanedUp) {
console.warn('[PangoAI Firebase] Hết thời gian chờ (Timeout 180s). Tự động ngắt kết nối.');
cleanup();
if (onError) {
onError(new Error('Quá thời gian chờ phản hồi từ AI (180s Timeout).'));
}
}
}, 180000);
// 4. Lắng nghe Snapshot Realtime
const rawUnsubscribe: Unsubscribe = onSnapshot(
docRef,
(snapshot) => {
if (!snapshot.exists()) return;
const data = snapshot.data();
const status = data.requestStatus as StreamUpdatePayload['requestStatus'];
onUpdate({
message: data.message || '',
requestStatus: status,
timestamp: data.timestamp,
});
// Tự động ngắt khi nhận trạng thái kết thúc (Terminal Statuses)
if (['COMPLETED', 'FAILED', 'CANCELLED'].includes(status)) {
console.log('[PangoAI Firebase] Luồng kết thúc với trạng thái:', status);
cleanup();
}
},
(error) => {
console.error('[PangoAI Firebase] Lỗi lắng nghe Firestore:', error);
cleanup();
if (onError) onError(error);
}
);
function cleanup() {
if (!isCleanedUp) {
isCleanedUp = true;
clearTimeout(safetyTimeout);
rawUnsubscribe();
}
}
return cleanup;
}