{"_id":"@ai.vn/hermes-zalo-gateway","_rev":"2-c5a70b56c8c9ab6ca32338a4538de58a","name":"@ai.vn/hermes-zalo-gateway","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.0":{"name":"@ai.vn/hermes-zalo-gateway","version":"1.0.0","keywords":["zalo","hermes","hermes-agent","chatbot","zca-js","bridge","messaging","gateway"],"license":"MIT","_id":"@ai.vn/hermes-zalo-gateway@1.0.0","maintainers":[{"name":"thodinh.sg","email":"d.tr.tho@gmail.com"}],"homepage":"https://github.com/thodinh/hermes-zalo-plugin#readme","bugs":{"url":"https://github.com/thodinh/hermes-zalo-plugin/issues"},"bin":{"hermes-zalo-plugin":"bin/cli.mjs"},"dist":{"shasum":"e0dd508ab308d33a66269fc77860d15686daa0ab","tarball":"https://registry.npmjs.org/@ai.vn/hermes-zalo-gateway/-/hermes-zalo-gateway-1.0.0.tgz","fileCount":17,"integrity":"sha512-vB4233Ow9LSu+hTqXv12HcS2YWJYxE56lRaGgPf3yCExnEHGVbMF8nG9dQYXviA2ecFRi6n2FmksNXMx/lHBmA==","signatures":[{"sig":"MEYCIQCtEoLB1qLBTNpNY1ClIEv+fQazLEMxFmRLePdTU3NNLAIhAImOlP6fYs7AJr00VLellhljmGWOToWwMPIcGHD2TPW6","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":231818},"main":"server.js","type":"module","engines":{"node":">=18.0.0"},"gitHead":"b32cd8c767f9f4fc5740f1057785c5a4acd35350","scripts":{"login":"node login.mjs","setup":"node install.mjs","start":"node server.js","uninstall":"node uninstall.mjs","postinstall":"node scripts/postinstall.mjs"},"_npmUser":{"name":"thodinh.sg","email":"d.tr.tho@gmail.com"},"repository":{"url":"git+https://github.com/thodinh/hermes-zalo-plugin.git","type":"git"},"_npmVersion":"11.19.0","description":"Connect a personal Zalo account to the Hermes Agent gateway via zca-js (unofficial Zalo API). SSE inbound + REST outbound. Cross-platform (macOS, Linux, Windows).","directories":{},"_nodeVersion":"26.7.0","dependencies":{"qrcode":"^1.5.4","zca-js":"^2.1.2","express":"^4.21.2","qrcode-terminal":"^0.12.0"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/hermes-zalo-gateway_1.0.0_1788069641477_0.4596842287451812","host":"s3://npm-registry-packages-npm-production"}},"1.0.1":{"name":"@ai.vn/hermes-zalo-gateway","version":"1.0.1","description":"Connect a personal Zalo account to the Hermes Agent gateway via zca-js (unofficial Zalo API). SSE inbound + REST outbound. Cross-platform (macOS, Linux, Windows).","type":"module","bin":{"hermes-zalo-plugin":"bin/cli.mjs"},"main":"server.js","scripts":{"start":"node server.js","login":"node login.mjs","setup":"node install.mjs","uninstall":"node uninstall.mjs","postinstall":"node scripts/postinstall.mjs"},"engines":{"node":">=18.0.0"},"keywords":["zalo","hermes","hermes-agent","chatbot","zca-js","bridge","messaging","gateway"],"license":"MIT","publishConfig":{"registry":"https://registry.npmjs.org","access":"public"},"repository":{"type":"git","url":"git+https://github.com/thodinh/hermes-zalo-plugin.git"},"bugs":{"url":"https://github.com/thodinh/hermes-zalo-plugin/issues"},"homepage":"https://github.com/thodinh/hermes-zalo-plugin#readme","dependencies":{"express":"^4.21.2","qrcode":"^1.5.4","qrcode-terminal":"^0.12.0","zca-js":"^2.1.2"},"gitHead":"cbdde0f2e52b6378ea0be6e4ba3bdf8cc10496de","_id":"@ai.vn/hermes-zalo-gateway@1.0.1","_nodeVersion":"24.19.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-M7ItaCCeWJR/M4TO5CV7PcL11k+fWvpwO/q1N3YYiJK6Rdq9bupKa8inqdp1KUI8CESab9SxkABB/Q1oWYxbmw==","shasum":"c72614ca0e73750ffbb678c58e64cf43bab76487","tarball":"https://registry.npmjs.org/@ai.vn/hermes-zalo-gateway/-/hermes-zalo-gateway-1.0.1.tgz","fileCount":17,"unpackedSize":231818,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@ai.vn%2fhermes-zalo-gateway@1.0.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIEZ2lAsTd84fm3rRzokVL3gWU0nPlJmHBXbEbG1mp99GAiASh5O6caJCizoLLoiSVaiEvqoHomt6iP8iNbVNBGO3lA=="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:bc883373-20a0-4aaa-ad17-319c79e31018"}},"directories":{},"maintainers":[{"name":"thodinh.sg","email":"d.tr.tho@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/hermes-zalo-gateway_1.0.1_1788070011283_0.5851248145957173"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-30T06:00:41.323Z","modified":"2026-08-30T06:06:51.739Z","1.0.0":"2026-08-30T06:00:41.620Z","1.0.1":"2026-08-30T06:06:51.427Z"},"bugs":{"url":"https://github.com/thodinh/hermes-zalo-plugin/issues"},"license":"MIT","homepage":"https://github.com/thodinh/hermes-zalo-plugin#readme","keywords":["zalo","hermes","hermes-agent","chatbot","zca-js","bridge","messaging","gateway"],"repository":{"type":"git","url":"git+https://github.com/thodinh/hermes-zalo-plugin.git"},"description":"Connect a personal Zalo account to the Hermes Agent gateway via zca-js (unofficial Zalo API). SSE inbound + REST outbound. Cross-platform (macOS, Linux, Windows).","maintainers":[{"name":"thodinh.sg","email":"d.tr.tho@gmail.com"}],"readme":"<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/thodinh/hermes-zalo-plugin/main/assets/logo.svg\" alt=\"hermes-zalo-plugin\" width=\"660\">\n</p>\n\n# hermes-zalo-plugin\n\n[English](./README.md) · 📖 **Tiếng Việt**\n\n[![npm version](https://img.shields.io/npm/v/hermes-zalo-plugin.svg)](https://www.npmjs.com/package/hermes-zalo-plugin)\n[![npm downloads](https://img.shields.io/npm/dm/hermes-zalo-plugin.svg)](https://www.npmjs.com/package/hermes-zalo-plugin)\n[![GitHub stars](https://img.shields.io/github/stars/thodinh/hermes-zalo-plugin?style=social)](https://github.com/thodinh/hermes-zalo-plugin/stargazers)\n[![license](https://img.shields.io/npm/l/hermes-zalo-plugin.svg)](./LICENSE)\n\nCầu nối (bridge) Node.js kết nối **zca-js** (API Zalo cá nhân KHÔNG chính thức)\nvới gateway của **Hermes Agent**. Nhờ nó, bạn có thể chat với Hermes agent từ\nmột tài khoản Zalo cá nhân.\n\n```\nMáy chủ Zalo  <──>  [ bridge này: Node + zca-js ]  <──>  [ adapter Hermes: platform \"zalo\" ]\n```\n\n- **Zalo ↔ bridge:** sự kiện chiều vào đến qua **ws** (listener); hành động chiều\n  ra (gửi / upload / react…) đi qua **HTTPS** tới API của Zalo.\n- **bridge ↔ Hermes — chiều vào** (Zalo → Hermes): Server-Sent Events tại\n  `GET /events` (heartbeat mỗi 15s + replay `Last-Event-ID` từ ring buffer).\n- **bridge ↔ Hermes — chiều ra** (Hermes → Zalo): REST `POST /send`,\n  `/send-attachment`, `/send-sticker`, `/send-voice`, `/typing`.\n\n**Vì sao chọn zca-js (thay vì thư viện Zalo không chính thức bằng Python)?** Cả\nhai đều **KHÔNG chính thức**, dựng bằng reverse-engineer cho API tài khoản Zalo\n**cá nhân** — không cái nào được Zalo bảo trợ. Mình chọn\n[zca-js](https://www.npmjs.com/package/zca-js) vì nó **còn được bảo trì tích cực\nhơn** và **hỗ trợ nhiều chức năng hơn** so với bản Python: bám theo thay đổi\nprotocol của Zalo, bao phủ nhắn tin, media, sticker, reaction, nhóm, poll, kết\nbạn… đủ 145 method mà bridge này phơi ra. Cái giá của lựa chọn đó — zca-js là\nTypeScript còn Hermes là Python — chính là thứ mà bridge Node mỏng này gánh.\n\n> ⚠️ **zca-js là API KHÔNG CHÍNH THỨC.** Nên dùng tài khoản Zalo phụ. Zalo có\n> thể giới hạn tốc độ (rate-limit) hoặc khóa tài khoản tự động hóa. Bạn tự chịu\n> rủi ro này.\n\n## Yêu cầu\n\nTrước khi cài, cần có sẵn:\n\n| Yêu cầu | Để làm gì | Lấy ở đâu |\n|---------|-----------|-----------|\n| **Node.js ≥ 18** (kèm `npm`) | chạy bridge | macOS: `brew install node` · Linux: [nvm](https://github.com/nvm-sh/nvm) hoặc gói `nodejs` của distro · Windows: bộ cài LTS tại [nodejs.org](https://nodejs.org). Kiểm tra: `node -v` |\n| **Tài khoản Zalo** (nên dùng phụ) | bridge đăng nhập bằng tài khoản này | app Zalo trên điện thoại để quét QR |\n| **Đã cài Hermes Agent** | agent chat nói chuyện với bridge | lệnh `hermes` có trên PATH |\n| **Python `aiohttp`** | adapter Zalo phía Hermes dùng cho HTTP/SSE | `pip install aiohttp` (luồng `hermes gateway setup` Zalo cũng nhắc lại) |\n\nTrình cài đặt kiểm tra Node + npm và dừng lại với thông báo rõ ràng nếu thiếu —\nkhông chạy nửa chừng rồi để bạn bối rối. Bản thân zca-js **không cần công cụ\nbuild** (không `bun`, không trình biên dịch); nó được lấy bản dựng sẵn từ npm.\n\n## Bắt đầu nhanh (chỉ 1 lần)\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/thodinh/hermes-zalo-plugin/main/assets/setup-flow.svg\" alt=\"4 bước: cài, quét QR, đăng ký Hermes, chat từ Zalo\" width=\"620\">\n</p>\n\nChạy trên **macOS, Linux, và Windows** — Node lo hết; không cần `bun`, không cần\nbuild từ source (zca-js lấy từ npm).\n\n**Điều kiện:** Node.js ≥ 18 ([nodejs.org](https://nodejs.org)).\n\n**Cài từ npm (khuyến nghị):**\n\n```bash\nnpm install -g @ai.vn/hermes-zalo-gateway\nhermes-zalo-plugin setup      # đăng nhập QR + dịch vụ nền\n```\n\n**Hoặc từ source checkout:**\n\n```bash\n# macOS / Linux\n./install.sh\n\n# Windows (PowerShell)\n.\\install.ps1\n```\n\nTrình cài đặt sẽ:\n1. cài dependencies (khi chạy từ source),\n2. hướng dẫn **đăng nhập QR** (quét một lần; credentials được lưu vào\n   `~/.hermes-zalo/`), và\n3. cài **dịch vụ nền** tự khởi động bridge khi đăng nhập/khởi động máy và tự\n   chạy lại khi crash — launchd (macOS), systemd user unit (Linux), hoặc\n   Scheduled Task (Windows).\n\nCác lệnh CLI: `hermes-zalo-plugin setup | login | start | stop | status | uninstall`.\n\nSau đó đăng ký vào Hermes:\n\n```bash\nhermes gateway setup     # chọn \"Zalo\" (🇻🇳)\nhermes gateway           # bắt đầu chuyển tiếp tin\n```\n\nVậy là xong — đăng nhập + setup chỉ làm một lần; bridge tự sống.\n\n### Cờ tùy chọn của installer\n\n| Cờ | Tác dụng |\n|----|----------|\n| `--no-service` | Chỉ cài deps + login; tự chạy bridge bằng `npm start`. |\n| `--relogin` | Bắt buộc đăng nhập QR lại (vd khi phiên hết hạn). |\n| `--service-only` | Chỉ (cài lại) dịch vụ nền. |\n\nGỡ dịch vụ nền (giữ lại credentials):\n\n```bash\nnode uninstall.mjs            # dừng + gỡ dịch vụ tự khởi động\nnode uninstall.mjs --purge    # xóa luôn credentials đã lưu (đăng xuất)\n```\n\n## Cài thủ công (nâng cao)\n\nNếu bạn không muốn dùng trình cài đặt:\n\n```bash\nnpm install                  # lấy zca-js từ npm\nnode login.mjs               # đăng nhập QR (--force để quét lại)\nnpm start                    # chạy bridge ở foreground\n```\n\nBạn cũng có thể lấy QR trong lúc server đang chạy:\n`GET /qr` (JSON kèm ảnh base64) hoặc `GET /qr.png` (ảnh PNG thô).\n\n## 3. Cấu hình (biến môi trường)\n\n| Biến | Mặc định | Ý nghĩa |\n|------|----------|---------|\n| `ZALO_PLUGIN_PORT` | `8787` | Cổng lắng nghe |\n| `ZALO_PLUGIN_HOST` | `127.0.0.1` | Host bind (giữ loopback trừ khi bạn thêm TLS) |\n| `ZALO_PLUGIN_TOKEN` | _(trống)_ | Khóa bí mật dùng chung; nếu đặt, mọi route đều yêu cầu (header `x-bridge-token`, `Authorization: Bearer`, hoặc `?token=`) |\n| `ZALO_DATA_DIR` | `~/.hermes-zalo` | Thư mục gốc cho mọi dữ liệu runtime (credentials, QR, cache thu hồi, log). Đặt `./data` để dùng kiểu cũ trong repo. Các biến từng-file bên dưới override từng đường dẫn. |\n| `ZALO_CREDENTIALS_PATH` | `~/.hermes-zalo/credentials.json` | Nơi lưu credentials |\n| `ZALO_QR_PATH` | `~/.hermes-zalo/qr.png` | Nơi ghi ảnh QR |\n| `ZALO_SELF_LISTEN` | tắt | Nhận cả tin nhắn do chính mình gửi đi |\n| `ZALO_FORCE_QR` | tắt | Bỏ qua credentials đã lưu, đăng nhập lại bằng QR |\n| `ZALO_CLIMSG_RETENTION_DAYS` | `30` | Số ngày giữ cache thu hồi (msgId→cliMsgId) trên đĩa tại `~/.hermes-zalo/climsgids/` (JSONL xoay theo ngày, tự dọn). Nạp lại khi khởi động để chức năng thu hồi (undo) sống sót qua restart. `0` = tắt lưu đĩa (chỉ trong RAM). |\n| `ZALO_ALLOWED_ACTION_GROUPS` | `read,send,interact` | Danh sách nhóm quyền (phân theo mức độ nguy hiểm): `read` < `send` < `interact` < `manage` < `destructive` (hoặc `all`). Chặn CẢ `/api/<method>` lẫn các route first-class. |\n| `ZALO_ALLOW_DESTRUCTIVE` | `false` | Phải `true` mới cho phép nhóm `destructive` (disperseGroup, deleteMessage, deleteChat, removeFriend, blockUser, leaveGroup, changeGroupOwner, updateProfile/Settings…). TẮT ngay cả khi groups=`all`. |\n| `ZALO_ALLOWED_ACTIONS` | _(trống)_ | Allowlist tùy chỉnh — danh sách tên method zca-js luôn được phép, bất kể nhóm. |\n| `ZALO_DENIED_ACTIONS` | _(trống)_ | Denylist tùy chỉnh — danh sách method luôn bị chặn. Ưu tiên cao nhất (thắng cả allowlist, groups, mọi thứ). |\n\n### Phân quyền hành động (action permissions)\n\nBridge phân loại toàn bộ 145 hành động của zca-js thành 5 nhóm theo mức độ nguy\nhiểm và từ chối (HTTP `403`) bất kỳ hành động nào không được chính sách cho\nphép. Thứ tự ưu tiên:\n\n1. `ZALO_DENIED_ACTIONS` — luôn chặn.\n2. `ZALO_ALLOWED_ACTIONS` — luôn cho phép.\n3. Nhóm `destructive` — chỉ khi `ZALO_ALLOW_DESTRUCTIVE=true`.\n4. `ZALO_ALLOWED_ACTION_GROUPS` — nhóm của method phải nằm trong danh sách.\n\nSố lượng mỗi nhóm: read 55, send 12, interact 13, manage 39, destructive 26.\nBảng phân loại đầy đủ nằm trong `permissions.js` (tự sinh). `GET /policy` trả về\nchính sách đang áp dụng + danh sách hành động được phép đã giải quyết.\n\n### Ai được nhắn với bot (người gửi / thread / chế độ nhóm)\n\nĐây là các biến **phía adapter** (đặt nơi chạy `hermes gateway`), kiểu giống\nTelegram — để TRỐNG = cho phép tất cả / mọi nơi:\n\n| Biến | Mặc định | Tác dụng |\n|------|----------|----------|\n| `ZALO_ALLOWED_USERS` | _(trống=tất cả)_ | Danh sách uid người gửi được phép điều khiển bot. |\n| `ZALO_ALLOWED_THREADS` | _(trống=tất cả)_ | Danh sách id thread/nhóm mà bot hoạt động trong đó. |\n| `ZALO_GROUP_MODE` | `mention` | Trong nhóm: `mention` (chỉ khi được @nhắc hoặc trả lời vào tin bot — phát hiện bằng uid thật, không đoán theo chữ), `all` (mọi tin nhắn), hoặc `off` (chỉ chat 1-1). |\n| `ZALO_LOG_IDS` | `false` | Log `uid`/`threadId` của mỗi tin đến để bạn tìm id thêm vào allowlist. |\n\nTrình wizard (`hermes gateway setup` → Zalo) gọi `/contacts` và cho bạn **tìm\ntheo tên rồi chọn** thay vì phải gõ id thô.\n\n> **Lưu ý mặc định** (chú ý sự bất đối xứng):\n> - **Người dùng** — để trống `ZALO_ALLOWED_USERS` thì **bất kỳ ai** cũng nhắn\n>   được với bot (cho phép tất cả, kiểu Telegram).\n> - **Nhóm** — trong `hermes gateway setup`, nếu **không chọn nhóm nào**, wizard\n>   set `ZALO_GROUP_MODE=off`: bot **không** trả lời trong **bất kỳ nhóm nào**\n>   (kể cả khi được @nhắc). Chat 1-1 (DM) vẫn hoạt động. Chọn nhóm cụ thể để rồi\n>   chọn cách bot xử trong nhóm (`mention` / `all` / `off`).\n\n### Giới hạn tốc độ gọi info (chống khóa tài khoản)\n\nzca-js không chính thức; gọi dồn dập `getUserInfo`/`getGroupInfo`/`getAllGroups`/`getAllFriends`\ncó nguy cơ bị chặn tạm thời. Bridge cache các kết quả này theo id với TTL, xếp\nhàng tuần tự với khoảng cách tối thiểu, và lùi dần (backoff, trả cache cũ) khi\nnghi bị rate-limit:\n\n| Biến | Mặc định | Tác dụng |\n|------|----------|----------|\n| `ZALO_INFO_CACHE_TTL` | `600` (giây) | TTL cache cho kết quả đọc info. |\n| `ZALO_INFO_MIN_INTERVAL_MS` | `1500` | Số ms tối thiểu giữa 2 lần gọi info; backoff lũy thừa (tối đa 5 phút) khi gặp lỗi rate-limit. |\n\n## 4. HTTP API\n\n- `GET  /health` → `{ ok, loggedIn, sessionDead, sessionDeadReason, ownId, qr, sseClients }`\n- `GET  /qr` / `GET /qr.png` → trạng thái QR / ảnh PNG\n- `GET  /events` → luồng SSE (`event: message` / `status` / `session_dead` / `reaction` / `undo` / `friend_event` / `group_event`)\n- `POST /relogin` → `{ forceQR? }` khôi phục phiên chết/hết hạn (chạy lại đăng nhập QR; rồi poll `/qr.png` để quét)\n- `POST /shutdown` → dừng êm (đóng listener, SSE, file stream, thoát). SIGTERM/SIGINT cũng vậy.\n- `POST /send` → `{ threadId, threadType: \"user\"|\"group\", text, mentions?, quote? }` (mentions = `[{pos,uid,len}]` để @nhắc; quote = một SendMessageQuote từ tin đến để trả lời)\n- `POST /react` → `{ threadId, threadType, msgId, cliMsgId?, icon }` (icon = HEART/LIKE/HAHA/WOW/CRY/ANGRY/… hoặc raw)\n- `POST /undo` → `{ threadId, threadType, msgId }` (thu hồi tin của mình; bridge tự tra cliMsgId thật từ cache echo của listener — chỉ cần truyền msgId)\n- `POST /send-card` → `{ threadId, threadType, userId, phoneNumber? }` (gửi danh thiếp)\n- `POST /friend/request|accept|reject` → `{ userId, msg? }`\n- `GET  /friends` → liệt kê tất cả bạn bè · `GET /find-user?phone=` → tra theo số điện thoại\n- `GET  /groups` → liệt kê tất cả nhóm (raw `gridVerMap`)\n- `GET  /contacts` → `{ groups:[{id,name}], friends:[{id,name}] }` — danh sách id+tên thân thiện cho wizard (batched + cache + giới hạn tốc độ)\n- `POST /group/create` `{name, members[]}` · `/group/add` `/group/remove` `/group/rename` `/group/deputy` `{groupId, members[]|name}` · `/group/leave` `{groupId, silent?}`\n- `POST /poll/create` → `{ groupId, question, options[], expiredTime?, allowMultiChoices?, allowAddNewOption?, hideVotePreview?, isAnonymous? }`\n- `POST /api/<method>` → `{ args: [...] }` — **passthrough chung tới BẤT KỲ method nào của zca-js** (đủ 145 API). Truyền args theo thứ tự như zca-js mô tả; dùng `\"user\"`/`\"group\"` ở vị trí cần ThreadType (tự chuyển đổi). Ví dụ: `/api/forwardMessage`, `/api/deleteMessage`, `/api/sendVideo`, `/api/getGroupMembersInfo`, `/api/getGroupChatHistory`, `/api/createReminder`, `/api/setMute`, `/api/votePoll`, `/api/blockUser`, `/api/updateProfile`. Method không tồn tại → lỗi.\n- `POST /send-attachment` → `{ threadId, threadType, path | paths[], caption? }` (đường dẫn file local; ảnh/file/video tự định tuyến theo phần mở rộng)\n- `POST /send-sticker` → `{ threadId, threadType, sticker: { id, cateId, type } }`\n- `GET  /stickers?keyword=hi&limit=5` → tìm sticker, trả về object đầy đủ `{ id, cateId, type, ... }` sẵn sàng đưa vào `/send-sticker`\n- `POST /send-voice` → `{ threadId, threadType, voiceUrl }`\n- `POST /typing` → `{ threadId, threadType }`\n- `GET  /chat-info?threadId=..&threadType=user|group`\n\nCấu trúc sự kiện `message` chiều vào:\n\n```json\n{\n  \"messageId\": \"...\", \"cliMsgId\": \"...\",\n  \"threadId\": \"...\", \"threadType\": \"user|group\",\n  \"senderId\": \"...\", \"senderName\": \"...\", \"text\": \"...\",\n  \"attachment\": null,\n  \"media\": null,                 // {kind,url,fileName,ext,mime,size} cho image/voice/file/video\n  \"msgType\": \"webchat\",\n  \"mentions\": [],                // chỉ nhóm: danh sách uid được @nhắc trong tin này\n  \"quotedOwnerId\": \"\",           // uid chủ nhân tin được trích dẫn (có khi là tin trả lời)\n  \"quote\": { \"...\": \"...\" },     // payload quote thô để dựng tin trả lời\n  \"ts\": \"...\", \"isSelf\": false\n}\n```\n\n`mentions` và `quotedOwnerId` chính là cái adapter dùng để phát hiện \"bot bị\nnhắc đến\" trong nhóm (khớp uid thật, không đoán theo chữ).\n\n## 5. Kết nối plugin Hermes\n\n`hermes-zalo-plugin setup` (và `install.sh` / `install.ps1` từ source) đã **đóng\ngói sẵn và tự cài** adapter phía Hermes — copy `hermes-plugin/` vào\n`~/.hermes/plugins/zalo/` và bật `zalo-platform` trong `~/.hermes/config.yaml`,\nnên bình thường bạn không cần tự đặt file nào. Việc còn lại là khai báo *ai và\nviệc gì* bot được phép làm:\n\n### Cách A — wizard hướng dẫn (khuyến nghị)\n\n```bash\nhermes gateway setup        # chọn \"Zalo\"\n```\n\nWizard hỏi bridge URL/token, rồi — với bridge đã đăng nhập sẵn — lấy danh sách\nnhóm và bạn bè (`GET /contacts`) và cho bạn **tìm theo tên rồi chọn** xem bot\nđược nhắn với ai / thread nào, chế độ trả lời trong nhóm, phân quyền hành động,\nvà thời gian giữ cache. Tất cả ghi vào `~/.hermes/.env`.\n\n### Cách B — đặt env thủ công\n\n```bash\nexport ZALO_PLUGIN_URL=\"http://127.0.0.1:8787\"\n# Phân quyền (kiểu Telegram: để trống = cho phép tất cả / mọi nơi)\n# export ZALO_ALLOWED_USERS=\"<uid1>,<uid2>\"      # giới hạn người gửi\n# export ZALO_ALLOWED_THREADS=\"<groupId>,<uid>\"  # giới hạn nhóm/chat 1-1\n# export ZALO_GROUP_MODE=\"mention\"               # mention | all | off\npip install aiohttp                               # nếu chưa có\nhermes gateway   # adapter Zalo kết nối tới bridge và bắt đầu chuyển tiếp tin\n```\n\nChạy bridge trước (đã đăng nhập), rồi mới chạy Hermes gateway.\n\n> ⚠️ `ZALO_ALLOW_ALL_USERS` và `ZALO_GROUP_REQUIRE_MENTION` đã LỖI THỜI\n> (deprecated). Để `ZALO_ALLOWED_USERS` trống là cho phép tất cả; dùng\n> `ZALO_GROUP_MODE` thay cho cờ mention cũ.\n\n## 6. Dùng hằng ngày\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/thodinh/hermes-zalo-plugin/main/assets/zalo-chat.svg\" alt=\"Tương tác với Hermes agent từ Zalo\" width=\"300\">\n</p>\n\n- **Chat 1-1:** nhắn tới tài khoản Zalo từ một điện thoại khác → agent trả lời\n  (tùy theo `ZALO_ALLOWED_USERS`).\n- **Trong nhóm:** mặc định (`ZALO_GROUP_MODE=mention`) bot chỉ trả lời khi được\n  @nhắc hoặc khi có người trả lời vào một tin của bot. Đặt `all` để trả lời mọi\n  tin, hoặc `off` để bỏ qua nhóm.\n- **Tìm ID sau này:** đặt `ZALO_LOG_IDS=true`, gửi một tin, rồi đọc dòng\n  `uid=… threadId=…` trong log gateway; thêm vào allowlist.\n- **Đổi ai/việc gì được phép:** sửa các biến `ZALO_*` trong `~/.hermes/.env`\n  (phía adapter: users/threads/mode) hoặc env của bridge (phân quyền hành động,\n  rate-limit), rồi khởi động lại gateway / bridge.\n- **Gửi media / sticker / reaction / poll:** agent gọi các route của bridge ở\n  trên; toàn bộ 145 API đều với tới được qua `POST /api/<method>` tùy theo chính\n  sách phân quyền.\n\n## 7. Xử lý sự cố\n\n| Triệu chứng | Nguyên nhân / cách khắc phục |\n|-------------|------------------------------|\n| `/health` báo `loggedIn:false` | Chưa thiết lập phiên — chạy `ZALO_FORCE_QR=1 node server.js` rồi quét, hoặc `POST /relogin`. |\n| `/health` báo `sessionDead:true` | Đã đăng nhập nơi khác / bị kick / cookie hết hạn. `POST /relogin {forceQR:true}` rồi quét lại. |\n| Hành động trả HTTP 403 | Bị chính sách phân quyền chặn — xem `GET /policy`; nới `ZALO_ALLOWED_ACTION_GROUPS` hoặc đặt `ZALO_ALLOW_DESTRUCTIVE=true` / `ZALO_ALLOWED_ACTIONS`. |\n| Bot bỏ qua tin trong nhóm | `ZALO_GROUP_MODE=mention` mà bạn không @nhắc/trả lời; hoặc thread không nằm trong `ZALO_ALLOWED_THREADS`. |\n| \"Zalo info calls are backing off\" | Đã chạm rate-limit; bridge đang tự giảm tốc. Chờ, hoặc tăng `ZALO_INFO_CACHE_TTL` để dựa vào cache. |\n| `getGroupInfo` trả về rỗng | Phải gọi với MỘT MẢNG id; truyền 1 string đơn sẽ không trả gì. |\n| Không thấy log realtime | Node buffer stdout khi không phải TTY — chạy với `stdbuf -oL -eL node server.js \\| tee ~/.hermes-zalo/bridge.log`. |\n| `hermes-zalo-plugin: command not found` sau khi `npm i -g` | Thư mục bin global của npm không nằm trên PATH (hay gặp khi cài bằng Node mà Hermes bundle ở `~/.hermes/node`). Từ v1.0.1, lệnh được tự symlink cạnh `node` của bạn khi cài; nếu vẫn không thấy, chạy `rehash` (zsh) / `hash -r` (bash) hoặc mở terminal mới — hoặc dùng `npx @ai.vn/hermes-zalo-gateway <cmd>`. |\n\n## Chạy như dịch vụ nền\n\nTrình cài đặt đã thiết lập sẵn (launchd / systemd / Scheduled Task) nên bridge\ntự khởi động và tự chạy lại khi crash. Nếu bạn dùng `--no-service`, chạy\n`node install.mjs --service-only` để thêm sau, hoặc chỉ cần `npm start` để chạy\nở foreground. Bridge tự kết nối lại websocket Zalo (zca-js `retryOnClose`);\nadapter Hermes tự kết nối lại luồng SSE với backoff + replay `Last-Event-ID`.\n\n## Giấy phép\n\nMIT © [Cường Tuấn Nguyễn](https://github.com/cuongdev)\n\n## Lịch sử Star\n\nNếu dự án giúp ích cho bạn, một ⭐ sẽ giúp người khác tìm thấy nó.\n\n[![Star History Chart](https://api.star-history.com/svg?repos=thodinh/hermes-zalo-plugin&type=Date)](https://star-history.com/#thodinh/hermes-zalo-plugin&Date)\n","readmeFilename":"README.vi.md"}