461 lines
11 KiB
Markdown
461 lines
11 KiB
Markdown
# NotebookLM API — Tài liệu đầy đủ
|
||
|
||
Server Node.js điều khiển Google NotebookLM qua Puppeteer, giả lập thao tác người dùng thật.
|
||
|
||
Base URL: `http://localhost:3456`
|
||
|
||
---
|
||
|
||
## Mục lục
|
||
|
||
1. [Cài đặt & Khởi động](#cài-đặt--khởi-động)
|
||
2. [Biến môi trường](#biến-môi-trường)
|
||
3. [Xác thực](#xác-thực)
|
||
4. [Notebooks](#notebooks)
|
||
5. [Sources](#sources)
|
||
6. [Chat](#chat)
|
||
7. [Trạng thái server](#trạng-thái-server)
|
||
8. [Mã lỗi](#mã-lỗi)
|
||
9. [Tích hợp](#tích-hợp)
|
||
|
||
---
|
||
|
||
## Cài đặt & Khởi động
|
||
|
||
**Yêu cầu:** Node.js 18+, Google Chrome
|
||
|
||
```bash
|
||
bash setup.sh # cài đặt + khởi động
|
||
|
||
# Hoặc thủ công:
|
||
npm install
|
||
cp .env.example .env
|
||
npm start
|
||
```
|
||
|
||
- Server khởi động tại `http://localhost:3456`
|
||
- Swagger UI tại `http://localhost:3456/docs`
|
||
- Chrome tự mở với profile riêng tại `./chrome-profile/`
|
||
|
||
**Dừng server:** `Ctrl+C` — KHÔNG dùng `kill -9` (sẽ mất cookies Google)
|
||
|
||
---
|
||
|
||
## Biến môi trường
|
||
|
||
File `.env` (copy từ `.env.example`):
|
||
|
||
| Biến | Mặc định | Mô tả |
|
||
|------|----------|-------|
|
||
| `PORT` | `3456` | Cổng HTTP/WebSocket |
|
||
| `HEADLESS` | `false` | `true` = ẩn cửa sổ Chrome |
|
||
| `CHROME_PATH` | tự phát hiện | Đường dẫn Chrome tuỳ chỉnh |
|
||
| `API_KEY` | _(trống)_ | Nếu đặt, mọi request phải có header `x-api-key: <key>` |
|
||
|
||
---
|
||
|
||
## Xác thực
|
||
|
||
### GET /api/auth/status
|
||
|
||
Kiểm tra trạng thái đăng nhập Google.
|
||
|
||
```bash
|
||
curl http://localhost:3456/api/auth/status
|
||
```
|
||
|
||
```json
|
||
{ "ok": true, "authenticated": true }
|
||
{ "ok": true, "authenticated": false }
|
||
```
|
||
|
||
---
|
||
|
||
### POST /api/auth/login
|
||
|
||
Mở browser để đăng nhập Google. Chờ tối đa 5 phút.
|
||
|
||
```bash
|
||
curl -X POST http://localhost:3456/api/auth/login
|
||
```
|
||
|
||
```json
|
||
{ "ok": true, "authenticated": true, "message": "Đăng nhập thành công, session đã lưu" }
|
||
```
|
||
|
||
Session lưu vào `./chrome-profile/` — các lần khởi động lại server không cần đăng nhập lại.
|
||
|
||
---
|
||
|
||
## Notebooks
|
||
|
||
### GET /api/notebooks
|
||
|
||
Lấy danh sách tất cả notebooks.
|
||
|
||
```bash
|
||
curl http://localhost:3456/api/notebooks
|
||
```
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"total": 3,
|
||
"notebooks": [
|
||
{
|
||
"id": "2c4f0f26-0797-4cc0-a350-48c4b70d14cc",
|
||
"title": "Dự án Q3 2026",
|
||
"url": "https://notebooklm.google.com/notebook/2c4f0f26-..."
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### POST /api/notebooks
|
||
|
||
Tạo notebook mới.
|
||
|
||
**Body:**
|
||
```json
|
||
{ "title": "Tên notebook" }
|
||
```
|
||
|
||
```bash
|
||
curl -X POST http://localhost:3456/api/notebooks \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"title": "Dự án Q3 2026"}'
|
||
```
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
|
||
"title": "Dự án Q3 2026",
|
||
"url": "https://notebooklm.google.com/notebook/..."
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### DELETE /api/notebooks/:id
|
||
|
||
Xoá notebook.
|
||
|
||
```bash
|
||
curl -X DELETE http://localhost:3456/api/notebooks/2c4f0f26-0797-4cc0-a350-48c4b70d14cc
|
||
```
|
||
|
||
```json
|
||
{ "ok": true, "deleted": true, "id": "2c4f0f26-..." }
|
||
```
|
||
|
||
---
|
||
|
||
## Sources
|
||
|
||
### GET /api/notebooks/:id/sources
|
||
|
||
Lấy danh sách sources trong notebook. Tự chờ đến khi tất cả sources convert xong.
|
||
|
||
**Query params:**
|
||
|
||
| Param | Mặc định | Mô tả |
|
||
|-------|----------|-------|
|
||
| `timeout` | `300` | Giây tối đa chờ converting xong (max 600) |
|
||
|
||
```bash
|
||
# Mặc định chờ 5 phút
|
||
curl "http://localhost:3456/api/notebooks/<id>/sources" --max-time 310
|
||
|
||
# Notebook nặng — chờ tối đa 10 phút
|
||
curl "http://localhost:3456/api/notebooks/<id>/sources?timeout=600" --max-time 610
|
||
```
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"total": 3,
|
||
"stillLoading": 0,
|
||
"sources": [
|
||
{ "index": 0, "title": "Báo cáo Q2.pdf", "type": "pdf", "loading": false },
|
||
{ "index": 1, "title": "https://example.com/article", "type": "url", "loading": false },
|
||
{ "index": 2, "title": "Ghi chú cuộc họp", "type": "text", "loading": false }
|
||
]
|
||
}
|
||
```
|
||
|
||
`stillLoading > 0` — gọi lại sau vài phút để lấy kết quả đầy đủ.
|
||
|
||
---
|
||
|
||
### POST /api/notebooks/:id/sources
|
||
|
||
Thêm source vào notebook. Hỗ trợ 3 loại:
|
||
|
||
#### Loại 1: URL website
|
||
|
||
```json
|
||
{ "type": "url", "content": "https://...", "title": "Tuỳ chọn" }
|
||
```
|
||
|
||
```bash
|
||
curl -X POST http://localhost:3456/api/notebooks/<id>/sources \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"type":"url","content":"https://vnexpress.net/bai-viet-abc","title":"VnExpress"}'
|
||
```
|
||
|
||
```json
|
||
{ "ok": true, "added": true, "type": "url", "url": "https://..." }
|
||
```
|
||
|
||
---
|
||
|
||
#### Loại 2: Văn bản paste
|
||
|
||
```json
|
||
{ "type": "text", "content": "Nội dung văn bản...", "title": "Tên tài liệu" }
|
||
```
|
||
|
||
```bash
|
||
curl -X POST http://localhost:3456/api/notebooks/<id>/sources \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"type":"text","content":"Báo cáo doanh thu tháng 6: ...","title":"Báo cáo T6/2026"}'
|
||
```
|
||
|
||
```json
|
||
{ "ok": true, "added": true, "type": "text", "length": 1234 }
|
||
```
|
||
|
||
---
|
||
|
||
#### Loại 3: File local
|
||
|
||
Upload file từ máy đang chạy server. Đường dẫn phải là **đường dẫn tuyệt đối** (absolute path). Hỗ trợ cả URL-encoded path (có `%20` thay dấu cách).
|
||
|
||
```json
|
||
{ "type": "file", "content": "/đường/dẫn/tuyệt/đối/file.pdf" }
|
||
```
|
||
|
||
**Định dạng hỗ trợ:** `.pdf` `.txt` `.md` `.docx` `.doc` `.pptx` `.ppt` `.xlsx` `.xls` `.mp3` `.mp4` `.jpg` `.jpeg` `.png`
|
||
|
||
```bash
|
||
# File bình thường
|
||
curl -X POST http://localhost:3456/api/notebooks/<id>/sources \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"type":"file","content":"/Users/me/Documents/report.pdf"}' \
|
||
--max-time 90
|
||
|
||
# Đường dẫn có dấu cách (cả 2 cách đều dùng được)
|
||
-d '{"type":"file","content":"/Users/me/Downloads/Telegram Desktop/file.md"}'
|
||
-d '{"type":"file","content":"/Users/me/Downloads/Telegram%20Desktop/file.md"}'
|
||
```
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"added": true,
|
||
"type": "file",
|
||
"filename": "report.pdf",
|
||
"path": "/Users/me/Documents/report.pdf"
|
||
}
|
||
```
|
||
|
||
> **Lưu ý:** Upload có thể mất 30–60 giây. Đặt `--max-time 90` khi dùng curl. Notebook phải do bạn sở hữu (không phải notebook public/shared).
|
||
|
||
---
|
||
|
||
## Chat
|
||
|
||
### POST /api/notebooks/:id/chat
|
||
|
||
Gửi câu hỏi và chờ câu trả lời đầy đủ (đồng bộ).
|
||
|
||
**Body:**
|
||
```json
|
||
{ "message": "Câu hỏi của bạn" }
|
||
```
|
||
|
||
```bash
|
||
curl -X POST http://localhost:3456/api/notebooks/<id>/chat \
|
||
-H "Content-Type: application/json" \
|
||
-d '{"message":"Tóm tắt các điểm chính trong tài liệu này"}' \
|
||
--max-time 120
|
||
```
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"question": "Tóm tắt các điểm chính trong tài liệu này",
|
||
"answer": "Dựa trên các tài liệu, các điểm chính bao gồm: ..."
|
||
}
|
||
```
|
||
|
||
Timeout mặc định 90 giây. Câu hỏi phức tạp với nhiều sources có thể cần lâu hơn.
|
||
|
||
---
|
||
|
||
### GET /api/notebooks/:id/chat/history
|
||
|
||
Lấy toàn bộ lịch sử hội thoại trong notebook.
|
||
|
||
```bash
|
||
curl http://localhost:3456/api/notebooks/<id>/chat/history
|
||
```
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"total": 4,
|
||
"history": [
|
||
{ "role": "user", "content": "Tóm tắt tài liệu" },
|
||
{ "role": "assistant", "content": "Tài liệu đề cập đến..." }
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### WS /api/notebooks/:id/chat/stream
|
||
|
||
Chat streaming qua WebSocket. Nhận câu trả lời theo từng chunk realtime.
|
||
|
||
**Kết nối:** `ws://localhost:3456/api/notebooks/<id>/chat/stream`
|
||
|
||
**Gửi:** `{ "message": "câu hỏi" }`
|
||
|
||
**Nhận:**
|
||
```json
|
||
{ "type": "connected", "notebookId": "<id>" }
|
||
{ "type": "chunk", "data": "Dựa trên" }
|
||
{ "type": "chunk", "data": " các tài liệu..." }
|
||
{ "type": "done", "data": { "answer": "Dựa trên các tài liệu..." } }
|
||
{ "type": "error", "data": "Thông báo lỗi" }
|
||
```
|
||
|
||
**Ví dụ JavaScript:**
|
||
```javascript
|
||
const ws = new WebSocket('ws://localhost:3456/api/notebooks/<id>/chat/stream');
|
||
|
||
ws.onopen = () => ws.send(JSON.stringify({ message: 'Tóm tắt nội dung chính' }));
|
||
|
||
ws.onmessage = ({ data }) => {
|
||
const msg = JSON.parse(data);
|
||
if (msg.type === 'chunk') process.stdout.write(msg.data);
|
||
if (msg.type === 'done') console.log('\n[Xong]');
|
||
if (msg.type === 'error') console.error('[Lỗi]', msg.data);
|
||
};
|
||
```
|
||
|
||
**Test nhanh bằng wscat:**
|
||
```bash
|
||
npx wscat -c "ws://localhost:3456/api/notebooks/<id>/chat/stream"
|
||
# Sau khi kết nối:
|
||
{"message":"Tóm tắt tài liệu này"}
|
||
```
|
||
|
||
---
|
||
|
||
## Trạng thái server
|
||
|
||
### GET /health
|
||
|
||
```bash
|
||
curl http://localhost:3456/health
|
||
```
|
||
|
||
```json
|
||
{
|
||
"ok": true,
|
||
"status": "running",
|
||
"queue": { "busy": false, "pending": 0 }
|
||
}
|
||
```
|
||
|
||
`queue.busy = true` — đang có thao tác browser chạy; các request khác xếp hàng chờ tự động.
|
||
|
||
---
|
||
|
||
## Mã lỗi
|
||
|
||
| HTTP | `error` | Ý nghĩa |
|
||
|------|---------|---------|
|
||
| 400 | `Thiếu type hoặc content` | Body thiếu field bắt buộc |
|
||
| 401 | `NOT_AUTHENTICATED` | Chưa đăng nhập → gọi `POST /api/auth/login` |
|
||
| 401 | `Thiếu hoặc sai API key` | Header `x-api-key` sai hoặc thiếu |
|
||
| 404 | `NOTEBOOK_NOT_FOUND` | Notebook ID không tồn tại hoặc không có quyền |
|
||
| 500 | _(message)_ | Lỗi nội bộ — xem log server |
|
||
|
||
---
|
||
|
||
## Tích hợp
|
||
|
||
### n8n / Make / Zapier
|
||
|
||
- Node: **HTTP Request**
|
||
- URL: `http://<ip-máy>:3456/api/notebooks/<id>/chat`
|
||
- Method: `POST`
|
||
- Body (JSON): `{"message": "{{$json.input}}"}`
|
||
|
||
### Python
|
||
|
||
```python
|
||
import requests
|
||
|
||
BASE = "http://localhost:3456"
|
||
NB_ID = "<notebook-id>"
|
||
|
||
# Thêm file
|
||
requests.post(f"{BASE}/api/notebooks/{NB_ID}/sources",
|
||
json={"type": "file", "content": "/path/to/file.pdf"},
|
||
timeout=90)
|
||
|
||
# Thêm URL
|
||
requests.post(f"{BASE}/api/notebooks/{NB_ID}/sources",
|
||
json={"type": "url", "content": "https://example.com"},
|
||
timeout=30)
|
||
|
||
# Hỏi
|
||
resp = requests.post(f"{BASE}/api/notebooks/{NB_ID}/chat",
|
||
json={"message": "Tóm tắt tài liệu"}, timeout=120)
|
||
print(resp.json()["answer"])
|
||
|
||
# Danh sách notebooks
|
||
notebooks = requests.get(f"{BASE}/api/notebooks").json()["notebooks"]
|
||
```
|
||
|
||
### Node.js / fetch
|
||
|
||
```javascript
|
||
const BASE = 'http://localhost:3456';
|
||
const NB_ID = '<notebook-id>';
|
||
|
||
// Thêm file
|
||
await fetch(`${BASE}/api/notebooks/${NB_ID}/sources`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ type: 'file', content: '/path/to/file.pdf' }),
|
||
signal: AbortSignal.timeout(90_000),
|
||
});
|
||
|
||
// Hỏi
|
||
const res = await fetch(`${BASE}/api/notebooks/${NB_ID}/chat`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ message: 'Tóm tắt nội dung' }),
|
||
signal: AbortSignal.timeout(120_000),
|
||
});
|
||
const { answer } = await res.json();
|
||
```
|
||
|
||
---
|
||
|
||
## Lưu ý quan trọng
|
||
|
||
- **Chỉ 1 thao tác browser tại một thời điểm** — các request song song xếp hàng tự động
|
||
- **Upload file** chỉ hoạt động với notebook do bạn sở hữu (không phải public/shared)
|
||
- **Dừng server đúng cách**: `Ctrl+C` — KHÔNG dùng `kill -9`
|
||
- **Chrome crash**: xoá `chrome-profile/SingletonLock` rồi restart server
|
||
- **Google giới hạn automation**: nếu bị chặn, đợi vài phút
|