abs-zalo-bot 0.8.2 → 0.11.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/README.md +26 -12
- package/hermes-plugin/EXTENDING.md +418 -0
- package/hermes-plugin/starter-kit/AGENT.md +3 -3
- package/hermes-plugin/zalo/__init__.py +3 -0
- package/hermes-plugin/zalo/adapter.py +1945 -0
- package/hermes-plugin/zalo/flood.py +91 -0
- package/hermes-plugin/zalo/plugin.yaml +56 -0
- package/hermes-plugin/zalo-style-guide.md +98 -50
- package/hermes-plugin/zalo_tools/__init__.py +29 -0
- package/hermes-plugin/zalo_tools/facebook.py +249 -0
- package/hermes-plugin/zalo_tools/file_maker.py +583 -0
- package/hermes-plugin/zalo_tools/people.py +163 -0
- package/hermes-plugin/zalo_tools/plugin.yaml +39 -0
- package/hermes-plugin/zalo_tools/tools.py +3115 -0
- package/mcp/server.js +1 -1
- package/package.json +34 -51
- package/scripts/doctor.js +19 -0
- package/scripts/hermes-install-lib.js +411 -0
- package/scripts/install-hermes.js +20 -0
- package/scripts/setup-env.js +66 -0
- package/scripts/uninstall-hermes.js +19 -0
- package/src/config.js +10 -0
- package/src/hermes_bridge.js +19 -8
- package/src/hermes_media.js +107 -4
- package/src/inbound_router.js +13 -6
- package/src/policy.js +67 -8
- package/src/quote_resolver.js +45 -6
- package/src/schema.js +39 -2
- package/src/store.js +24 -0
- package/src/zalo_math.js +122 -0
- package/src/zalo_mentions.js +123 -4
- package/src/zalo_runtime.js +162 -55
- package/src/zalo_styler.js +274 -127
- package/.env.example +0 -61
- package/Dockerfile +0 -23
- package/SECURITY.md +0 -22
- package/config/bots.example.json +0 -33
- package/config.toml +0 -65
- package/docker-compose.yml +0 -15
- package/install.sh +0 -6
- package/setup.bat +0 -67
- package/setup.ps1 +0 -30
- package/setup.sh +0 -12
- package/start.bat +0 -22
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/abs-zalo-bot)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](test/)
|
|
6
6
|
[](mcp/)
|
|
7
7
|
[](mcp/)
|
|
8
8
|
|
|
@@ -20,7 +20,7 @@ Install once · run with 1 command or browser QR · AI Agents connect via Model
|
|
|
20
20
|
| **Architecture** | **Dual-Adapter: Personal QR + Official OA (Webhook)** | Single unofficial scraping adapter |
|
|
21
21
|
| **Safety & Privacy** | **Fail-Closed PolicyGuard + Secret Redaction** | No guardrails (high ban/checkpoint risk) |
|
|
22
22
|
| **AI Integration** | **Native Model Context Protocol (MCP Stdio Server)** | Raw HTTP webhooks / Manual glue code |
|
|
23
|
-
| **Code Quality** | **
|
|
23
|
+
| **Code Quality** | **138/138 Automated Unit & Integration Tests** | Little to no test coverage |
|
|
24
24
|
| **Multi-Agent Ready** | **Hermes Agent, Claude Code, OpenAI Codex, Cursor** | Single-system or standalone CLI only |
|
|
25
25
|
|
|
26
26
|
---
|
|
@@ -184,7 +184,7 @@ For profile-aware quality, select `gateway_skill = "your-owner-authored-hermes-s
|
|
|
184
184
|
- `__underline__`: Zalo Underline (`u`).
|
|
185
185
|
- `~~strikethrough~~`: Zalo StrikeThrough (`s`).
|
|
186
186
|
- Color tags: `[RED]...[/RED]` (Ruby Red), `[GREEN]...[/GREEN]` (Emerald Green), `[ORANGE]...[/ORANGE]` (Amber Orange), `[YELLOW]...[/YELLOW]` (Royal Gold) — supports Vietnamese equivalents `[ĐỎ]`, `[XANH]`, `[CAM]`, `[VÀNG]`.
|
|
187
|
-
- **Safe
|
|
187
|
+
- **Safe Styled Chunker (`formatAndChunkZaloMarkdown`)**: Splits long output without dropping characters or breaking UTF-16 surrogate pairs. Every bubble is capped at 2,000 UTF-16 code units, 40 styles, and 3,000 encoded bytes; styles are clipped and rebased per bubble.
|
|
188
188
|
|
|
189
189
|
---
|
|
190
190
|
|
|
@@ -227,22 +227,36 @@ cat hermes-plugin/starter-kit/SOUL.md >> ~/.hermes/SOUL.md
|
|
|
227
227
|
|
|
228
228
|
---
|
|
229
229
|
|
|
230
|
-
## 🆕 What's New in v0.
|
|
230
|
+
## 🆕 What's New in v0.11.0 (Hermes Platform & Enterprise Toolsets)
|
|
231
231
|
|
|
232
|
-
> **
|
|
232
|
+
> **Hermes Native Platform Release** — 1-click Hermes integration, dual-tier role-based security, live dashboard telemetry, and modular zero-bloat MCP extensibility.
|
|
233
233
|
|
|
234
234
|
| Feature | Detail |
|
|
235
235
|
| :--- | :--- |
|
|
236
|
-
| **
|
|
237
|
-
| **
|
|
238
|
-
| **
|
|
239
|
-
| **
|
|
236
|
+
| **1-Click Hermes Installer** | Run `npm run install:hermes` to automatically detect Hermes layout, atomically merge `config.yaml`, and enable `platforms/zalo`. |
|
|
237
|
+
| **Diagnostic Doctor** | Run `npm run doctor` to execute an 11-point health check verifying plugins, tokens, and bridge connectivity. |
|
|
238
|
+
| **Dual-Tier Toolsets** | **Owner Mode (`zalo_owner`)**: Owner UID unlocks full Hermes terminal, file I/O, browser, and skills.<br>**Public Mode (`zalo_public`)**: Safe conversational tools only, blocking terminal and file tampering from strangers. |
|
|
239
|
+
| **Live Dashboard Telemetry** | Full bi-directional event stream rendered in real-time on Hermes Agent Dashboard / Web UI. |
|
|
240
|
+
| **Lean Modular Architecture** | Zero bloated dependencies in core. Connect any local or cloud tool (ZeroTTS, yt-dlp, deep research) on-demand via standard MCP. |
|
|
240
241
|
|
|
241
242
|
---
|
|
242
243
|
|
|
243
|
-
##
|
|
244
|
+
## 🆕 What's New in v0.9.2
|
|
244
245
|
|
|
245
|
-
|
|
246
|
+
> **Rich-message reliability release** — complete long replies, quoted context, and broader media normalization.
|
|
247
|
+
|
|
248
|
+
| Feature | Detail |
|
|
249
|
+
| :--- | :--- |
|
|
250
|
+
| **Lossless Styled Chunking** | Long Markdown replies are rendered once, split on readable boundaries, then sent sequentially. Each bubble stays within 2,000 UTF-16 code units, 40 styles, and 3,000 encoded bytes. Styles are clipped and rebased instead of discarded. |
|
|
251
|
+
| **Safe Delivery Semantics** | A quote appears only on the first bubble; an attachment only on the last. Plain-text retry occurs only after a numeric provider rejection. Ambiguous network failures are not retried, preventing likely duplicate sends. |
|
|
252
|
+
| **Quoted Context** | Quoted text and media become normalized Hermes context. Provider URLs remain staging inputs and are excluded from model-facing raw metadata. |
|
|
253
|
+
| **Media Normalization** | Additional Zalo media URL fields are classified and staged through the existing jail and SSRF protections. |
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## 🔄 Upgrade (For Existing Users)
|
|
258
|
+
|
|
259
|
+
If you already installed `abs-zalo-bot`, upgrade to **v0.9.2** after backing up your runtime data:
|
|
246
260
|
|
|
247
261
|
- **If installed via NPM:**
|
|
248
262
|
```bash
|
|
@@ -258,7 +272,7 @@ If you already installed `abs-zalo-bot`, upgrading to **v0.7.1** takes 5 seconds
|
|
|
258
272
|
docker compose pull && docker compose up -d
|
|
259
273
|
```
|
|
260
274
|
|
|
261
|
-
>
|
|
275
|
+
> The upgrade does not intentionally modify `data/sessions/`, `data/bridge.sqlite3`, or `.env`. Back them up before upgrading; reconnection behavior still depends on the active Zalo session.
|
|
262
276
|
|
|
263
277
|
---
|
|
264
278
|
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
# Thêm công cụ mới — những cái bẫy đã trả giá
|
|
2
|
+
|
|
3
|
+
Tài liệu này không mô tả kiến trúc (xem README). Nó ghi lại **những chỗ hỏng mà
|
|
4
|
+
không báo lỗi** — loại lỗi tốn nhiều giờ nhất, vì mọi thứ trông vẫn chạy đúng.
|
|
5
|
+
|
|
6
|
+
Điểm chung của sáu cái bẫy dưới đây: hệ thống không hề gãy. Log vẫn xanh, bot vẫn
|
|
7
|
+
trả lời, chỉ là trả lời sai thứ. Nên nguyên tắc bao trùm là **đo ở nơi người dùng
|
|
8
|
+
thật chạm tới**, không đo ở tầng gần mình nhất.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Hai bản module, hai trạng thái khác nhau
|
|
13
|
+
|
|
14
|
+
**Triệu chứng:** bot trả lời "Zalo chưa kết nối, chạy `npm start`…" trong khi
|
|
15
|
+
log vừa in `sidecar ready — logged in as …`.
|
|
16
|
+
|
|
17
|
+
Hermes nạp plugin dưới namespace riêng `hermes_plugins.<slug>`. Nếu adapter viết
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from plugins.zalo_tools.tools import set_active_adapter # ✗
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
thì Python dựng ra một đối tượng module **thứ hai**, mang `_ACTIVE_ADAPTER` riêng:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
adapter ──gắn cầu vào──> plugins.zalo_tools.tools _ACTIVE_ADAPTER = adapter
|
|
27
|
+
agent ──gọi công cụ──> hermes_plugins.zalo_tools.tools _ACTIVE_ADAPTER = None
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Hai bên không bao giờ gặp nhau. Cách đúng là phân giải lúc chạy (xem `_zalo_tools()`
|
|
31
|
+
trong `zalo/adapter.py`) và **ghi tên module đã chọn vào log**:
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
[zalo] connected to sidecar at ws://… (công cụ: hermes_plugins.zalo_tools.tools)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> **Áp dụng rộng:** bất kỳ trạng thái nào chia sẻ giữa hai plugin đều dính bẫy này.
|
|
38
|
+
> Hằng số là chuỗi thì trùng lặp vô hại — chỉ **trạng thái thay đổi được** mới bắt
|
|
39
|
+
> buộc dùng chung một bản.
|
|
40
|
+
|
|
41
|
+
## 2. Plugin `kind: platform` nạp lười
|
|
42
|
+
|
|
43
|
+
Hermes hoãn import mọi platform plugin cho tới khi gateway thật sự chạm tới nền
|
|
44
|
+
tảng đó. Công cụ đăng ký ở đấy vào registry **muộn hơn** lúc Hermes chốt danh sách
|
|
45
|
+
toolset, nên bị coi là tên lạ và loại sạch — không một dòng cảnh báo.
|
|
46
|
+
|
|
47
|
+
**Quy tắc:** công cụ luôn nằm ở plugin `kind: standalone` riêng. Platform plugin
|
|
48
|
+
chỉ giữ adapter.
|
|
49
|
+
|
|
50
|
+
## 3. Hermes tự bật mọi toolset plugin nó chưa từng thấy
|
|
51
|
+
|
|
52
|
+
Đây là bẫy nguy hiểm nhất, vì nó **âm thầm vô hiệu hoá phân quyền**. Toolset mới
|
|
53
|
+
mặc định BẬT cho mọi phiên cho tới khi được khai là "đã biết":
|
|
54
|
+
|
|
55
|
+
```yaml
|
|
56
|
+
known_plugin_toolsets:
|
|
57
|
+
zalo:
|
|
58
|
+
- zalo_owner
|
|
59
|
+
- zalo_public
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Thiếu dòng này thì `zalo_owner` được cấp cho cả người lạ nhắn vào nhóm, dù adapter
|
|
63
|
+
đã giới hạn qua `toolsets_for_source()`.
|
|
64
|
+
|
|
65
|
+
## 4. Đừng đặt tên toolset trùng khoá nền tảng
|
|
66
|
+
|
|
67
|
+
Toolset tên `zalo` bị Hermes tự bật cho mọi phiên vì trùng khoá platform. Đổi
|
|
68
|
+
thành `zalo_owner` mới chặn được. (Đổi tên là cần, nhưng chưa đủ — vẫn phải làm
|
|
69
|
+
mục 3.)
|
|
70
|
+
|
|
71
|
+
## 5. Phân toolset chỉ là *giấu*, không phải *chặn*
|
|
72
|
+
|
|
73
|
+
Mục 3 và 4 cho thấy danh sách toolset có thể bị hệ thống can thiệp sau lưng mình.
|
|
74
|
+
Nên rào chắn thật phải nằm ở **tầng thực thi**: mỗi công cụ nhóm chủ nhân được
|
|
75
|
+
bọc một lớp kiểm tra danh tính người gửi (`_owner_only` trong `tools.py`). Dù công
|
|
76
|
+
cụ có lọt vào danh sách vì cấu hình sai, người ngoài gọi vẫn bị từ chối.
|
|
77
|
+
|
|
78
|
+
## 6. Composite `hermes-<platform>` tự sinh kéo theo cả kanban
|
|
79
|
+
|
|
80
|
+
Bẫy này ảnh hưởng **mọi** plugin platform, không riêng Zalo.
|
|
81
|
+
|
|
82
|
+
`hermes-zalo` không có trong `TOOLSETS`. Khi thiếu, `resolve_toolset()` tự sinh nó
|
|
83
|
+
bằng `_HERMES_CORE_TOOLS` — và bộ lõi ấy chứa sẵn 14 công cụ `kanban_*`. Khối
|
|
84
|
+
"recover non-configurable toolsets" trong `tools_config.py` thấy
|
|
85
|
+
`kanban ⊆ universe` nên bật kanban cho **mọi** người nhắn vào nền tảng, đi vòng
|
|
86
|
+
qua `toolsets_for_source()`. Không cấu hình nào cản được:
|
|
87
|
+
`known_builtin_toolsets` không ăn thua (kanban không phải "recently shipped"),
|
|
88
|
+
còn `agent.disabled_toolsets` thì áp dụng toàn cục cho mọi nền tảng.
|
|
89
|
+
|
|
90
|
+
Cách xử lý ở đây (`define_platform_composite()` trong `tools.py`): **định nghĩa
|
|
91
|
+
tường minh** `hermes-zalo` để nhánh tự sinh không chạy nữa, lấy
|
|
92
|
+
`_HERMES_CORE_TOOLS` làm gốc (để bám theo Hermes khi nâng cấp) và trừ đi đúng
|
|
93
|
+
phần kanban. Chủ nhân vẫn dùng kanban qua Zalo được vì adapter liệt kê thẳng
|
|
94
|
+
`kanban` trong override dành riêng cho họ.
|
|
95
|
+
|
|
96
|
+
> Nhớ dọn `toolsets._resolve_toolset_memo` sau khi định nghĩa: bộ đệm khoá theo
|
|
97
|
+
> registry chứ không theo `TOOLSETS`, nên định nghĩa đến muộn có thể bị kết quả
|
|
98
|
+
> đã đệm che mất.
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Ghép API zca-js: đọc `.d.ts`, đừng tin trí nhớ
|
|
103
|
+
|
|
104
|
+
Tên trường giữa các API **không** thống nhất. Đầu ra của API này thường không
|
|
105
|
+
vừa đầu vào của API kia.
|
|
106
|
+
|
|
107
|
+
Ví dụ đã trả giá — `zalo_send_sticker` im lặng thất bại suốt vì:
|
|
108
|
+
|
|
109
|
+
| | Hình dạng |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `searchSticker` **trả về** | `{sticker_id, cate_id, type}` — snake_case |
|
|
112
|
+
| `sendSticker` **đòi** | `{id, cateId, type}` — camelCase, tên khác |
|
|
113
|
+
|
|
114
|
+
Truyền thẳng object sang thì `id`/`cateId` thành `undefined`, Zalo trả
|
|
115
|
+
`"Missing sticker id"`, còn agent thì thử vài lượt rồi tự chế lại câu trả lời
|
|
116
|
+
bằng emoji chữ — nhìn từ ngoài y như bot "không thích" gửi sticker.
|
|
117
|
+
|
|
118
|
+
Cũng vậy, **số lượng tham số** phải khớp. `getGroupChatHistory(groupId, count?)`
|
|
119
|
+
chỉ nhận hai; gọi ba tham số thì `null` rơi vào chỗ `count` và số tin yêu cầu bị
|
|
120
|
+
bỏ qua trong im lặng.
|
|
121
|
+
|
|
122
|
+
**Quy trình bắt buộc trước khi thêm một công cụ gọi API mới:**
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
cat node_modules/zca-js/dist/apis/<tenApi>.d.ts
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Đọc đúng ba thứ: **thứ tự tham số**, **số lượng tham số**, **tên trường** của
|
|
129
|
+
object. Rồi gọi thử thật qua cầu nối trước khi viết công cụ Python.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## Kiểm chứng: đo đúng tầng
|
|
134
|
+
|
|
135
|
+
Ba lần trong quá trình làm, phép thử báo xanh trong khi hệ thống thật vẫn hỏng —
|
|
136
|
+
đều vì đo ở tầng thấp hơn tầng người dùng chạm tới:
|
|
137
|
+
|
|
138
|
+
| Đã đo | Bỏ qua mất | Hệ quả |
|
|
139
|
+
|---|---|---|
|
|
140
|
+
| `resolve_toolset()` trực tiếp | `_get_platform_tools()` | tưởng agent có công cụ, thực ra không |
|
|
141
|
+
| script Node gọi thẳng bridge | toàn bộ tầng Python | sticker "gửi được" nhưng bot vẫn không gửi được |
|
|
142
|
+
| số toolset trả về | công cụ có gọi nổi không | tưởng phân quyền xong, thực ra công cụ chết |
|
|
143
|
+
|
|
144
|
+
**Thứ tự kiểm chứng đúng, từ yếu tới mạnh:**
|
|
145
|
+
|
|
146
|
+
1. Đọc `.d.ts` — biết hình dạng đúng
|
|
147
|
+
2. Gọi thật qua cầu nối — biết API sống
|
|
148
|
+
3. Gọi qua handler Python với `set_turn_context` — biết phân quyền và ngữ cảnh đúng
|
|
149
|
+
4. **Tag bot trong nhóm thật** — cái duy nhất chứng minh cả chuỗi thông
|
|
150
|
+
|
|
151
|
+
Chỉ bước 4 mới là bằng chứng. Ba bước trên chỉ giúp thu hẹp chỗ hỏng.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## Log lúc đăng ký plugin KHÔNG tới được tệp
|
|
156
|
+
|
|
157
|
+
Plugin nạp trước khi handler ghi log gắn vào, nên mọi `logger.info` trong
|
|
158
|
+
`register()` biến mất — kể cả khi đăng ký thành công. Đừng dùng nó để xác minh.
|
|
159
|
+
|
|
160
|
+
Chỗ đo được thật là **trong adapter lúc `connect()`**: logger ở đó đã hoạt động,
|
|
161
|
+
và nó nằm trong đúng tiến trình gateway đang chạy. Xem
|
|
162
|
+
`_log_permission_selfcheck()` — mỗi lần khởi động ghi đúng hai dòng:
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
[zalo] tự kiểm quyền — chủ nhân: 92 công cụ (39 Zalo), nhạy cảm: [...]
|
|
166
|
+
[zalo] tự kiểm quyền — người trong nhóm: 13 công cụ (13 Zalo), không có công cụ nhạy cảm
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Nếu dòng thứ hai có bất kỳ công cụ nhạy cảm nào, phân quyền đã hỏng — biết ngay
|
|
170
|
+
lúc khởi động thay vì đợi ai đó phát hiện trong nhóm.
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Trạng thái từng công cụ (kiểm chứng 2026-09-06)
|
|
176
|
+
|
|
177
|
+
Đã gọi thật qua cầu nối, không suy từ tài liệu. Bot lúc kiểm là **phó nhóm** ở
|
|
178
|
+
nhóm thử.
|
|
179
|
+
|
|
180
|
+
**Chạy được — 31 công cụ.** Toàn bộ nhóm đọc dữ liệu, gửi nội dung (văn bản,
|
|
181
|
+
sticker, tệp, liên kết, thoại, chuyển tiếp), bình chọn, lời nhắc, đổi tên nhóm,
|
|
182
|
+
link nhóm, tắt thông báo, ghim, hồ sơ bot, sổ người quen, kho tài liệu, đọc
|
|
183
|
+
trang web.
|
|
184
|
+
|
|
185
|
+
**Hỏng hoặc không ổn định — 3 công cụ:**
|
|
186
|
+
|
|
187
|
+
| Công cụ | Triệu chứng | Nguyên nhân |
|
|
188
|
+
|---|---|---|
|
|
189
|
+
| `zalo_read_history` | HTTP 404 | Giới hạn zca-js 2.1.2, gọi đúng chữ ký vẫn hỏng |
|
|
190
|
+
| `zalo_undo` | Không dùng được | `sendMessage` chỉ trả `{msgId}`, còn `undo` đòi cả `cliMsgId` — không có đường lấy |
|
|
191
|
+
| `zalo_web_search` | **Chập chờn** — cùng lúc có truy vấn được, có truy vấn lỗi | Đang dùng chế độ không khoá của Exa. Đặt `EXA_API_KEY` cho ổn định |
|
|
192
|
+
|
|
193
|
+
`zalo_undo` sửa được: listener vẫn nhận lại tin bot tự gửi (kèm `cliMsgId`), nên
|
|
194
|
+
có thể đệm một bảng `msgId → cliMsgId` ngắn hạn rồi tra khi thu hồi.
|
|
195
|
+
|
|
196
|
+
**Chưa kiểm được — 6 công cụ.** Chúng để lại dấu vết vĩnh viễn hoặc tác động
|
|
197
|
+
tới người thật, nên không thử tự động: `zalo_create_note` (không có API xoá ghi
|
|
198
|
+
chú), `zalo_create_group`, `zalo_invite_to_groups`, `zalo_join_group_link`,
|
|
199
|
+
`zalo_group_member_change`, `zalo_group_deputy`, `zalo_review_member` (cần có
|
|
200
|
+
người đang chờ duyệt). Tham số của chúng đã đối chiếu với `.d.ts`, nhưng đối
|
|
201
|
+
chiếu không phải là bằng chứng.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Hai lỗi khả dụng đã sửa trong đợt này
|
|
206
|
+
|
|
207
|
+
**`zalo_list_groups` trả về vô dụng.** `getAllGroups` một mình chỉ cho
|
|
208
|
+
`{groupId: version}` — agent nhận một nắm số và không nói nổi cho người dùng
|
|
209
|
+
biết đó là nhóm nào. Nay gọi thêm `getGroupInfo` để trả tên, sĩ số và vai trò
|
|
210
|
+
của bot trong nhóm.
|
|
211
|
+
|
|
212
|
+
**Tạo được lời nhắc mà không xoá được.** Đặt nhầm giờ là lời nhắc nằm lại trong
|
|
213
|
+
nhóm vĩnh viễn, phải nhờ người vào Zalo xoá tay. Đã thêm `zalo_remove_reminder`.
|
|
214
|
+
|
|
215
|
+
Bài học chung: một công cụ "gọi không lỗi" chưa chắc dùng được. Phải nhìn vào
|
|
216
|
+
thứ nó trả về và hỏi *agent làm gì được với cái này*, và mỗi hành động tạo ra
|
|
217
|
+
thứ gì đó phải có đường dọn tương ứng.
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
## Một lỗ hổng phát hiện khi rà lại quyền trong nhóm
|
|
221
|
+
|
|
222
|
+
`zalo_send_file` là công cụ **công khai** và nó nhận đường dẫn tệp trên máy chủ.
|
|
223
|
+
Không giới hạn thư mục, nên bất kỳ ai trong nhóm chỉ cần nhờ *"gửi giúp mình
|
|
224
|
+
tệp `<hermes-home>/.env`"* là bot tải khoá API lên nhóm.
|
|
225
|
+
|
|
226
|
+
Việc lọc bí mật của Hermes không cứu được: nó soát **văn bản** đầu ra, còn đây
|
|
227
|
+
là tệp nhị phân đi thẳng lên máy chủ Zalo. Nhắc trong prompt cũng không phải là
|
|
228
|
+
ranh giới.
|
|
229
|
+
|
|
230
|
+
Đã vá: người ngoài chỉ gửi được tệp **nằm trong kho tài liệu** — đúng phạm vi
|
|
231
|
+
`zalo_kb_read` đã mở, không rộng thêm một tấc. Chủ nhân giữ nguyên quyền gửi tệp
|
|
232
|
+
bất kỳ.
|
|
233
|
+
|
|
234
|
+
Bài học rộng hơn: **mỗi công cụ công khai nhận đường dẫn, URL hay ID đều phải
|
|
235
|
+
được hỏi lại là "người ngoài truyền giá trị xấu nhất vào đây thì sao"**. Lần rà
|
|
236
|
+
đầu chỉ chia công cụ theo mức nguy hiểm mà quên soi từng tham số một.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Ba lỗi tìm ra khi chạy thật trong nhóm
|
|
241
|
+
|
|
242
|
+
Cả ba đều không lộ ra ở bất kỳ phép thử tầng dưới nào.
|
|
243
|
+
|
|
244
|
+
### `uploadAttachment` báo thành công nhưng không gửi gì
|
|
245
|
+
|
|
246
|
+
Agent nói *"em đã gửi đính kèm 2 file"*, lịch sử phiên xác nhận nó **đã gọi**
|
|
247
|
+
`zalo_send_file` hai lần và cả hai trả `success: true` — nhưng trong nhóm không
|
|
248
|
+
có tệp nào.
|
|
249
|
+
|
|
250
|
+
`uploadAttachment` chỉ đẩy tệp lên CDN của Zalo và trả về `fileUrl`/`fileId`.
|
|
251
|
+
Nó **không** đăng tệp thành tin nhắn. Muốn gửi thật phải dùng
|
|
252
|
+
`sendMessage({msg, attachments}, threadId, type)`.
|
|
253
|
+
|
|
254
|
+
Dấu hiệu phân biệt nằm ngay ở giá trị trả về:
|
|
255
|
+
|
|
256
|
+
| Cách gọi | Trả về | Thành tin nhắn? |
|
|
257
|
+
|---|---|---|
|
|
258
|
+
| `uploadAttachment` | `{fileUrl, fileId, totalSize…}` | ❌ |
|
|
259
|
+
| `sendMessage` + `attachments` | `{message:{msgId}, attachment:[{msgId}]}` | ✅ |
|
|
260
|
+
|
|
261
|
+
**Quy tắc rút ra: mọi API gửi nội dung phải trả về `msgId`. Không có `msgId`
|
|
262
|
+
thì chưa có tin nhắn nào cả, dù `success: true`.**
|
|
263
|
+
|
|
264
|
+
### Tin nhắn kèm link bị vứt trong im lặng
|
|
265
|
+
|
|
266
|
+
`msg.data.content` không phải lúc nào cũng là chuỗi. Dán một đường link, gửi
|
|
267
|
+
ảnh hay tệp thì Zalo đổi nó thành object `{title, description, href, thumb…}`.
|
|
268
|
+
Code cũ chỉ nhận chuỗi nên tin có link thành rỗng, và bị bỏ ngay ở dòng
|
|
269
|
+
`if not text: return` — **không một dòng log nào**, nên nhìn từ ngoài y như bot
|
|
270
|
+
cố tình phớt lờ.
|
|
271
|
+
|
|
272
|
+
Cách phát hiện: đối chiếu tin nhắn thấy trên điện thoại với log. Tin không kèm
|
|
273
|
+
link đều có log, tin kèm link không có dòng nào — chênh lệch đó chỉ ra chỗ hỏng.
|
|
274
|
+
|
|
275
|
+
### Tiến trình nội bộ nhảy vào nhóm
|
|
276
|
+
|
|
277
|
+
`⌛ Working — 3 min — iteration 3/500, zalo_kb_list` hiện giữa cuộc trò chuyện.
|
|
278
|
+
|
|
279
|
+
Hermes có mặc định hiển thị riêng cho từng nền tảng (`_PLATFORM_DEFAULTS`),
|
|
280
|
+
nhưng **plugin platform không có mặc định nào** nên rơi vào cấu hình toàn cục
|
|
281
|
+
vốn dành cho terminal. Nhóm chat giống kênh Slack chứ không giống terminal: mỗi
|
|
282
|
+
dòng là một tin vĩnh viễn ai cũng thấy, không sửa lại được.
|
|
283
|
+
|
|
284
|
+
Khai trong `config.yaml`:
|
|
285
|
+
|
|
286
|
+
```yaml
|
|
287
|
+
display:
|
|
288
|
+
platforms:
|
|
289
|
+
zalo:
|
|
290
|
+
tool_progress: "off"
|
|
291
|
+
long_running_notifications: false
|
|
292
|
+
busy_ack_detail: false
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
> Bất kỳ plugin platform nào cũng nên khai khối này ngay khi dựng, đừng đợi tới
|
|
296
|
+
> lúc tiến trình nội bộ rơi vào mặt khách hàng.
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Chậm ở đâu: đo, đừng đoán
|
|
301
|
+
|
|
302
|
+
Một lượt trả lời mất **320 giây** trong khi mô hình được quảng cáo là siêu
|
|
303
|
+
nhanh. Đo từng tầng thì thấy mô hình vô can:
|
|
304
|
+
|
|
305
|
+
| Tầng | Thời gian |
|
|
306
|
+
|---|---|
|
|
307
|
+
| Gọi mô hình (đo trực tiếp qua router) | 1,8 – 3,0s |
|
|
308
|
+
| Gọi mô hình kèm 14 lược đồ công cụ | 3,1 – 5,2s |
|
|
309
|
+
| Mọi công cụ khi ổ đĩa đang nóng | < 1s |
|
|
310
|
+
|
|
311
|
+
Mốc thời gian trong `state.db` chỉ đúng thủ phạm:
|
|
312
|
+
|
|
313
|
+
```
|
|
314
|
+
5.3s gọi tool_search
|
|
315
|
+
1.6s gọi zalo_kb_list
|
|
316
|
+
239.98s ← kết quả zalo_kb_list
|
|
317
|
+
0.7s ← lần gọi thứ hai, cùng công cụ
|
|
318
|
+
30.02s ← zalo_send_file
|
|
319
|
+
30.03s ← zalo_send_file
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Lần hai chỉ 0,7s. Đây là chênh lệch **nguội / nóng** của ổ mạng: kho tài liệu
|
|
323
|
+
nằm trên RaiDrive gắn Google Drive, và khi nguội thì duyệt 1180 thư mục hoặc
|
|
324
|
+
kéo một tệp về đều mất hàng chục giây tới vài phút.
|
|
325
|
+
|
|
326
|
+
Hai bản vá:
|
|
327
|
+
|
|
328
|
+
* **Đệm danh sách kho** (`_kb_listing`, TTL 300s) — đo được 4,6s lần đầu và
|
|
329
|
+
0,00s các lần sau. Đệm cả cây rồi lọc trong bộ nhớ, nên câu hỏi với từ khoá
|
|
330
|
+
khác cũng không phải duyệt lại.
|
|
331
|
+
* **Nới thời gian chờ riêng cho lệnh đọc tệp** (`SLOW_METHODS`, 150s). Hai lần
|
|
332
|
+
đo được 30,02s và 30,03s — sát ngưỡng `ACK_TIMEOUT_SECONDS = 30` tới mức chỉ
|
|
333
|
+
cần chậm thêm chút là hỏng. Nới riêng nhóm này chứ không nới tất cả: một lệnh
|
|
334
|
+
gửi chữ mà treo 2 phút thì nên báo hỏng sớm.
|
|
335
|
+
|
|
336
|
+
> Truy vết bằng mốc thời gian trong `state.db` hiệu quả hơn hẳn việc đoán, vì
|
|
337
|
+
> nó chỉ thẳng ra khoảng trống nằm ở đâu. Con số tròn (30,0 / 240,0) luôn đáng
|
|
338
|
+
> ngờ — hoặc là timeout, hoặc là một hằng số nào đó, hiếm khi là công việc thật.
|
|
339
|
+
|
|
340
|
+
## Công cụ web chập chờn vì không có khoá backend
|
|
341
|
+
|
|
342
|
+
`web_search` và `web_extract` khi chưa cấu hình khoá sẽ xoay vòng qua các dịch
|
|
343
|
+
vụ không khoá (Firecrawl, Keenable, Exa). Đo cùng một URL sáu lần: **3 lần hỏng,
|
|
344
|
+
3 lần được** — mỗi lần một backend khác nhau báo lỗi.
|
|
345
|
+
|
|
346
|
+
Đã thêm thử lại trong `_core` — che bớt triệu chứng, đưa tỉ lệ lên 4/5.
|
|
347
|
+
|
|
348
|
+
**Cách chữa thật là đặt khoá.** Sau khi cắm `EXA_API_KEY`, đo lại với đầu vào
|
|
349
|
+
khác nhau mỗi lần (để bộ đệm không che kết quả):
|
|
350
|
+
|
|
351
|
+
| | Chưa có khoá | Có khoá |
|
|
352
|
+
|---|---|---|
|
|
353
|
+
| `web_search` | chập chờn | **4/4**, mỗi lượt ~1–1,5s |
|
|
354
|
+
| `web_extract` | 3/6 | **5/6** |
|
|
355
|
+
|
|
356
|
+
Lần trượt duy nhất là `moet.gov.vn`, và Exa trả rõ `CRAWL_LIVECRAWL_TIMEOUT` —
|
|
357
|
+
trang đích không cho thu thập chứ không phải backend hỏng. Lượt đó tính phí $0.
|
|
358
|
+
|
|
359
|
+
Vì backend đã ổn định, số lần thử lại giảm từ 3 xuống 2: một trang thật sự
|
|
360
|
+
không đọc được thì thử lại chỉ tổ bắt người trong nhóm chờ thêm.
|
|
361
|
+
|
|
362
|
+
## Link Google Docs: `/edit` không phải là nội dung
|
|
363
|
+
|
|
364
|
+
Link `/edit` trả về khung ứng dụng JavaScript, nên bộ đọc trang nhận được một
|
|
365
|
+
trang gần như trống kèm nút đăng nhập — và rất dễ kết luận nhầm là *"tài liệu
|
|
366
|
+
không được chia sẻ"*. Tài liệu công khai vẫn đọc được bình thường qua đường
|
|
367
|
+
`/export`:
|
|
368
|
+
|
|
369
|
+
| Loại | Đường đọc được |
|
|
370
|
+
|---|---|
|
|
371
|
+
| Docs | `/document/d/<id>/export?format=txt` |
|
|
372
|
+
| Sheets | `/spreadsheets/d/<id>/export?format=csv` |
|
|
373
|
+
| Slides | `/presentation/d/<id>/export/txt` |
|
|
374
|
+
| Tệp Drive | `/uc?export=download&id=<id>` |
|
|
375
|
+
|
|
376
|
+
`_google_export_url()` đổi tự động. Đổi **sau** khi kiểm tra an toàn, không phải
|
|
377
|
+
trước — để phép kiểm luôn nhìn đúng địa chỉ người dùng đưa vào.
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Kho tài liệu trên Google Drive: 10% số tệp vô hình
|
|
382
|
+
|
|
383
|
+
Bot báo *"chưa có tờ trình cho năm học 26-27"* trong khi người dùng mở File
|
|
384
|
+
Explorer ra thì thấy rõ. Soát thẳng ổ đĩa mới ra:
|
|
385
|
+
|
|
386
|
+
```
|
|
387
|
+
ĐOÀN CNT 26-27\...\01. Đội Thanh niên Xung kích\ĐỘI TNXK - TỜ TRÌNH CÔNG NHẬN BLĐ KHOÁ MỚI.gdoc.URL
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Đuôi tệp là **`.gdoc.URL`**. Khi kho tài liệu là một ổ Google Drive gắn qua
|
|
391
|
+
RaiDrive (hoặc Drive for desktop), mọi tài liệu Google **gốc** — Docs, Sheets,
|
|
392
|
+
Slides — không hiện thành `.docx` mà thành một tệp lối tắt bé xíu:
|
|
393
|
+
|
|
394
|
+
```ini
|
|
395
|
+
[InternetShortcut]
|
|
396
|
+
URL=https://docs.google.com/document/d/<id>/edit?usp=drivesdk
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
Bộ lọc định dạng chỉ nhận `.docx`, `.pdf`, `.txt`… nên toàn bộ nhóm này bị bỏ
|
|
400
|
+
qua trong im lặng. Đếm trên kho thật: **553/5388 tệp (10,3%)** là `.url` —
|
|
401
|
+
trong đó có đúng tài liệu người dùng đang hỏi.
|
|
402
|
+
|
|
403
|
+
Bot không hề báo lỗi. Nó liệt kê những tệp nó *thấy được*, không tìm thấy thứ
|
|
404
|
+
cần, rồi kết luận là "chưa có" — nghe rất thuyết phục và hoàn toàn sai.
|
|
405
|
+
|
|
406
|
+
Bản vá: coi `.url` là một loại đọc được, đọc địa chỉ trong tệp rồi tải chính
|
|
407
|
+
tài liệu đó về qua đường `/export`. Kết quả: số tệp khớp "xung kích" tăng từ 3
|
|
408
|
+
lên 10, và tài liệu kia đọc ra đủ 922 ký tự.
|
|
409
|
+
|
|
410
|
+
> **Bắt buộc kiểm địa chỉ trước khi tải.** Một tệp `.url` là nội dung do người
|
|
411
|
+
> khác đặt vào kho. Thả vào một lối tắt trỏ tới `http://127.0.0.1/...` là có
|
|
412
|
+
> ngay đường vòng đọc dữ liệu nội bộ, đi qua lưng bộ chặn của `zalo_web_read`.
|
|
413
|
+
> Đã thử bốn dạng (loopback, metadata đám mây, `file://`, lối tắt rỗng) — chặn
|
|
414
|
+
> hết.
|
|
415
|
+
|
|
416
|
+
Bài học chung: **khi bot nói "không tìm thấy", hãy kiểm bộ lọc trước khi tin
|
|
417
|
+
nó.** Một câu trả lời tự tin dựa trên dữ liệu bị lọc mất còn nguy hiểm hơn một
|
|
418
|
+
lỗi rõ ràng, vì không ai nghĩ tới chuyện đi kiểm lại.
|
|
@@ -21,6 +21,6 @@ Tài liệu này xác định ranh giới vận hành, cơ chế bảo vệ và
|
|
|
21
21
|
|
|
22
22
|
## 3. Quản lý Độ dài & Rich Text Zalo
|
|
23
23
|
|
|
24
|
-
- Zalo giới hạn
|
|
25
|
-
- Khi gửi tin dài hoặc có định dạng,
|
|
26
|
-
- Định dạng
|
|
24
|
+
- Zalo giới hạn độ dài, số style và kích thước mã hóa của mỗi bong bóng chat.
|
|
25
|
+
- Khi gửi tin dài hoặc có định dạng, dùng `formatAndChunkZaloMarkdown`: tối đa 2.000 UTF-16 code units, 40 style và 3.000 byte mỗi bong bóng; không bỏ ký tự, không cắt đôi surrogate pair.
|
|
26
|
+
- Định dạng bằng Markdown: `**in đậm**`, `# Tiêu đề`, `[RED]...[/RED]`. Nếu provider trả mã lỗi dạng số cho styled payload, bridge retry plain text đúng một lần; lỗi mạng không retry để tránh gửi trùng.
|