# Zalo Agent

> Self-hosted AI agent living inside Zalo (personal account). Multi-account, 15 tools + external MCP servers, web dashboard, scheduler, durable memory, rich-text messages. Provider-agnostic: any OpenAI-compatible endpoint or Anthropic. TypeScript + SQLite. MIT.

- **Type:** MCP server
- **Install:** `agentstack add mcp-vuhai2002-zalo-agent`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [vuhai2002](https://agentstack.voostack.com/s/vuhai2002)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [vuhai2002](https://github.com/vuhai2002)
- **Source:** https://github.com/vuhai2002/zalo-agent

## Install

```sh
agentstack add mcp-vuhai2002-zalo-agent
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

Agent AI thường trú trên Zalo. Chạy được trên hai loại kênh: tài khoản Zalo cá nhân (qua zca-js)
và tài khoản Zalo Bot chính thức (qua Zalo Bot API). Nhiều account chạy chung một tiến trình,
mỗi account một "não" riêng, 15 công cụ, dashboard web đầy đủ. Tự host, không phụ thuộc nhà cung cấp LLM nào.

  Cài đặt •
  Hai loại kênh •
  Tính năng •
  Dashboard •
  An toàn •
  Kiến trúc •
  English

  
  
  
  
  
  

---

Luồng một lượt: **tin Zalo -> lọc (allowlist, @mention) -> gộp theo thread -> agent loop (LLM tự
gọi tool) -> làm sạch + dịch định dạng -> cắt theo trần byte -> gửi**.

Không có bước nào chạy code do model sinh ra. Nội dung lấy từ web luôn bị bọc trong thẻ đánh dấu
là dữ liệu, không phải mệnh lệnh.

## Hai loại kênh

Một tiến trình chạy được cả hai loại tài khoản cùng lúc, trộn lẫn tùy ý. Chọn loại **lúc tạo
account** trên dashboard, không đổi được sau đó.

| | Tài khoản cá nhân | Tài khoản Zalo Bot chính thức |
|---|---|---|
| Thư viện / API | [`zca-js`](https://github.com/RFS-ADRENO/zca-js) (**không chính thức**) | **Zalo Bot API** - `bot-api.zaloplatforms.com` (chính thức) |
| Đăng nhập | quét QR ngay trên dashboard | dán **Bot Token** vào dashboard |
| Nhận tin | WebSocket listener + tự kết nối lại | long polling `getUpdates` |
| Rủi ro khóa tài khoản | **có** - chỉ dùng nick phụ | **không** |
| Ai nhắn được | phải là bạn bè | ai có link cũng nhắn được |
| Công cụ dùng được | đủ **15** | **7 trong 15** (xem bảng dưới) |
| Định dạng chữ | định dạng gốc Zalo (`textProperties`) | chữ trơn |
| Trần một tin | theo cấu hình (mặc định 2000 ký tự) | 2000 ký tự, server ép cứng |
| Gửi file / ảnh / thả cảm xúc / tag | có | **không có method trên API** |
| Lịch hẹn | có | có |
| Allowlist mặc định | mở | **đóng** (`mode: "list"`) |

> [!IMPORTANT]
> **Zalo Bot API KHÁC Zalo OA API.** Hai sản phẩm này bị nhầm lẫn khắp nơi, kể cả trong tài liệu
> bên thứ ba. Chính sách "chỉ được nhắn trong 7 ngày kể từ tương tác cuối" và biểu phí gửi tin là
> của **OA API** (`openapi.zalo.me`), **không** áp cho Bot API mà dự án này dùng.

### Tạo tài khoản Zalo Bot

1. Mở Zalo, tìm Official Account **"Zalo Bot Manager"**.
2. Chọn **"Tạo bot"** (mở mini app Zalo Bot Creator). Tên bot **bắt buộc bắt đầu bằng "Bot"**.
3. Token được gửi vào tin nhắn Zalo cho bạn.
4. Dashboard > **Accounts** > Thêm account > Loại kênh = **Tài khoản bot chính thức** > dán token.

Server **kiểm token với Zalo trước khi lưu** (gọi `getMe`) - token sai thì không lưu gì cả, tránh
dựng ra một account trông như đã cấu hình xong mà không bao giờ chạy. Lưu xong account tự khởi
động lại ngay.

### Vì sao 8 công cụ không chạy trên kênh Bot

Không phải chọn cho an toàn - là **đo trên API sống**: dò 17 method, 13 cái trả
`{"ok":false,"description":"Not Found","error_code":404}`. Không tồn tại
`sendDocument` / `sendFile` / `sendVideo` / `sendAudio` / `editMessageText` / `deleteMessage` /
`setMessageReaction` / `forwardMessage` / `getChat` / `getChatMember`.

Những công cụ đó bị **gỡ khỏi schema** gửi cho model (model không biết chúng tồn tại, không tốn
token mô tả, không thể bị prompt injection dụ gọi) **và** persona được ghép thêm một luật nói rõ
đây là giới hạn nền tảng chứ agent không hỏng - ai nhờ thì agent trả lời thẳng là tài khoản bot
không gửi được, mời nhắn qua tài khoản cá nhân. Ẩn công cụ mà không nói lý do thì người nhắn
tưởng agent bị lỗi.

> Trước đây danh sách này có **8** công cụ - `schedule_task` nằm trong đó. Nó là mục duy nhất
> không dẫn được một số đo 404 nào: Bot API gửi chủ động tốt (đo: 10 tin trong 416ms), chỉ là
> bộ hẹn lịch khóa cứng vào `zca-js` nên tài khoản bot không có đường gửi. Đã nối ở V3.19.

## Agent làm được gì

15 công cụ, bật/tắt từng cái theo account ngay trên dashboard.

| Công cụ | Làm gì | Kênh Bot |
|---|---|---|
| `web_search` | Tìm web theo chuỗi nguồn (Brave -> DuckDuckGo). DuckDuckGo không cần key | có |
| `web_fetch` | Đọc 1 URL công khai. Chặn IP nội bộ và metadata cloud (chống SSRF) | có |
| `kb_search` | Tra tài liệu chủ agent tự nạp (chính sách, bảng giá, hướng dẫn) - FTS5 + bm25, hợp nhất bằng RRF | có |
| `save_memory` | Ghi nhớ lâu dài về người dùng, sống qua nhiều phiên chat | có |
| `get_datetime` | Ngày giờ chính xác theo múi giờ cấu hình | có |
| `read_image` | Nhìn kỹ lại ảnh với câu hỏi cụ thể: đếm số lượng, đọc chữ nhỏ, soi chi tiết | có |
| `create_image` | Vẽ ảnh AI mới, hoặc **SỬA ảnh người dùng vừa gửi** - phần còn lại giữ nguyên từng pixel | không |
| `create_word_document` | Soạn .docx (tiêu đề, đoạn văn, bảng, hai cột) rồi gửi thẳng trong chat | không |
| `create_excel_file` | Soạn .xlsx nhiều sheet, **có công thức kèm sẵn kết quả** nên xem trước trên điện thoại vẫn thấy số | không |
| `send_file` | Gửi file từ kho `data/shared-files/` hoặc tải từ URL công khai | không |
| `schedule_task` | Đặt/xem/sửa/hủy lịch để agent tự nhắn lại đúng cuộc trò chuyện này | có |
| `tag_member` | @mention đúng người trong nhóm | không |
| `get_group_info` | Tên nhóm, số thành viên, danh sách thành viên | không |
| `add_reaction` | Thả cảm xúc vào tin nhắn | không |
| `tai_video` | Tải video TikTok/Facebook bản không watermark rồi gửi thẳng vào chat | không |

Bộ công cụ thật của một lượt là **phần giao** giữa hai danh sách tắt: **agent** khai năng lực
("vai này biết làm gì"), **account** áp chính sách ("nick này được phép làm gì"). Không bên nào
bật ngược lại được bên kia, nên thêm một agent mới không bao giờ nới rộng quyền của một nick.

Lượt chạy theo lịch còn hẹp hơn nữa: **10 công cụ bị loại** khỏi lượt đó, vì lượt theo lịch chạy cô
lập khỏi lịch sử chat và 6 trong số đó gửi thẳng ra Zalo, né mất trần "số tin chủ động mỗi ngày".

### Tin nhắn có định dạng thật

Trên kênh cá nhân, agent không trả về một khối chữ phẳng. Markdown của model được **dịch** thành
định dạng gốc của Zalo (`textProperties`), không phải bị xóa đi:

- **In đậm**, *in nghiêng*, ~~gạch ngang~~, gạch chân, tiêu đề cỡ lớn
- **Bốn màu chữ** (đỏ, cam, vàng, xanh lá) cho thư mời và thông báo trang trọng
- Emoji dẫn dòng chọn theo đúng nghĩa của dòng

Toàn bộ đã **đo trên máy thật, cả Zalo Web lẫn Zalo điện thoại** - hai client render khác nhau
ở vài chỗ, và những chỗ đó đã bị loại khỏi thiết kế. Zalo còn từ chối cả tin khi chữ cộng định
dạng vượt một ngưỡng byte, nên bộ gửi tự co trần ký tự cho vừa ngân sách rồi mới cắt.

Kênh Bot gửi **chữ trơn** - không phải vì sợ lỗi, mà vì chữ tới đường gửi đã hết markdown rồi
(lớp dịch đã bóc dấu ra thành `Style[]` ở tầng trên), nên xin server dựng lại chỉ có thể bớt ký tự.

### Kho tri thức (RAG)

Nạp tài liệu để agent tra cứu bằng công cụ `kb_search` - chính sách công ty, bảng giá, hướng dẫn
nội bộ, FAQ.

- Định dạng: **.docx, .xlsx, .pdf, .txt, .md** hoặc gõ tay thẳng trên dashboard
- Tìm bằng **SQLite FTS5 + bm25**, hợp nhất nhiều truy vấn bằng **RRF** (chừa sẵn chỗ cho lớp vector sau)
- Nội dung nạp bằng **công cụ**, KHÔNG nhét sẵn vào prompt - giữ nguyên khoản đầu tư prompt cache
  của những lượt không đụng tới kho
- Mỗi agent chỉ đọc được nguồn **đã gán tường minh** cho nó. Mặc định **không** nguồn nào
- Kiểm **chữ ký thật của file** (magic bytes) cho docx/xlsx/pdf, không tin đuôi tên
- Đọc file + cắt đoạn chạy trong **worker thread riêng**, có hai cầu dao: tài liệu quay CPU vô hạn
  bị `terminate()` cắt, tài liệu phình heap bị chặn bằng trần RAM đặt trên dashboard
- Đọc docx/xlsx bằng **SAX theo luồng**, không phải regex bắt cặp thẻ: đo được cặp regex cũ khóa
  cứng event loop **38,09 giây** trên một bom XML 1,7 KB nén (ReDoS bậc hai). Số đó nằm trong test
  hồi quy

### MCP client (cắm server ngoài)

Bot làm MCP **client**: cắm các server MCP bên ngoài (chỉ transport HTTP, không stdio - VPS hạn
chế chạy lệnh tùy ý) để agent tự khám phá và gọi tool của chúng, xếp chồng lên 15 công cụ built-in.

- **Gán theo từng agent, mặc định TẮT** (default-deny): phải gán server cho agent cụ thể trên
  dashboard, agent chưa gán không thấy tool ngoài nào - người lạ chat với agent chưa cấu hình
  không chạm được tool ngoài
- Kết quả tool ngoài bọc như **nội dung không tin cậy**, cùng hàng rào với `web_fetch`; nhánh hỏng
  không ném lỗi ra ngoài
- Header xác thực (token, API key server MCP) mã hóa **AES-256-GCM** như mọi secret khác trong dự án
- **Fingerprint drift**: chụp "dấu vân tay" bộ tool lúc người vận hành duyệt - server đổi tool
  ngầm sau đó thì rơi về trạng thái "chờ duyệt lại", không nạp tool cho tới khi duyệt lại
- Mọi tool ngoài xếp nhóm `action` (không tin annotation server tự khai) và bị loại khỏi lượt chạy
  theo lịch
- `MCP_ENABLED` là kill-switch runtime - tắt trên dashboard là chặn ngay, không cần xóa từng gán

Quản lý ở tab **MCP** trên dashboard. Sơ đồ kiến trúc đầy đủ:
[`docs/mcp-client-architecture.html`](docs/mcp-client-architecture.html).

### Lịch hẹn (agent tự nhắn)

Ba loại lịch: `once`, `every`, `cron`. Đặt bằng lời ngay trong chat hoặc trên dashboard.

- Loại `message`: gửi nguyên văn, **0 token**
- Loại `agent`: chạy một lượt AI riêng, cô lập khỏi lịch sử chat (tra cứu rồi báo cáo)
- Hai lớp chống spam: chặn lịch lặp quá dày lúc tạo, và trần số tin chủ động mỗi ngày
- Lịch trễ vẫn GỬI kèm nhãn "(nhắc trễ, lịch gốc HH:MM)" thay vì im lặng nuốt
- Không nhận chuỗi ngày giờ dạng tự do - chỉ `{date, time}` rời hoặc `{inMinutes}`, vì chuỗi naive
  qua `new Date()` bị hiểu theo giờ hệ điều hành. Mọi quy đổi múi giờ đi qua một đường duy nhất

### Trí nhớ

- Lịch sử hội thoại trong SQLite, theo account + thread
- **Rolling summary**: phần rơi khỏi cửa sổ replay được tóm tắt lại chứ không mất
- **Ngân sách token thật** cho ngữ cảnh (đặt riêng được cho từng agent), không chỉ đếm số tin -
  vượt ngân sách thì bỏ ảnh cũ trước, rồi mới bỏ tin cũ
- **Fact bền** qua `save_memory`, có luật riêng tư bất đối xứng giữa chat riêng và nhóm
- Ảnh nhận được lưu lại, mô tả bằng model vision phụ và cache lại để lượt sau khỏi trả tiền lần nữa

## Dashboard

Hono + React + Tailwind, phục vụ ngay từ chính tiến trình agent tại `http://127.0.0.1:3900`.

| Trang | Dùng để |
|---|---|
| Tổng quan | Token vào/ra theo ngày, gộp mọi account |
| Accounts | Thêm tài khoản Zalo, chọn **loại kênh** (cá nhân / bot chính thức), **quét QR login ngay trên web** hoặc dán **Bot Token**, bật/tắt từng account |
| Agents | Mỗi account một "não": persona riêng, model riêng, bộ công cụ riêng, nguồn tri thức riêng |
| Sessions | Đọc lại từng cuộc trò chuyện, xem tóm tắt agent tự dựng, bật/tắt agent theo thread, **xóa sạch ngữ cảnh** một cuộc trò chuyện |
| Contacts | Danh bạ đã gặp |
| Memory | Xem, sửa, xóa từng điều agent đã nhớ |
| Kho tri thức | Nạp tài liệu (upload hoặc gõ tay), xem đoạn đã cắt, gán nguồn cho từng agent để `kb_search` tra |
| Lịch hẹn | Danh sách lịch, chạy thử ngay, lịch sử từng lần chạy |
| Tools | Bật/tắt 15 công cụ theo account, cấu hình vẽ ảnh và model vision. Chọn một account bot thì công cụ nền tảng không hỗ trợ hiện rõ lý do |
| MCP | Thêm/sửa/xóa MCP server ngoài (chỉ HTTP), gán server cho từng agent (mặc định tắt), duyệt lại khi bộ tool đổi (drift) |
| Cấu hình | **70 tham số** vận hành chỉnh nóng, không cần khởi động lại, chia 13 nhóm |
| Trace | Xem lại từng bước của một lượt agent: model nghĩ gì, gọi tool nào, tham số ra sao |
| Logs | Nhật ký hệ thống |

13 nhóm cấu hình: Nhà cung cấp LLM, Chung, Lượt trả lời (9), Ngữ cảnh & Trí nhớ (6), Tra cứu web
(2), Tạo file Word/Excel (5), Tải video (6), Vẽ ảnh (4), Gửi tin trên Zalo (11), Trace và dọn dẹp (4), Lịch hẹn
(9), Kho tri thức (9), MCP server ngoài (4).

Thứ tự ưu tiên cấu hình ở mọi nơi: **dashboard (DB) > `.env` > mặc định trong schema**. Thiếu cấu
hình LLM không chặn boot - phải vào được dashboard mới nhập được.

## Vì sao đáng tin

Đây là phần khó thấy từ ảnh chụp màn hình, nhưng là phần chiếm nhiều công nhất.

**Chịu được lỗi thật của môi trường thật**

- Mọi lời gọi LLM đi qua **streaming**. Router nằm sau Cloudflare, mà Cloudflare cắt bằng 524 nếu
  origin chưa trả byte đầu trong 100 giây. Đo trên router thật với prompt sinh 4000 chữ:
  non-stream chết ở giây 125, streaming xong ở giây 135 với byte đầu ở giây 7,5
- **Gộp tin theo thread**: người ta nhắn ngắt quãng ba tin thì agent trả lời một lần, không đẻ ra
  ba lượt làm lại từ đầu
- **Chèn tin giữa lượt**: nhắn thêm trong lúc agent đang chạy thì tin đó được kéo vào ngay ở ranh
  giới bước, không phải đợi lượt sau
- Câu trả lời bị Zalo từ chối vì định dạng thì **gửi lại dạng chữ trơn** - mất định dạng chứ
  không mất nội dung
- Chống lặp vòng tool ba tầng, trần thời gian mỗi lượt, trần token đầu ra
- Phân loại lỗi nhà cung cấp thay vì thử lại mù: hết quota tôn trọng `Retry-After` thật, tràn ngữ
  cảnh cắt sâu hơn rồi thử lại, sai khóa thì KHÔNG thử lại và báo câu riêng

**Phòng prompt injection nghiêm túc** (agent đọc tin của người lạ)

- Nội dung web bọc trong thẻ `` có **nonce ngẫu nhiên sinh mỗi lần gọi**, persona
  dạy rõ đó là dữ liệu chứ không phải lệnh. Kẻ tấn công soạn nội dung trước khi biết nonce nên
  không thể viết ra thẻ đóng giả
- `web_fetch` và `send_file` chặn loopback, mạng nội bộ, endpoint metadata của cloud
- **Không có tool chuyển tiền hay thanh toán** - cố ý không làm, dù thư viện có sẵn
- Tool tạo file chỉ nhận **dữ liệu** (tiêu đề, đoạn văn, bảng), tuyệt đối không chạy code model sinh ra
- Câu trả lời rò system prompt bị chặn hẳn trước khi ra Zalo
- Mọi biểu thức chính quy chạm nội dung ngoài đều tuyến tính (chống ReDoS)

**Bảo mật tại chỗ**

- Cookie Zalo và Bot Token mã hóa **AES-256-GCM**, khóa nằm ngoài DB
- Bot Token nằm trong ĐƯỜNG DẪN của Zalo Bot API (`/bot{token}/{method}`), nên client che token
  trong mọi chuỗi sắp vào thông điệp lỗi - ba lớp che, vì đã dựng lại được đường token đi từ trang
  lỗi của cổng trung gian vào dashboard và vào file log
- Mật khẩu dashboard băm bằng scrypt, có rate-limit đăng nhập
- Toàn bộ dữ liệu nằm trên máy bạn: SQLite trong `data/`, không gửi đi đâu ngoài nhà cung cấp LLM bạn chọn

## Cài đặt

```bash
corepack enable
pnpm install
copy .env.example .env
```

`.env` chỉ có **2 dòng phải điền**:

| Biến | Lấy ở đâu |
|---|---|
| `CREDENTIALS_ENCRYPTION_KEY` | `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` |
| `DASHBOARD_PASSWORD` | tự đặt, tối thiểu 8 ký tự |

Mọi thứ còn lại nhập trên dashboard: nhà cung cấp LLM + model + API key (**Cấu hình > Nhà cung cấp
LLM**, làm trước tiên), rồi tài khoản Zalo (trang **Accounts** - quét QR cho tài khoản cá nhân,
hoặc dán Bot Token cho tài khoản bot). Không cần file `config/accounts.json` - DB là nguồn sự thật.

## Chạy

```bash
pnpm dev                    # agent (watch mode) + dashboard
pnpm build:web              # build UI -> web/dist, tiến trình tự serve tại http://127.0.0.1:3900
pnpm dev:web                # dev UI dashboard (Vite, proxy vào API)
pnpm zalo-login acc-chinh   # login QR bằng CLI (cách cũ - trên web tiện hơn; chỉ dùng cho tài khoản CÁ NHÂN)
pnpm zalo-bot-check         # dò Zalo Bot API bằng token thật, in ra method nào sống method nào 404
pnpm test                   # 2657 test
pnpm typecheck              # bắt buộc chạy trước khi báo hoàn thành
pnpm eval                   # 17 case chạy MODEL THẬT, không tin nào ra Zalo thật
```

## Nhà cung cấp LLM

Cắm rời qua `LLM_PROVIDER`, đổi được lúc chạy từ dashboard:

- `openai-compatible` - bất kỳ endpoint nào theo chuẩn OpenAI (router proxy, OpenRouter, LM Studio, Ollama...)
- `anthropic` - gọi thẳng API gốc của Anthropic
- `google` - gọi thẳng API gốc của Google (Gemini). Phải đi đường này chứ không
  qua lớp giả OpenAI của họ: lớp giả làm rơi `thought_signature` nên mọi lượt
  có gọi công cụ sẽ lỗi

Vẽ ảnh và model vision phụ cấu hình riêng, cũng theo chuẩn OpenAI-compatible.

## An toàn - đọc trước khi chạy

> [!WARNING]
> `zca-js` là API **không chính thức**. Zalo có thể khóa tài khoản.
> **Chỉ dùng nick phụ.** Đừng dùng tài khoản chính hay tài khoản có giá trị.
> Rủi ro này **không áp cho tài khoản Zalo Bot chính thức** - đó là API công khai của Zalo.

- `data/` chứa cookie Zalo và Bot Token (đã mã hóa) cùng toàn bộ lịch sử - **không commit, không chia sẻ**
- Mỗi tài

…

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [vuhai2002](https://github.com/vuhai2002)
- **Source:** [vuhai2002/zalo-agent](https://github.com/vuhai2002/zalo-agent)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** yes
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-vuhai2002-zalo-agent
- Seller: https://agentstack.voostack.com/s/vuhai2002
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
