create-openclaw-bot 5.8.2 → 5.8.4

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.
@@ -114,413 +114,326 @@ const workspaceRoot = /** @type {OpenClawWorkspaceRoot} */ (
114
114
  function buildHeartbeatDoc(options = {}) {
115
115
  const { isVi = true } = options;
116
116
  return isVi
117
- ? `# HEARTBEAT\n\n- Không có tác vụ tồn đọng mặc định.\n- Giữ nguyên danh tính, vai trò, và tính cách đã có trong IDENTITY.md, SOUL.md, AGENTS.md.\n- Không tự onboarding lại user.\n- Nếu user chỉ mở đầu ngắn như "alo", trả lời ngắn gọn, đúng vai trò hiện tại.\n- Trước MỖI phản hồi Telegram nhìn thấy bởi user, nếu turn hiện tại có reaction tool native thì ưu tiên dùng để thả \`👍\`, trừ khi \`👍\` đã có sẵn.\n- Nếu turn hiện tại không có reaction tool hoặc chat không hỗ trợ reaction thật, trả lời bình thường và không được nói là mình đã thả reaction.\n`
118
- : `# HEARTBEAT\n\n- No pending task by default.\n- Keep the identity, role, and personality already defined in IDENTITY.md, SOUL.md, and AGENTS.md.\n- Do not re-onboard the user.\n- If the user sends a short opener like "hi" or "alo", reply briefly and stay in character.\n- Before EVERY user-visible Telegram reply, if the current turn exposes a native reaction tool, use it to add \`👍\` first unless \`👍\` is already present.\n- If the reaction tool is unavailable or the chat does not support real reactions, reply normally and do not claim that you reacted.\n`;
117
+ ? `# HEARTBEAT\n\n- Không có tác vụ tồn đọng mặc định.\n- Giữ nguyên danh tính, vai trò, và tính cách đã có trong IDENTITY.md, SOUL.md, AGENTS.md.\n- Không tự onboarding lại user.\n- Nếu user chỉ mở đầu ngắn như "alo", trả lời ngắn gọn, đúng vai trò hiện tại.\n`
118
+ : `# HEARTBEAT\n\n- No pending task by default.\n- Keep the identity, role, and personality already defined in IDENTITY.md, SOUL.md, and AGENTS.md.\n- Do not re-onboard the user.\n- If the user sends a short opener like "hi" or "alo", reply briefly and stay in character.\n`;
119
119
  }
120
120
 
121
121
  function buildBootstrapDoc(options = {}) {
122
122
  const { isVi = true, botName = 'Bot' } = options;
123
123
  return isVi
124
- ? `# BOOTSTRAP\n\n## Vai trò ngay khi khởi động\n- Bạn là **${botName}**.\n- Danh tính, vai trò, và phong cách của bạn đã được định nghĩa sẵn trong **IDENTITY.md**, **SOUL.md**, và **AGENTS.md**.\n- Dùng các file đó làm nguồn sự thật cao nhất cho persona của bạn trong workspace này.\n\n## Bắt buộc\n- Không hỏi user đặt lại tên, vibe, persona, emoji ký tên, hay kiểu trợ lý cho chính bạn.\n- Không nói kiểu "mới tỉnh dậy", "vừa online", "đang chọn danh tính", hay mở màn onboarding tương tự.\n- Không cư xử như một bot mới tạo trống rỗng.\n- Khi user chỉ nhắn ngắn như "alo", hãy chào ngắn gọn và trả lời đúng vai trò hiện tại.\n\n## Telegram\n- Trước MỖI phản hồi Telegram nhìn thấy bởi user, nếu turn hiện tại có reaction tool/action native thì ưu tiên dùng nó để thả \`👍\`, trừ khi \`👍\` đã có sẵn.\n- Không được nói là mình đã thả reaction nếu tool chưa chạy được hoặc reaction không hiện thật trên chat.\n- Nếu chat đích không hỗ trợ reaction thật hoặc tool không được expose, vẫn trả lời bình thường và không tranh luận về giới hạn kỹ thuật.\n`
125
- : `# BOOTSTRAP\n\n## Startup role\n- You are **${botName}**.\n- Your identity, role, and style are already defined in **IDENTITY.md**, **SOUL.md**, and **AGENTS.md**.\n- Use those files as the highest-priority source of truth for your persona in this workspace.\n\n## Mandatory\n- Do not ask the user to redefine your name, vibe, persona, signature emoji, or assistant style.\n- Do not say you just woke up, just came online, are choosing your identity, or any similar onboarding line.\n- Do not behave like a blank freshly-created bot.\n- If the user only sends a short opener like "hi" or "alo", greet briefly and reply in your existing role.\n\n## Telegram\n- Before EVERY user-visible Telegram reply, if the current turn exposes a native reaction tool/action, use it to add \`👍\` first unless \`👍\` is already present.\n- Never claim that you reacted unless the tool actually ran and the reaction is real on the chat.\n- If the target chat does not support real reactions or the tool is not exposed, still reply normally and do not mention the technical limitation.\n`;
124
+ ? `# BOOTSTRAP\n\n## Vai trò ngay khi khởi động\n- Bạn là **${botName}**.\n- Danh tính, vai trò, và phong cách của bạn đã được định nghĩa sẵn trong **IDENTITY.md**, **SOUL.md**, và **AGENTS.md**.\n- Dùng các file đó làm nguồn sự thật cao nhất cho persona của bạn trong workspace này.\n\n## Bắt buộc\n- Không hỏi user đặt lại tên, vibe, persona, emoji ký tên, hay kiểu trợ lý cho chính bạn.\n- Không nói kiểu "mới tỉnh dậy", "vừa online", "đang chọn danh tính", hay mở màn onboarding tương tự.\n- Không cư xử như một bot mới tạo trống rỗng.\n- Khi user chỉ nhắn ngắn như "alo", hãy chào ngắn gọn và trả lời đúng vai trò hiện tại.\n\n## Telegram & Zalo\n- Trước MỖI phản hồi Telegram hoặc Zalo nhìn thấy bởi user, nếu turn hiện tại có reaction tool/action native thì ưu tiên dùng nó để thả \`👍\`, trừ khi \`👍\` đã có sẵn.\n- Không được nói là mình đã thả reaction nếu tool chưa chạy được hoặc reaction không hiện thật trên chat.\n- Nếu chat đích không hỗ trợ reaction thật hoặc tool không được expose, vẫn trả lời bình thường và không tranh luận về giới hạn kỹ thuật.\n`
125
+ : `# BOOTSTRAP\n\n## Startup role\n- You are **${botName}**.\n- Your identity, role, and style are already defined in **IDENTITY.md**, **SOUL.md**, and **AGENTS.md**.\n- Use those files as the highest-priority source of truth for your persona in this workspace.\n\n## Mandatory\n- Do not ask the user to redefine your name, vibe, persona, signature emoji, or assistant style.\n- Do not say you just woke up, just came online, are choosing your identity, or any similar onboarding line.\n- Do not behave like a blank freshly-created bot.\n- If the user only sends a short opener like "hi" or "alo", greet briefly and reply in your existing role.\n\n## Telegram & Zalo\n- Before EVERY user-visible Telegram or Zalo reply, if the current turn exposes a native reaction tool/action, use it to add \`👍\` first unless \`👍\` is already present.\n- Never claim that you reacted unless the tool actually ran and the reaction is real on the chat.\n- If the target chat does not support real reactions or the tool is not exposed, still reply normally and do not mention the technical limitation.\n`;
126
126
  }
127
127
 
128
- function buildSearchToolJs() {
129
- return `/**
130
- * search-tool.js Stealth search via Playwright Headless Chromium or CDP fallback
131
- * Zero tokens, no API keys, concurrent multi-engine scraping (Google + Bing + DuckDuckGo).
132
- * Usage: node search-tool.js "<query>" [limit]
133
- */
134
- let playwright;
135
- try {
136
- playwright = require('playwright-core');
137
- } catch (e) {
138
- try {
139
- playwright = require('/usr/local/lib/node_modules/openclaw/node_modules/playwright-core');
140
- } catch (err) {
141
- try {
142
- const path = require('path');
143
- playwright = require(path.join(process.cwd(), 'node_modules', 'playwright-core'));
144
- } catch (x) {
145
- console.error(JSON.stringify({ error: 'Playwright not found! Install it or run within OpenClaw environment.' }));
146
- process.exit(1);
147
- }
128
+ function buildCronjobSkillMd(isVi = true) {
129
+ if (isVi) {
130
+ return `# Cron / Lên lịch nhắc nhở (tool: \`cron\`)
131
+ - **Tên tool chính xác:** Tên công cụ \`cron\` (tuyệt đối không nhầm là \`native\` hay command line bên ngoài).
132
+ - **⛔ TUYỆT ĐỐI KHÔNG sửa trực tiếp file JSON** như \`jobs.json\`, \`jobs-state.json\` trong thư mục \`.openclaw/cron/\`. Dữ liệu cron được lưu trong SQLite database, file JSON chỉ là legacy format đã ngưng hỗ trợ. Mọi thao tác PHẢI thông qua tool \`cron\`.
133
+ - **Khi tạo cronjob mới (action \`add\`):**
134
+ - **TUYỆT ĐỐI KHÔNG điền trường \`agentId\`** trong object \`job\` (hãy bỏ qua/omitted trường này). Hệ thống OpenClaw sẽ tự động gán chính xác ID của bạn vào job đó.
135
+ - Tuyệt đối **không tự điền** \`agentId\` là \`"bot"\` hay \`"main"\`, vì làm vậy sẽ khiến cronjob thuộc về agent khác và bạn sẽ mất quyền kiểm soát/xóa nó sau này.
136
+ - **Session:** Luôn dùng \`sessionTarget: "isolated"\` cho các job chạy nền (báo cáo, nhắc nhở, gửi tin nhắn tự động). Chỉ dùng \`"main"\` cho system event/reminder ngắn.
137
+ - **Timezone:** Luôn chỉ định timezone rõ ràng bằng trường \`tz\` (ví dụ: \`"Asia/Ho_Chi_Minh"\`). Nếu không chỉ định, hệ thống sẽ dùng timezone của Gateway host (thường là UTC) và job sẽ chạy sai giờ.
138
+ - **Delivery:** Đối với job cần gửi kết quả ra chat, set \`delivery.mode: "announce"\` kèm \`delivery.channel\` và \`delivery.to\`.
139
+ - **Khi user yêu cầu tắt/bật/xóa cronjob:**
140
+ 1. **Bước 1 (Tìm kiếm):** Gọi tool \`cron\` với action \`list\` (và \`includeDisabled: true\`) để xem danh sách tất cả cronjob đang chạy trên hệ thống và tìm đúng \`jobId\` phù hợp với yêu cầu.
141
+ 2. **Bước 2 (Xử lý):**
142
+ - Để xóa: Gọi action \`remove\` với \`id\` tìm được.
143
+ - Để tắt/tạm dừng: Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
144
+ - Để bật lại: Gọi action \`update\` với \`id\` và patch \`{"enabled": true}\`.
145
+ 3. **Tuyên bố trung thực:** Tuyệt đối không bao giờ trả lời "đã xóa" hay "không có" dựa trên suy đoán của bản thân mà chưa gọi tool \`cron\` để kiểm tra thực tế.
146
+ - Khi user yêu cầu tạo nhắc nhở / lệnh tự động định kỳ, bạn hãy TỰ ĐỘNG dùng tool \`cron\` (action \`add\`) để tạo. **Tuyệt đối không** bắt user dùng crontab hay Task Scheduler chạy tay trên host.
147
+ - Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.
148
+ - **QUAN TRỌNG VỀ TARGETING GROUP CHAT**: Khi tạo hoặc cấu hình cron job gửi tin nhắn thông báo (announce mode) đến một Group Chat, giá trị của trường \`delivery.to\` **bắt buộc** phải sử dụng tiền tố thích hợp trước ID của group. Với kênh Telegram/Matrix/Discord/Slack, dùng tiền tố \`group:\` (ví dụ: \`group:123456\`). RIÊNG với kênh Zalo (\`zalouser\`), **bắt buộc** phải sử dụng tiền tố \`g:\` (ví dụ: \`g:3815464776067464419\`) để tránh bị OpenClaw core lược bỏ tiền tố và gửi nhầm vào DM chat cá nhân.
149
+ - **One-shot job:** Dùng schedule kind \`"at"\` với ISO 8601 timestamp. Job sẽ tự xóa sau khi chạy thành công trừ khi set \`deleteAfterRun: false\`.
150
+ - Bỏ qua việc tra cứu docs nội bộ như \`cron-jobs.mdx\`; tin tưởng khả năng dùng tool hiện có để hoàn thành yêu cầu.`;
148
151
  }
149
- }
150
- const { chromium } = playwright;
152
+ return `# ⏰ Cron / Scheduled Tasks (tool: \`cron\`)
153
+ - **Exact tool name:** The tool name is \`cron\` (never mistake it for \`native\` or external command lines).
154
+ - **⛔ NEVER edit JSON files directly** such as \`jobs.json\` or \`jobs-state.json\` in \`.openclaw/cron/\`. Cron data is stored in SQLite database; JSON files are legacy format no longer supported. All operations MUST go through the \`cron\` tool.
155
+ - **When creating a new cronjob (action \`add\`):**
156
+ - **ABSOLUTELY DO NOT specify the \`agentId\` field** in the \`job\` object (leave this field omitted). The OpenClaw system will automatically assign your correct agent ID to that job.
157
+ - Never manually specify \`agentId\` as \`"bot"\` or \`"main"\`, as this will cause the cronjob to belong to another agent and you will lose control to manage/delete it later.
158
+ - **Session:** Always use \`sessionTarget: "isolated"\` for background jobs (reports, reminders, automated messages). Only use \`"main"\` for short system events/reminders.
159
+ - **Timezone:** Always specify timezone explicitly via the \`tz\` field (e.g., \`"Asia/Ho_Chi_Minh"\`). If omitted, the system uses the Gateway host timezone (often UTC) and the job will run at the wrong time.
160
+ - **Delivery:** For jobs that should send results to chat, set \`delivery.mode: "announce"\` with \`delivery.channel\` and \`delivery.to\`.
161
+ - **When the user requests to disable/enable/delete a cronjob:**
162
+ 1. **Step 1 (Search):** Call the \`cron\` tool with action \`list\` (and \`includeDisabled: true\`) to view all cron jobs on the system and find the matching \`jobId\`.
163
+ 2. **Step 2 (Processing):**
164
+ - To delete: Call action \`remove\` with the \`id\` found.
165
+ - To disable/pause: Call action \`update\` with \`id\` and patch \`{"enabled": false}\`.
166
+ - To enable: Call action \`update\` with \`id\` and patch \`{"enabled": true}\`.
167
+ 3. **Honest statement:** Never claim a job is "deleted" or "not found" based on guessing without calling the \`cron\` tool to verify the actual state.
168
+ - When the user asks to schedule tasks or reminders, use the built-in \`cron\` tool (action \`add\`) automatically. Do NOT ask users to run crontab or Task Scheduler manually on the host.
169
+ - When operating cron/scheduler tools, do **not** put \`current\` into the Session directory.
170
+ - **IMPORTANT ABOUT GROUP CHAT TARGETING**: When creating or configuring a cron job to send messages (announce mode) to a Group Chat, the value of the \`delivery.to\` field **must** use the appropriate prefix before the group ID. For Telegram/Matrix/Discord/Slack, use the \`group:\` prefix (e.g., \`group:123456\`). ESPECIALLY for Zalo (\`zalouser\`), you **must** use the \`g:\` prefix (e.g., \`g:3815464776067464419\`) to prevent the OpenClaw core from stripping the prefix and misrouting the message to a private DM.
171
+ - **One-shot jobs:** Use schedule kind \`"at"\` with an ISO 8601 timestamp. The job auto-deletes after successful run unless \`deleteAfterRun: false\` is set.
172
+ - Skip internal doc lookups such as \`cron-jobs.mdx\`; rely on the available tools and complete the scheduling task directly.`;
173
+ }
174
+
175
+ function buildInfographicGeneratorSkillMd() {
176
+ return `---
177
+ name: infographic-generator
178
+ description: Tạo ảnh infographic, banner hoặc poster trực tiếp bằng 1 prompt gửi tới API tạo ảnh.
179
+ ---
180
+
181
+ Khi người dùng yêu cầu tạo ảnh infographic, tin tức, cẩm nang, hoặc poster bằng tiếng Việt, hãy sử dụng skill này để gọi trực tiếp API tạo ảnh qua script \`image-generator.js\`. Phương pháp này tạo ra các tác phẩm thiết kế đồng nhất và tuyệt đẹp chỉ bằng một câu prompt chi tiết duy nhất.
151
182
 
152
- const query = process.argv[2];
153
- const limit = parseInt(process.argv[3]) || 5;
154
- const CDP_URL = 'http://127.0.0.1:9222';
183
+ ## 🚀 1. LỆNH THỰC THI
155
184
 
156
- if (!query) {
157
- console.error(JSON.stringify({ error: 'Usage: node search-tool.js "<query>" [limit]' }));
185
+ Để tạo ảnh, hãy gọi tool \`exec\` để chạy lệnh:
186
+ \`node skills/infographic-generator/image-generator.js "<prompt chi tiết bằng tiếng Anh>" <tên_ảnh>.png\`
187
+
188
+ _(Ví dụ: \`node skills/infographic-generator/image-generator.js "..." output.png\`)_
189
+
190
+ ---
191
+
192
+ ## 📐 2. QUY ĐỊNH KÍCH THƯỚC & TỶ LỆ (ASPECT RATIO)
193
+
194
+ Khi gọi API, mặc định kích thước là tỷ lệ **1:1** (hình vuông). Tuy nhiên, hãy tùy biến linh hoạt theo yêu cầu của người dùng bằng cách điều chỉnh từ khóa mô tả tỷ lệ và khung hình trong prompt:
195
+
196
+ - **Mặc định (1:1)**: Thêm từ khóa \`square aspect ratio, 1:1 square canvas\` vào prompt. Phù hợp cho các infographic dạng ô lưới hoặc bài đăng mạng xã hội thông thường.
197
+ - **Poster dọc (Vertical Poster)**: Thêm từ khóa \`vertical poster aspect ratio, 2:3 portrait format, vertical infographic\` vào prompt. Phù hợp cho cẩm nang chi tiết có nhiều mục (3-9 mục).
198
+ - **Landscape (16:9)**: Thêm từ khóa \`16:9 landscape aspect ratio, wide horizontal banner\` vào prompt. Phù hợp cho banner nằm ngang, ảnh bìa.
199
+
200
+ ---
201
+
202
+ ## ✍️ 3. QUY ĐỊNH FOOTER BẮT BUỘC
203
+
204
+ Mọi ảnh infographic/poster được tạo ra bằng skill này bắt buộc phải có dòng chữ bản quyền nằm ở cạnh dưới, canh giữa:
205
+
206
+ - **Nội dung chữ bắt buộc**: \`"designed by Williams - trợ lý của tuanminhhole"\`
207
+ - **Cách mô tả trong prompt**: Thêm vào cuối prompt mô tả chi tiết:
208
+ _\`"At the bottom center of the image, there is a clean and tiny centered footer text that reads: 'designed by Williams - trợ lý của tuanminhhole'"\`_
209
+
210
+ ---
211
+
212
+ ## 🎨 4. BA PHONG CÁCH THIẾT KẾ CHỦ ĐẠO
213
+
214
+ Hãy chọn 1 trong 3 phong cách dưới đây tùy thuộc vào ngữ cảnh yêu cầu:
215
+
216
+ ### Phong cách 1: Tin tức báo chí / News Editorial
217
+
218
+ - **Đặc điểm**: Bố cục chuyên nghiệp, chia nhiều cột dọc/ngang (multi-column), sử dụng các đường kẻ mỏng hoặc nét đứt mảnh để phân chia các ô tin tức rõ ràng.
219
+ - **Phông chữ**: Font tiêu đề Serif (có chân) sang trọng, font nội dung Sans-serif (không chân) hiện đại.
220
+ - **Minh họa**: Icon dạng vector phẳng (flat vector icons), tối giản, chuyên nghiệp.
221
+ - **Từ khóa prompt gợi ý**: \`news editorial infographic style, newspaper grid layout, clear divider lines, minimal serif headers, flat vector icons, professional business theme, clean corporate colors.\`
222
+
223
+ ### Phong cách 2: Cẩm nang/Hướng dẫn chi tiết
224
+
225
+ - **Đặc điểm**: Bố cục lưới (ví dụ: 3x3 grid) gồm nhiều ô được đánh số thứ tự (1, 2, 3...). Mỗi ô có nền màu pastel nhẹ nhàng (như xanh lá nhạt, kem nhạt, vàng nhạt) với viền bo góc tròn mềm mại. Có hình mascot (như chú heo đất đeo kính, két sắt, nhân vật hoạt hình) xuất hiện làm điểm nhấn.
226
+ - **Phông chữ**: Font chữ tròn, thân thiện, rõ ràng.
227
+ - **Minh họa**: Icon hoạt hình 2D sống động, nhiều màu sắc.
228
+ - **Từ khóa prompt gợi ý**: \`detailed guide infographic poster, 3x3 numbered grid layout, rounded pastel cards, cute 2D cartoon mascot, playful vector icons, warm cream background, clear numbered badges.\`
229
+
230
+ ### Phong cách 3: Layout Neo-Brutalism hoạt hình
231
+
232
+ - **Đặc điểm**: Đường viền đen dày nổi bật (thick dark borders), đổ bóng cứng màu đen (hard solid drop shadows), màu sắc tương phản mạnh mẽ (Neo-Brutalism), phong cách hoạt hình 2D phẳng, hiện đại và trẻ trung.
233
+ - **Phông chữ**: Font chữ in đậm, cá tính và không chân.
234
+ - **Minh họa**: Mascot và các icon phẳng nét vẽ dày cá tính.
235
+ - **Từ khóa prompt gợi ý**: \`neo-brutalism infographic poster, vector cartoon flat 2D style, thick dark solid borders, hard black drop shadows, bright vibrant background cards (yellow, cyan, lime green, orange), playful modern bold typography.\`
236
+
237
+ ---
238
+
239
+ ## 🔤 5. QUY TẮC PHÒNG TRÁNH LỖI FONT TIẾNG VIỆT
240
+
241
+ Mô hình Gemini 3.1 Flash Image hỗ trợ ghi text tiếng Việt cực tốt, nhưng để tránh việc AI tự động dùng các font chữ lạ bị lỗi hiển thị dấu tiếng Việt (như phác, ngã, hỏi bị lệch phông), hãy áp dụng nghiêm ngặt các quy tắc sau:
242
+
243
+ 1. **Chỉ định phông chữ tiêu chuẩn**: Trong prompt, ghi rõ tên các font chữ phổ biến hỗ trợ Unicode tiếng Việt tốt như: **Arial, Inter, Montserrat, Roboto, Plus Jakarta Sans, Fredoka** (chỉ dùng cho phong cách hoạt hình).
244
+ _Ví dụ: "in clean bold Arial font", "using modern Montserrat typeface"._
245
+ 2. **Tránh phông chữ lạ**: Tuyệt đối **KHÔNG** sử dụng các từ khóa như \`decorative, script, handwritten, gothic, calligraphy, futuristic fonts\` vì chúng hầu như không hỗ trợ tiếng Việt và sẽ tạo ra chữ lỗi phông rất xấu.
246
+ 3. **Định dạng Text rõ ràng**: Đặt toàn bộ các đoạn text tiếng Việt cần hiển thị trong dấu nháy đơn hoặc nháy kép để mô hình nhận diện chính xác phần văn bản cần viết.
247
+ _Ví dụ: \`At the top, the main title in bold Arial font reads: 'BÍ KÍP TRÁNH NÓNG MÙA HÈ'\`._
248
+
249
+ ---
250
+
251
+ ## 📝 6. MẪU PROMPT CHUNG CHO BOT LLM (TÙY CHỈNH THEO YÊU CẦU)
252
+
253
+ Mẫu prompt này được đúc kết từ các prompt tiêu chuẩn giúp mô hình tạo ảnh hoạt động tối ưu nhất. Bot LLM sẽ tự động tùy biến các phần nằm trong dấu ngoặc vuông \`[...]\` dựa trên tiêu đề, nội dung, số lượng bố cục và màu sắc phù hợp với chủ đề của người dùng, trong khi các phần còn lại được giữ nguyên cố định (bao gồm phong cách vẽ và footer bản quyền).
254
+
255
+ ### A. Công thức Prompt Tiếng Anh (Khuyên Dùng cho API)
256
+
257
+ \`\`\`text
258
+ An infographic poster with [Tỷ lệ khung hình] and [Loại nền].
259
+ Art style is modern illustration style mixed with hand-drawn elements.
260
+ At the top, the main title in clean bold [Tên Font tiếng Việt chuẩn] reads: '[TIÊU ĐỀ TIẾNG VIỆT LỚN]'.
261
+ The layout is divided into [Số lượng] cards or sections [Bố cục chia ô từ trên xuống dưới / Bố cục ô lưới / Quy trình cách thức].
262
+ The background and accent colors of the cards are [Màu sắc hài hòa tương ứng phù hợp với chủ đề].
263
+ Each card contains a clean flat vector illustration representing [Mô tả ngắn gọn hình vẽ minh họa] and a clear text label in bold [Tên Font tiếng Việt chuẩn] reads: '[NHÃN TIẾNG VIỆT CHO TỪNG Ô]'.
264
+ The text throughout the image must be clean, legible, and easy to read.
265
+ At the bottom center of the image, there is a clean and tiny centered footer text that reads: 'designed by Williams - trợ lý của tuanminhhole'.
266
+ High-resolution, high quality, professional infographic poster, no spelling mistakes.
267
+ \`\`\`
268
+
269
+ ### B. Công thức Prompt Tiếng Việt (Phong cách gốc giống Ảnh mẫu)
270
+
271
+ \`\`\`text
272
+ Infographic [Khung hình/tỷ lệ], nền [Loại nền].
273
+ Phong cách minh họa hiện đại pha hand-drawn.
274
+ Tiêu đề lớn '[TIÊU ĐỀ TIẾNG VIỆT LỚN]'.
275
+ Bố cục chia [Số lượng] ô rõ ràng [từ trên xuống dưới / dạng lưới / quy trình cách thức].
276
+ Màu sắc hài hòa [Mô tả tông màu phù hợp].
277
+ Mỗi ô vẽ minh họa vector phẳng [Mô tả ngắn hình ảnh cần vẽ cho ô] và nhãn chữ '[NHÃN TIẾNG VIỆT]'.
278
+ Chữ rõ ràng, dễ đọc, không sai chính tả.
279
+ Cạnh dưới canh giữa có chữ nhỏ: 'designed by Williams - trợ lý của tuanminhhole'.
280
+ Ảnh chất lượng cao, sắc nét.
281
+ \`\`\`
282
+ `;
283
+ }
284
+
285
+ function buildInfographicGeneratorJs() {
286
+ return `const fs = require('fs');
287
+ const path = require('path');
288
+
289
+ const prompt = process.argv[2];
290
+ const outputPath = process.argv[3] || 'image.png';
291
+
292
+ if (!prompt) {
293
+ console.error('Usage: node image-generator.js "<prompt>" [output_path]');
158
294
  process.exit(1);
159
295
  }
160
296
 
161
- (async () => {
162
- let browser;
163
- let ctx;
164
- let isStandalone = false;
297
+ // Find openclaw.json path dynamically by walking up
298
+ let openclawJsonPath = '';
299
+ let currentDir = process.cwd();
300
+ for (let i = 0; i < 5; i++) {
301
+ const candidate = path.join(currentDir, 'openclaw.json');
302
+ if (fs.existsSync(candidate)) {
303
+ openclawJsonPath = candidate;
304
+ break;
305
+ }
306
+ const candidateInDot = path.join(currentDir, '.openclaw', 'openclaw.json');
307
+ if (fs.existsSync(candidateInDot)) {
308
+ openclawJsonPath = candidateInDot;
309
+ break;
310
+ }
311
+ const parent = path.dirname(currentDir);
312
+ if (parent === currentDir) break;
313
+ currentDir = parent;
314
+ }
315
+
316
+ // Resolve API Key and Base URL from openclaw.json
317
+ let apiKey = 'sk-50599bc9642941c0-obzd49-1940044a'; // default fallback key
318
+ let baseUrl = 'http://9router:20128/v1'; // default fallback URL
319
+ if (openclawJsonPath) {
165
320
  try {
166
- // Try connecting to active Chrome CDP first
167
- try {
168
- browser = await chromium.connectOverCDP(CDP_URL, { timeout: 3000 });
169
- ctx = browser.contexts()[0];
170
- } catch (e) {
171
- // Fallback to standalone headless Chromium launch
172
- browser = await chromium.launch({
173
- headless: true,
174
- args: [
175
- '--no-sandbox',
176
- '--disable-gpu',
177
- '--disable-dev-shm-usage',
178
- '--disable-blink-features=AutomationControlled'
179
- ]
180
- });
181
- isStandalone = true;
182
- ctx = await browser.newContext({
183
- userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36'
184
- });
321
+ const config = JSON.parse(fs.readFileSync(openclawJsonPath, 'utf8'));
322
+ const provider = config.models?.providers?.['9router'];
323
+ if (provider) {
324
+ if (provider.apiKey) apiKey = provider.apiKey;
325
+ if (provider.baseUrl) baseUrl = provider.baseUrl;
185
326
  }
327
+ } catch (e) {}
328
+ }
186
329
 
187
- // Run search queries concurrently on three search engines
188
- const [googleResults, bingResults, ddgResults] = await Promise.all([
189
- // Google
190
- (async () => {
191
- const page = await ctx.newPage();
192
- try {
193
- await page.goto('https://www.google.com/search?q=' + encodeURIComponent(query) + '&hl=vi', { waitUntil: 'domcontentloaded', timeout: 10000 });
194
- const res = await page.evaluate(() => {
195
- const list = [];
196
- const links = Array.from(document.querySelectorAll('a h3'));
197
- for (const head of links) {
198
- const a = head.closest('a');
199
- if (!a) continue;
200
- const url = a.href;
201
- const title = head.textContent || '';
202
- let snippet = '';
203
- let parent = a.parentElement;
204
- while (parent && parent.tagName !== 'DIV') {
205
- parent = parent.parentElement;
206
- }
207
- if (parent) {
208
- const descEl = parent.parentElement?.querySelector('.VwiC3b, .yHGvwa, div[style*="-webkit-line-clamp"]');
209
- if (descEl) {
210
- snippet = descEl.textContent || '';
211
- } else {
212
- const texts = Array.from(parent.parentElement?.querySelectorAll('div, span') || [])
213
- .map(el => el.textContent.trim())
214
- .filter(txt => txt.length > 30 && !txt.includes(title));
215
- if (texts.length > 0) snippet = texts[0];
216
- }
217
- }
218
- if (url && title) {
219
- list.push({ title, url, snippet });
220
- }
221
- }
222
- return list;
223
- });
224
- await page.close();
225
- return res;
226
- } catch (e) {
227
- if (page) await page.close();
228
- return [];
330
+ const modelPriorityPatterns = [
331
+ /recraft-?v3/i,
332
+ /flux-pro-?(v1\\.1-)?ultra/i,
333
+ /flux-kontext-max/i,
334
+ /flux-pro-?(v1\\.1)?/i,
335
+ /flux-kontext-pro/i,
336
+ /recraft-?v2/i,
337
+ /recraft/i,
338
+ /ideogram-?v2/i,
339
+ /ideogram/i,
340
+ /runway.*turbo/i,
341
+ /runway/i,
342
+ /flux-?(1-)?dev/i,
343
+ /dall-e-3/i,
344
+ /stable-image-ultra/i,
345
+ /sd3\\.5-large-turbo/i,
346
+ /sd3\\.5-large/i,
347
+ /stable-diffusion-v35/i,
348
+ /sd3\\.5/i,
349
+ /stable-image-core/i,
350
+ /stable-diffusion-3/i,
351
+ /sd3/i,
352
+ /sd3\\.5-medium/i,
353
+ /flux-?(1-)?schnell/i,
354
+ /grok/i,
355
+ /gpt/i,
356
+ /minimax/i,
357
+ /gemini-3\\.1/i,
358
+ /gemini-3/i,
359
+ /gemini-2\\.5/i,
360
+ /gemini/i,
361
+ /sdxl/i,
362
+ /stable-diffusion/i,
363
+ /sdwebui/i,
364
+ /comfyui/i,
365
+ ];
366
+
367
+ (async () => {
368
+ try {
369
+ // Query active image generation models to choose the best one
370
+ let selectedModel = '';
371
+ try {
372
+ const modelsResponse = await fetch(\`\${baseUrl}/models/image\`, {
373
+ headers: {
374
+ 'Authorization': \`Bearer \${apiKey}\`
229
375
  }
230
- })(),
231
-
232
- // Bing
233
- (async () => {
234
- const page = await ctx.newPage();
235
- try {
236
- await page.goto('https://www.bing.com/search?q=' + encodeURIComponent(query), { waitUntil: 'domcontentloaded', timeout: 10000 });
237
- const res = await page.evaluate(() => {
238
- const list = [];
239
- const items = document.querySelectorAll('li.b_algo');
240
- for (const item of items) {
241
- const titleEl = item.querySelector('h2 a');
242
- if (!titleEl) continue;
243
- const title = titleEl.textContent || '';
244
- const url = titleEl.href;
245
- let snippet = '';
246
- const snippetEl = item.querySelector('.b_caption p, .b_snippet, p');
247
- if (snippetEl) {
248
- snippet = snippetEl.textContent || '';
249
- }
250
- if (url && title) {
251
- list.push({ title, url, snippet });
252
- }
253
- }
254
- return list;
255
- });
256
- await page.close();
257
- return res;
258
- } catch (e) {
259
- if (page) await page.close();
260
- return [];
376
+ });
377
+ const modelsData = await modelsResponse.json();
378
+ if (modelsData && Array.isArray(modelsData.data) && modelsData.data.length > 0) {
379
+ const modelIds = modelsData.data.map(m => m.id);
380
+ for (const pattern of modelPriorityPatterns) {
381
+ const found = modelIds.find(id => pattern.test(id));
382
+ if (found) {
383
+ selectedModel = found;
384
+ break;
385
+ }
261
386
  }
262
- })(),
263
-
264
- // DuckDuckGo
265
- (async () => {
266
- const page = await ctx.newPage();
267
- try {
268
- await page.goto('https://html.duckduckgo.com/html/?q=' + encodeURIComponent(query), { waitUntil: 'domcontentloaded', timeout: 10000 });
269
- const res = await page.evaluate(() => {
270
- const list = [];
271
- const elements = document.querySelectorAll('.result');
272
- for (const el of elements) {
273
- const titleEl = el.querySelector('.result__title a');
274
- const snippetEl = el.querySelector('.result__snippet');
275
- if (titleEl) {
276
- list.push({
277
- title: titleEl.textContent.trim(),
278
- url: titleEl.href,
279
- snippet: snippetEl ? snippetEl.textContent.trim() : ''
280
- });
281
- }
282
- }
283
- return list;
284
- });
285
- await page.close();
286
- return res;
287
- } catch (e) {
288
- if (page) await page.close();
289
- return [];
387
+ if (!selectedModel) {
388
+ selectedModel = modelIds[0];
290
389
  }
291
- })()
292
- ]);
293
-
294
- // Deduplicate results by normalized URL
295
- const allResults = [...googleResults, ...bingResults, ...ddgResults];
296
- const uniqueResults = [];
297
- const seenUrls = new Set();
298
- for (const res of allResults) {
299
- if (!res.url || !res.title) continue;
300
- let normUrl = res.url.replace(/^(https?:\\/\\/)?(www\\.)?/, '').toLowerCase();
301
- if (normUrl.endsWith('/')) normUrl = normUrl.slice(0, -1);
302
- if (!seenUrls.has(normUrl)) {
303
- seenUrls.add(normUrl);
304
- uniqueResults.push(res);
305
390
  }
391
+ } catch (e) {
392
+ console.warn('[ImageGen] Failed to auto-resolve active models, using fallback:', e.message);
306
393
  }
307
394
 
308
- // Score results to prioritize numeric price data for financial queries
309
- const isPriceQuery = /giá|vàng|đô|usd|sjc|sh|hôm nay|price|gold|rate|vnd|xe|vnđ/i.test(query);
310
- const scoredResults = uniqueResults.map(res => {
311
- let score = 0;
312
- // Base length score
313
- score += Math.min(res.snippet.length / 50, 5);
314
-
315
- if (isPriceQuery) {
316
- // Number density check
317
- const numCount = (res.snippet.match(/\\d+/g) || []).length;
318
- score += Math.min(numCount * 2, 10);
319
-
320
- // Priority keywords boost
321
- if (/lượng|chỉ|triệu|nghìn|vnd|usd|sjc|xe|bán|mua|giá/i.test(res.snippet)) {
322
- score += 8;
323
- }
324
- }
325
- return { ...res, score };
326
- });
327
-
328
- // Sort by score desc
329
- scoredResults.sort((a, b) => b.score - a.score);
330
-
331
- // Map back to output format and limit
332
- const output = scoredResults.map(({ score, ...rest }) => rest).slice(0, limit);
333
- console.log(JSON.stringify(output, null, 2));
334
-
335
- } catch (err) {
336
- console.error(JSON.stringify({ error: err.message }));
337
- } finally {
338
- if (browser && isStandalone) {
339
- try {
340
- await browser.close();
341
- } catch(e) {}
395
+ if (!selectedModel) {
396
+ selectedModel = 'gemini/gemini-3.1-flash-image-preview'; // default fallback
342
397
  }
343
- }
344
- })();
345
- `;
346
- }
347
398
 
348
- function buildBrowserToolJs(variant = 'wizard') {
349
- // v2: Full-featured browser-tool.js matching OpenClaw native browser plugin capabilities
350
- // Both 'cli' and 'wizard' variants now use the same full script
351
- const playwrightRequire = variant === 'cli'
352
- ? "require('playwright')"
353
- : "require('/usr/local/lib/node_modules/openclaw/node_modules/playwright-core')";
354
-
355
- return `/**
356
- * browser-tool.js v2 — Full-featured Chrome CDP controller
357
- * Commands: open|get_url|get_text|get_links|get_posts|evaluate|console|screenshot|screenshot_full|pdf|click|fill|press|hover|select|upload|scroll|wait|resize|tabs|new_tab|switch_tab|close_tab|status
358
- */
359
- const { chromium } = ${playwrightRequire};
360
- const action = process.argv[2];
361
- const param1 = process.argv[3];
362
- const param2 = process.argv[4];
363
- const CDP_URL = 'http://127.0.0.1:9222';
364
- (async () => {
365
- let browser;
366
- try {
367
- browser = await chromium.connectOverCDP(CDP_URL, { timeout: 5000 });
368
- const ctx = browser.contexts()[0];
369
- const pages = ctx.pages();
370
- let page = pages.length > 0 ? pages[0] : await ctx.newPage();
371
- if (action === 'open') {
372
- console.log('[Browser] Opening: ' + param1);
373
- await page.goto(param1, { waitUntil: 'domcontentloaded', timeout: 30000 });
374
- await page.waitForTimeout(1500);
375
- console.log('[Browser] Opened: ' + (await page.title()) + ' | ' + page.url());
376
- } else if (action === 'get_url') {
377
- console.log(page.url());
378
- } else if (action === 'status') {
379
- const allPages = ctx.pages();
380
- console.log('[Browser] Connected! Tabs: ' + allPages.length);
381
- console.log('[Browser] Current: ' + (await page.title()) + ' | ' + page.url());
382
- } else if (action === 'get_text') {
383
- const maxLen = parseInt(param1) || 4000;
384
- const text = await page.evaluate(() => { document.querySelectorAll('script,style,noscript,svg').forEach(e => e.remove()); return document.body.innerText.trim(); });
385
- console.log(text.substring(0, maxLen));
386
- } else if (action === 'get_links') {
387
- const filter = param1 || '';
388
- const links = await page.evaluate((f) => { const a = Array.from(document.querySelectorAll('a[href]')).map(e => e.href).filter(h => h && h.startsWith('http')); return [...new Set(f ? a.filter(h => h.includes(f)) : a)]; }, filter);
389
- console.log(JSON.stringify(links.slice(0, 50), null, 2));
390
- } else if (action === 'get_posts') {
391
- const posts = await page.evaluate(() => {
392
- const results = [];
393
- const articles = document.querySelectorAll('[role="article"]');
394
- for (const article of articles) {
395
- const textEl = article.querySelector('[data-ad-comet-preview="message"],[data-ad-preview="message"]');
396
- const fullText = (textEl ? textEl.innerText.trim() : '') || article.innerText.substring(0, 800);
397
- const allLinks = Array.from(article.querySelectorAll('a[href]'));
398
- let permalink = '';
399
- for (const a of allLinks) { const h = a.href || ''; if (h.includes('/posts/') || h.includes('/permalink/') || h.includes('story_fbid')) { permalink = h.split('?')[0]; break; } }
400
- let author = '';
401
- for (const el of article.querySelectorAll('a[role="link"] strong, h2 a, h3 a, h4 a')) { const n = el.innerText.trim(); if (n && n.length > 1 && n.length < 50) { author = n; break; } }
402
- let timePosted = '';
403
- const timeLinks = allLinks.filter(a => { const h = a.href || ''; return h.includes('/posts/') || h.includes('/permalink/'); });
404
- if (timeLinks.length > 0) { const t = timeLinks[0].innerText.trim(); if (t && t.length < 30) timePosted = t; }
405
- if (!timePosted) { const te = article.querySelector('abbr,[data-utime]'); if (te) timePosted = te.innerText.trim() || te.getAttribute('title') || ''; }
406
- if (fullText.length > 20) results.push({ author: author || 'N/A', text: fullText.substring(0, 500), permalink: permalink || 'N/A', time: timePosted || 'N/A' });
407
- }
408
- return results;
409
- });
410
- console.log(posts.length === 0 ? '[Browser] No posts found. Try scroll then get_posts again.' : JSON.stringify(posts.slice(0, 10), null, 2));
411
- } else if (action === 'evaluate') {
412
- const code = process.argv.slice(3).join(' ');
413
- if (!code) { console.log('[Browser] Usage: evaluate <js_code>'); process.exit(1); }
414
- const result = await page.evaluate(code);
415
- console.log(result !== undefined && result !== null ? (typeof result === 'object' ? JSON.stringify(result, null, 2) : String(result)) : '[Browser] Done');
416
- } else if (action === 'console') {
417
- const msgs = []; page.on('console', m => msgs.push('[' + m.type() + '] ' + m.text()));
418
- await page.waitForTimeout(2000);
419
- console.log(msgs.length === 0 ? '[Browser] No console messages in 2s' : msgs.join('\\n'));
420
- } else if (action === 'screenshot') {
421
- const p = param1 || '/tmp/screenshot.png'; await page.screenshot({ path: p, fullPage: false }); console.log('[Browser] Screenshot: ' + p);
422
- } else if (action === 'screenshot_full') {
423
- const p = param1 || '/tmp/screenshot_full.png'; await page.screenshot({ path: p, fullPage: true }); console.log('[Browser] Full screenshot: ' + p);
424
- } else if (action === 'pdf') {
425
- const p = param1 || '/tmp/page.pdf'; await page.pdf({ path: p, format: 'A4' }); console.log('[Browser] PDF: ' + p);
426
- } else if (action === 'click') {
427
- await page.locator(param1).first().click({ timeout: 5000 }); await page.waitForTimeout(600); console.log('[Browser] Clicked: ' + param1);
428
- } else if (action === 'fill') {
429
- await page.locator(param1).first().fill(param2, { timeout: 5000 }); console.log('[Browser] Filled: ' + param1);
430
- } else if (action === 'press') {
431
- await page.keyboard.press(param1); await page.waitForTimeout(1000); console.log('[Browser] Pressed: ' + param1);
432
- } else if (action === 'hover') {
433
- await page.locator(param1).first().hover({ timeout: 5000 }); console.log('[Browser] Hovered: ' + param1);
434
- } else if (action === 'select') {
435
- await page.locator(param1).first().selectOption(param2, { timeout: 5000 }); console.log('[Browser] Selected: ' + param2);
436
- } else if (action === 'upload') {
437
- await page.locator(param1).first().setInputFiles(param2, { timeout: 5000 }); console.log('[Browser] Uploaded: ' + param2);
438
- } else if (action === 'scroll') {
439
- const px = parseInt(param1) || 800; await page.evaluate((p) => window.scrollBy(0, p), px); await page.waitForTimeout(2000); console.log('[Browser] Scrolled: ' + px + 'px');
440
- } else if (action === 'wait') {
441
- const ms = parseInt(param1) || 1000; await page.waitForTimeout(ms); console.log('[Browser] Waited: ' + ms + 'ms');
442
- } else if (action === 'resize') {
443
- const w = parseInt(param1) || 1280, h = parseInt(param2) || 720; await page.setViewportSize({ width: w, height: h }); console.log('[Browser] Resized: ' + w + 'x' + h);
444
- } else if (action === 'tabs') {
445
- const ap = ctx.pages(); for (let i = 0; i < ap.length; i++) { const t = await ap[i].title().catch(() => '(untitled)'); console.log('[' + i + '] ' + t + ' | ' + ap[i].url() + (ap[i] === page ? ' < current' : '')); }
446
- } else if (action === 'new_tab') {
447
- const np = await ctx.newPage(); if (param1) await np.goto(param1, { waitUntil: 'domcontentloaded', timeout: 30000 }); console.log('[Browser] New tab' + (param1 ? ': ' + param1 : ''));
448
- } else if (action === 'switch_tab') {
449
- const idx = parseInt(param1), ap = ctx.pages(); if (isNaN(idx) || idx < 0 || idx >= ap.length) { console.log('[Browser] Invalid index. Use tabs to list.'); } else { page = ap[idx]; await page.bringToFront(); console.log('[Browser] Switched to [' + idx + ']: ' + page.url()); }
450
- } else if (action === 'close_tab') {
451
- const ap = ctx.pages(), idx = param1 !== undefined ? parseInt(param1) : ap.indexOf(page); if (ap.length <= 1) { console.log('[Browser] Cannot close last tab.'); } else if (isNaN(idx) || idx < 0 || idx >= ap.length) { console.log('[Browser] Invalid index.'); } else { await ap[idx].close(); console.log('[Browser] Closed tab [' + idx + ']'); }
399
+ console.log(\`[ImageGen] Generating: "\${prompt}" using model "\${selectedModel}"...\`);
400
+ const response = await fetch(\`\${baseUrl}/images/generations\`, {
401
+ method: 'POST',
402
+ headers: {
403
+ 'Content-Type': 'application/json',
404
+ 'Authorization': \`Bearer \${apiKey}\`
405
+ },
406
+ body: JSON.stringify({
407
+ model: selectedModel,
408
+ prompt: prompt,
409
+ n: 1,
410
+ size: 'auto',
411
+ response_format: 'b64_json'
412
+ })
413
+ });
414
+ const data = await response.json();
415
+ if (data.error) {
416
+ console.error('[ImageGen] API Error:', data.error.message || data.error);
417
+ process.exit(1);
418
+ }
419
+ if (data.data && data.data[0] && data.data[0].b64_json) {
420
+ const buf = Buffer.from(data.data[0].b64_json, 'base64');
421
+ const absoluteOutputPath = path.isAbsolute(outputPath) ? outputPath : path.join(process.cwd(), outputPath);
422
+ fs.writeFileSync(absoluteOutputPath, buf);
423
+ console.log(\`[ImageGen] Saved image to: \${outputPath}\`);
452
424
  } else {
453
- console.log('browser-tool.js v2 Commands:');
454
- console.log(' Nav: open <url> | get_url | status');
455
- console.log(' Content: get_text [max] | get_links [filter] | get_posts | evaluate <js> | console');
456
- console.log(' Export: screenshot [path] | screenshot_full [path] | pdf [path]');
457
- console.log(' Interact: click <sel> | fill <sel> <txt> | press <key> | hover <sel> | select <sel> <val> | upload <sel> <path>');
458
- console.log(' View: scroll [px] | wait <ms> | resize <w> <h>');
459
- console.log(' Tabs: tabs | new_tab [url] | switch_tab <idx> | close_tab [idx]');
425
+ console.error('[ImageGen] No image data returned');
426
+ process.exit(1);
460
427
  }
461
- } catch(e) {
462
- if (e.message.includes('ECONNREFUSED') || e.message.includes('Timeout')) {
463
- console.error('[Browser] Chrome Debug not running! Start with --remote-debugging-port=9222');
464
- } else { console.error('[Browser] Error:', e.message); }
465
- } finally { if (browser) await browser.close(); }
428
+ } catch (e) {
429
+ console.error('[ImageGen] Fetch Error:', e.message);
430
+ process.exit(1);
431
+ }
466
432
  })();
467
433
  `;
468
434
  }
469
435
 
470
- function buildBrowserDoc(options = {}) {
471
- const { isVi = true, variant = 'wizard', workspaceRoot = '' } = options;
472
- const wsRoot = workspaceRoot.replace(/\/+$/, '');
473
- const btPath = wsRoot ? `${wsRoot}/browser-tool.js` : 'browser-tool.js';
474
- let modeHeading = '';
475
- if (variant === 'cli-server') {
476
- modeHeading = isVi
477
- ? `# 🌍 Trình duyệt ảo (Browser Automation)\n\n## 💡 Hướng dẫn vận hành:\n- **Script điều khiển:** \`browser-tool.js\` (Mọi câu lệnh browser đều chạy qua script này).\n- **Môi trường chạy:**\n - **Trên VPS / Linux Server (Headless):** Trình duyệt chạy ngầm hoàn toàn độc lập (Headless) bên trong Docker / Server qua Xvfb. Không thể mở màn hình Chrome thật.\n - **Trên Máy tính cá nhân (Windows/Mac) - Dù chạy Docker hay Native:**\n - **Mặc định:** Chạy ngầm (headless) cực kỳ ổn định.\n - **Chế độ quan sát (Xem bot click):** Nếu bạn muốn xem trực tiếp Chrome thật hoạt động trên màn hình, hãy chạy file \`start-chrome-debug.bat\` (trên Windows) hoặc \`start-chrome-debug.sh\` (trên Mac) ở máy của bạn **trước khi** bot kết nối! Bot sẽ tự động chuyển sang điều khiển màn hình Chrome thật của bạn.\n- **Kết nối mặc định:** \`http://127.0.0.1:9222\`\n\n`
478
- : `# 🌍 Browser Automation\n\n## 💡 Operating Guide:\n- **Control script:** \`browser-tool.js\` (All browser commands are executed through this script).\n- **Running environment:**\n - **On VPS / Linux Server (Headless Server Mode):** The browser runs fully headless and isolated inside Docker / Server via Xvfb. No GUI Chrome can be launched.\n - **On Personal Computers (Windows/Mac) - Docker or Native:**\n - **Default:** Runs headless and stable in the background.\n - **Observer Mode (Visual Chrome GUI):** If you want to see the real Chrome window being controlled, run \`start-chrome-debug.bat\` (on Windows) or \`start-chrome-debug.sh\` (on Mac) on your host machine **before** the bot connects! The bot will automatically hook into your real desktop Chrome.\n- **Default endpoint:** \`http://127.0.0.1:9222\`\n\n`;
479
- } else {
480
- modeHeading = isVi
481
- ? `# 🌍 Hướng dẫn Browser (Chrome CDP)\n- **Script điều khiển:** \`browser-tool.js\`\n- **Kết nối Chrome debug:** \`http://127.0.0.1:9222\`\n- **Xem trực quan:** Hãy chạy file \`start-chrome-debug.bat\` (trên Windows) hoặc \`start-chrome-debug.sh\` (trên Mac) để mở Chrome chế độ Debug.\n\n`
482
- : `# 🌍 Browser Guide (Chrome CDP)\n- **Control script:** \`browser-tool.js\`\n- **Chrome debug endpoint:** \`http://127.0.0.1:9222\`\n- **Visual interface:** Run \`start-chrome-debug.bat\` (on Windows) or \`start-chrome-debug.sh\` (on Mac) to open Chrome in Debug mode.\n\n`;
483
- }
484
436
 
485
- return `${modeHeading}# Navigation
486
- node ${btPath} status
487
- node ${btPath} open "https://google.com"
488
- node ${btPath} get_url
489
-
490
- # ⭐ Content extraction — LUÔN dùng get_posts cho Facebook
491
- node ${btPath} get_posts
492
- node ${btPath} get_text
493
- node ${btPath} get_text 8000
494
- node ${btPath} get_links
495
- node ${btPath} get_links "/posts/"
496
- node ${btPath} evaluate "document.title"
497
- node ${btPath} console
498
-
499
- # Screenshots & export
500
- node ${btPath} screenshot
501
- node ${btPath} screenshot_full
502
- node ${btPath} pdf
503
-
504
- # Interactions
505
- node ${btPath} click "button.submit"
506
- node ${btPath} fill "input[name='q']" "search"
507
- node ${btPath} press "Enter"
508
- node ${btPath} hover "a.link"
509
- node ${btPath} select "select#id" "value"
510
- node ${btPath} upload "input[type=file]" "/tmp/photo.jpg"
511
-
512
- # Scrolling & viewport
513
- node ${btPath} scroll
514
- node ${btPath} scroll 1500
515
- node ${btPath} wait 3000
516
- node ${btPath} resize 1920 1080
517
-
518
- # Tab management
519
- node ${btPath} tabs
520
- node ${btPath} new_tab "https://example.com"
521
- node ${btPath} switch_tab 1
522
- node ${btPath} close_tab 2`;
523
- }
524
437
 
525
438
  function buildSecurityRules(isVi = true) {
526
439
  if (isVi) {
@@ -580,101 +493,19 @@ node ${btPath} close_tab 2`;
580
493
  browserDocVariant = '',
581
494
  } = options;
582
495
 
583
- const skillsSection = skillListStr || (isVi ? '- _(Chưa có skill nào)_' : '- _(No skills installed)_');
584
-
585
- const browserRef = hasBrowser
586
- ? (browserDocVariant === 'cli-server'
587
- ? (isVi
588
- ? `\n\n## 🌐 Browser Automation
589
- - Xem hướng dẫn chi tiết tại **BROWSER.md**
590
- - Script điều khiển: \`browser-tool.js\`
591
- - Chế độ hiện tại:
592
- - **Trên VPS / Linux Server:** Chạy ngầm độc lập qua Docker hoặc Xvfb.
593
- - **Trên Windows/Mac (Docker hoặc Native):** Chạy ngầm mặc định, hoặc chạy file \`start-chrome-debug.bat\` / \`start-chrome-debug.sh\` để xem trình duyệt trực quan trên màn hình.
594
- - Kết nối mặc định: \`http://127.0.0.1:9222\`
595
- - **Tìm kiếm Web:** Nếu không có công cụ Web Search (hoặc Web Search không khả dụng/bị lỗi), hãy **luôn sử dụng ngay công cụ terminal (exec/run_command) để chạy lệnh: \`node search-tool.js "<từ khóa>" 5\`**! Lệnh này sẽ tự động chạy ngầm qua DuckDuckGo/Google/Bing bằng trình duyệt ngầm tàng hình của bạn và trả về kết quả JSON sạch ngay lập tức. Tuyệt đối KHÔNG được mở trình duyệt thủ công, chụp ảnh màn hình hay click tìm kiếm bằng tay từng bước!
596
- - Nếu browser lỗi, thử lại 1 lần rồi mới báo user với lỗi cụ thể`
597
- : `\n\n## 🌐 Browser Automation
598
- - See detailed guide at **BROWSER.md**
599
- - Control script: \`browser-tool.js\`
600
- - Current mode:
601
- - **On VPS / Linux Server:** Runs headless via Docker or Xvfb.
602
- - **On Windows/Mac (Docker or Native):** Runs headless by default, or run \`start-chrome-debug.bat\` / \`start-chrome-debug.sh\` to see the GUI.
603
- - Default endpoint: \`http://127.0.0.1:9222\`
604
- - **Web Searching:** If the Web Search tool is unavailable or fails, **always use your terminal execution tool (exec/run_command) to run: \`node search-tool.js "<query>" 5\`**! This will automatically execute the search via DuckDuckGo/Google/Bing under stealth browser mode and return a clean JSON result immediately. Never open the browser manually, take screenshots, or click the search button step-by-step!
605
- - If browser fails, retry once before reporting the concrete error to the user`)
606
- : (isVi
607
- ? `\n\n## 🌐 Browser Automation
608
- - Xem hướng dẫn chi tiết tại **BROWSER.md**
609
- - Script điều khiển: \`browser-tool.js\`
610
- - Kết nối Chrome debug: \`http://127.0.0.1:9222\`
611
- - **Tìm kiếm Web:** Hãy **luôn sử dụng ngay công cụ terminal (exec/run_command) để chạy lệnh: \`node search-tool.js "<từ khóa>" 5\`**! Lệnh này sẽ tự động chạy ngầm qua DuckDuckGo/Google/Bing bằng trình duyệt ngầm tàng hình của bạn và trả về kết quả JSON sạch ngay lập tức. Tuyệt đối KHÔNG được mở trình duyệt thủ công, chụp ảnh màn hình hay click tìm kiếm bằng tay từng bước!
612
- - Nếu tool/profile hỏi đường browser desktop, dùng profile \`host-chrome\` trước`
613
- : `\n\n## 🌐 Browser Automation
614
- - See detailed guide at **BROWSER.md**
615
- - Control script: \`browser-tool.js\`
616
- - Chrome debug endpoint: \`http://127.0.0.1:9222\`
617
- - **Web Searching:** If the Web Search tool is unavailable or fails, **always use your terminal execution tool (exec/run_command) to run: \`node search-tool.js "<query>" 5\`**! This will automatically execute the search via DuckDuckGo/Google/Bing under stealth browser mode and return a clean JSON result immediately. Never open the browser manually, take screenshots, or click the search button step-by-step!
618
- - If a desktop browser profile is needed, use the \`host-chrome\` profile first`))
619
- : '';
620
-
621
- const telegramSection = (variant === 'relay')
622
- ? (isVi
623
- ? `\n\n## Telegram\n- Đã bật \`reactionLevel:minimal\`, \`replyToMode:first\`, \`actions.sendMessage\`, và \`actions.reactions\`.\n- Trước MỖI phản hồi Telegram nhìn thấy bởi user, hãy dùng **tool/action native của OpenClaw** để thả \`👍\` trước nếu chat hiện tại hỗ trợ reaction và nếu \`👍\` chưa có sẵn.\n- Không được nói "mình vừa thả like rồi" nếu turn hiện tại chưa gọi được tool reaction hoặc reaction không hiện thật trên chat.\n- Không tự mô tả việc đi gọi Telegram Bot API thủ công.\n- Nếu tool reaction không khả dụng trong turn hiện tại hoặc chat đích không hỗ trợ reaction thật, tiếp tục trả lời bình thường thay vì tranh cãi về kỹ thuật nội bộ.\n- Khi nhận handoff từ bot khác: trả lời công khai bằng chính account Telegram của mình, ưu tiên dùng outbound Telegram action.`
624
- : `\n\n## Telegram\n- Configured with \`reactionLevel:minimal\`, \`replyToMode:first\`, \`actions.sendMessage\`, and \`actions.reactions\`.\n- Before EVERY user-visible Telegram reply, use the **native OpenClaw tool/action** to add \`👍\` first when the current chat supports reactions and \`👍\` is not already present.\n- Never claim "I already liked it" unless the current turn actually executed the reaction and the reaction is real on the target chat.\n- Do not describe or roleplay manual Telegram Bot API calls.\n- If the reaction tool is unavailable in the current turn or the target chat does not support real reactions, continue replying normally instead of discussing internal limitations.\n- When receiving a handoff from another bot: reply publicly from your own Telegram account, prefer outbound Telegram action.`)
625
- : '';
626
-
627
- const cronSection = hasScheduler
628
- ? (isVi
629
- ? `\n\n## \u23F0 Cron / Lên lịch nhắc nhở (tool: \`cron\`)
630
- - **Tên tool chính xác:** Tên công cụ là \`cron\` (tuyệt đối không nhầm là \`native\` hay command line bên ngoài).
631
- - **Khi tạo cronjob mới (action \`add\`):**
632
- - **TUYỆT ĐỐI KHÔNG điền trường \`agentId\`** trong object \`job\` (hãy bỏ qua/omitted trường này). Hệ thống OpenClaw sẽ tự động gán chính xác ID của bạn vào job đó.
633
- - Tuyệt đối **không tự điền** \`agentId\` là \`"bot"\` hay \`"main"\`, vì làm vậy sẽ khiến cronjob thuộc về agent khác và bạn sẽ mất quyền kiểm soát/xóa nó sau này.
634
- - **Khi user yêu cầu tắt/bật/xóa cronjob:**
635
- 1. **Bước 1 (Tìm kiếm):** Gọi tool \`cron\` với action \`list\` (và \`includeDisabled: true\`) để xem danh sách tất cả cronjob đang chạy trên hệ thống và tìm đúng \`jobId\` phù hợp với yêu cầu.
636
- 2. **Bước 2 (Xử lý):**
637
- - Để xóa: Gọi action \`remove\` với \`id\` tìm được.
638
- - Để tắt/tạm dừng: Gọi action \`update\` với \`id\` và patch \`{"enabled": false}\`.
639
- - Để bật lại: Gọi action \`update\` với \`id\` và patch \`{"enabled": true}\`.
640
- 3. **Tuyên bố trung thực:** Tuyệt đối không bao giờ trả lời "đã xóa" hay "không có" dựa trên suy đoán của bản thân mà chưa gọi tool \`cron\` để kiểm tra thực tế.
641
- - Khi user yêu cầu tạo nhắc nhở / lệnh tự động định kỳ, bạn hãy TỰ ĐỘNG dùng tool \`cron\` (action \`add\`) để tạo. **Tuyệt đối không** bắt user dùng crontab hay Task Scheduler chạy tay trên host.
642
- - Khi thao tác tool cho cron/scheduler, **không điền \`current\` vào thư mục Session**.
643
- - **QUAN TRỌNG VỀ TARGETING GROUP CHAT**: Khi tạo hoặc cấu hình cron job gửi tin nhắn thông báo (announce mode) đến một Group Chat, giá trị của trường \`delivery.to\` **bắt buộc** phải sử dụng tiền tố \`group:\` trước ID của group (ví dụ: \`group:3815464776067464419\` hoặc \`group:xxxx\`). Tuyệt đối không được chỉ điền ID thuần túy vì hệ thống sẽ hiểu nhầm đó là một DM chat cá nhân (direct message) và gửi sai địa chỉ.
644
- - Bỏ qua việc tra cứu docs nội bộ như \`cron-jobs.mdx\`; tin tưởng khả năng dùng tool hiện có để hoàn thành yêu cầu.`
645
- : `\n\n## \u23F0 Cron / Scheduled Tasks (tool: \`cron\`)
646
- - **Exact tool name:** The tool name is \`cron\` (never mistake it for \`native\` or external command lines).
647
- - **When creating a new cronjob (action \`add\`):**
648
- - **ABSOLUTELY DO NOT specify the \`agentId\` field** in the \`job\` object (leave this field omitted). The OpenClaw system will automatically assign your correct agent ID to that job.
649
- - Never manually specify \`agentId\` as \`"bot"\` or \`"main"\`, as this will cause the cronjob to belong to another agent and you will lose control to manage/delete it later.
650
- - **When the user requests to disable/enable/delete a cronjob:**
651
- 1. **Step 1 (Search):** Call the \`cron\` tool with action \`list\` (and \`includeDisabled: true\`) to view all cron jobs on the system and find the matching \`jobId\`.
652
- 2. **Step 2 (Processing):**
653
- - To delete: Call action \`remove\` with the \`id\` found.
654
- - To disable/pause: Call action \`update\` with \`id\` and patch \`{"enabled": false}\`.
655
- - To enable: Call action \`update\` with \`id\` and patch \`{"enabled": true}\`.
656
- 3. **Honest statement:** Never claim a job is "deleted" or "not found" based on guessing without calling the \`cron\` tool to verify the actual state.
657
- - When the user asks to schedule tasks or reminders, use the built-in \`cron\` tool (action \`add\`) automatically. Do NOT ask users to run crontab or Task Scheduler manually on the host.
658
- - When operating cron/scheduler tools, do **not** put \`current\` into the Session directory.
659
- - **IMPORTANT ABOUT GROUP CHAT TARGETING**: When creating or configuring a cron job to send messages (announce mode) to a Group Chat, the value of the \`delivery.to\` field **must** use the \`group:\` prefix before the group ID (e.g., \`group:3815464776067464419\` or \`group:xxxx\`). Never specify just the numeric ID, as the system will interpret it as a private DM and deliver to the wrong destination.
660
- - Skip internal doc lookups such as \`cron-jobs.mdx\`; rely on the available tools and complete the scheduling task directly.`)
661
- : '';
662
-
663
- const zaloModSection = '';
664
-
665
496
  const dmOverride = isVi
666
- ? `\n\n## Override: DM và Telegram reaction\n- Nếu metadata không nói rõ đây là group/supergroup, mặc định xem là chat riêng/DM và trả lời bình thường.\n- Quy tắc im lặng khi không ai được gọi chỉ áp dụng cho group chat, không áp dụng cho DM/chat riêng.\n- Trước MỖI phản hồi Telegram nhìn thấy bởi user, nếu turn hiện tại có tool/action reaction native thì ưu tiên dùng nó để thả \`👍\` trước, trừ khi \`👍\` đã có sẵn.\n- Nếu turn hiện tại không expose tool reaction hoặc chat đích không hỗ trợ reaction thật, cứ trả lời đúng vai trò và không được nói là mình đã thả reaction.`
667
- : `\n\n## Override: DM and Telegram reaction\n- If metadata does not clearly say this is a group/supergroup, treat it as a private DM and reply normally.\n- The stay-silent rule for unaddressed messages applies only to group chats, never to DMs.\n- Before EVERY user-visible Telegram reply, if the current turn exposes a native reaction tool/action, use it to add \`👍\` first unless \`👍\` is already present.\n- If the reaction tool is unavailable or the target chat does not support real reactions, just reply in-character and do not claim that you reacted.`;
497
+ ? `\n\n## Override: DM và Telegram/Zalo reaction\n- Nếu metadata không nói rõ đây là group/supergroup, mặc định xem là chat riêng/DM và trả lời bình thường.\n- Quy tắc im lặng khi không ai được gọi chỉ áp dụng cho group chat, không áp dụng cho DM/chat riêng.\n- Trước MỖI phản hồi Telegram hoặc Zalo nhìn thấy bởi user, nếu turn hiện tại có tool/action reaction native thì ưu tiên dùng nó để thả \`👍\` trước, trừ khi \`👍\` đã có sẵn.\n- Nếu turn hiện tại không expose tool reaction hoặc chat đích không hỗ trợ reaction thật, cứ trả lời đúng vai trò và không được nói là mình đã thả reaction.`
498
+ : `\n\n## Override: DM and Telegram/Zalo reaction\n- If metadata does not clearly say this is a group/supergroup, treat it as a private DM and reply normally.\n- The stay-silent rule for unaddressed messages applies only to group chats, never to DMs.\n- Before EVERY user-visible Telegram or Zalo reply, if the current turn exposes a native reaction tool/action, use it to add \`👍\` first unless \`👍\` is already present.\n- If the reaction tool is unavailable or the target chat does not support real reactions, just reply in-character and do not claim that you reacted.`;
668
499
 
669
500
  if (variant === 'relay') {
670
501
  return isVi
671
- ? `# Hướng dẫn dùng tool\n\n## Tools sẵn\n${skillsSection}\n\n## Quy tắc chung\n- Tóm tắt kết quả tool thay vì dump raw output.\n- Mọi bot đều có quyền sử dụng tất cả tool (scheduler, browser, exec). Vai trò (dev/marketing/...) chỉ là persona, KHÔNG giới hạn quyền dùng tool.\n- Workspace của bạn là \`.openclaw/${agentWorkspaceDir}/\`.${browserRef}${telegramSection}${cronSection}${zaloModSection}${dmOverride}\n`
672
- : `# Tool Usage Guide\n\n## Available Tools\n${skillsSection}\n\n## General Rules\n- Summarize tool output instead of dumping raw output.\n- All bots have equal access to all tools (scheduler, browser, exec). Roles (dev/marketing/...) are persona only, NOT tool permissions.\n- Your workspace is \`.openclaw/${agentWorkspaceDir}/\`.${browserRef}${telegramSection}${cronSection}${zaloModSection}${dmOverride}\n`;
502
+ ? `# Hướng dẫn dùng tool\n\n## Nguyên tắc chung\n- Ưu tiên dùng tool/skill phù hợp thay vì tự suy đoán\n- Nếu tool trả về lỗi — thử lại 1 lần, sau đó báo user\n- Không chạy tool liên tục mà không có mục đích rõ ràng\n- Luôn tóm tắt kết quả tool cho user thay vì dump raw output.\n- Mọi bot đều có quyền sử dụng tất cả tool (scheduler, browser, exec). Vai trò (dev/marketing/...) chỉ là persona, KHÔNG giới hạn quyền dùng tool.\n- Workspace của bạn là \`.openclaw/${agentWorkspaceDir}/\`.\n\n## 📁 Kỹ năng (Skills)\n- Xem chi tiết hướng dẫn các kỹ năng được cài đặt tại thư mục [skills](./skills/).\n${dmOverride}\n`
503
+ : `# Tool Usage Guide\n\n## General Rules\n- Summarize tool output instead of dumping raw output.\n- All bots have equal access to all tools (scheduler, browser, exec). Roles (dev/marketing/...) are persona only, NOT tool permissions.\n- Your workspace is \`.openclaw/${agentWorkspaceDir}/\`.\n\n## 📁 Skills\n- See detailed guidelines of installed skills in the [skills](./skills/) directory.\n${dmOverride}\n`;
673
504
  }
674
505
 
675
506
  return isVi
676
- ? `# Hướng dẫn sử dụng Tools\n\n## Danh sách skills đã cài\n${skillsSection}\n\n## Nguyên tắc chung\n- Ưu tiên dùng tool/skill phù hợp thay vì tự suy đoán\n- Nếu tool trả về lỗi — thử lại 1 lần, sau đó báo user\n- Không chạy tool liên tục mà không có mục đích rõ ràng\n- Luôn tóm tắt kết quả tool cho user thay vì dump raw output${browserRef}\n\n## Quy ước\n- Web Search: TUYỆT ĐỐI KHÔNG dùng tool \`web_search\` native đã bị hiệu hóa hoàn toàn trên hệ thống. Thay vào đó, mỗi khi cần tìm kiếm Internet/Web, bạn BẮT BUỘC phải gọi công cụ \`exec\` để chạy lệnh terminal: \`node search-tool.js "<từ khóa>" 5\`! Lệnh này cực kỳ nhanh, tàng hình và miễn phí.\n- Browser: chỉ mở trang khi user yêu cầu cụ thể\n- Memory: tự ghi nhớ thông tin tự nhiên, không cần user nhắc${cronSection}${zaloModSection}\n\n## \uD83D\uDCC1 File & Workspace\n- Bot có thể đọc/ghi file trong thư mục workspace: \`${workspacePath}\`\n- Dùng để lưu notes, scripts, cấu hình tạm\n\n## \u26A0\uFE0F Tool Error Handling\n- Retry tối đa 2 lần nếu tool lỗi network\n- Nếu vẫn lỗi: báo user kèm mô tả lỗi cụ thể và gợi ý workaround${dmOverride}\n`
677
- : `# Tool Usage Guide\n\n## Installed Skills\n${skillsSection}\n\n## General Principles\n- Prefer using the right tool/skill over guessing\n- If a tool returns an error — retry once, then report to user\n- Don't run tools repeatedly without a clear purpose\n- Always summarize tool output for user instead of dumping raw data${browserRef}\n\n## Conventions\n- Web Search: DO NOT use the native \`web_search\` tool as it is completely disabled. Instead, whenever you need to search the Internet/Web, you MUST call the \`exec\` tool to run terminal command: \`node search-tool.js "<query>" 5\`! This is extremely fast, stealthy and free.\n- Browser: only open pages when user specifically requests\n- Memory: proactively remember important info without user prompting${cronSection}${zaloModSection}\n\n## \uD83D\uDCC1 File & Workspace\n- Bot can read/write files in workspace: \`${workspacePath}\`\n\n## \u26A0\uFE0F Tool Error Handling\n- Retry up to 2 times on network errors\n- If still failing: report to user with specific error description and workaround${dmOverride}\n`;
507
+ ? `# Hướng dẫn sử dụng Tools\n\n## Nguyên tắc chung\n- Ưu tiên dùng tool/skill phù hợp thay vì tự suy đoán\n- Nếu tool trả về lỗi — thử lại 1 lần, sau đó báo user\n- Không chạy tool liên tục mà không có mục đích rõ ràng\n- Luôn tóm tắt kết quả tool cho user thay vì dump raw output\n\n## 📁 Kỹ năng (Skills)\n- Xem chi tiết hướng dẫn các kỹ năng được cài đặt tại thư mục [skills](./skills/).\n\n## 📁 File & Workspace\n- Bot có thể đọc/ghi file trong thư mục workspace: \`${workspacePath}\`\n- Dùng để lưu notes, scripts, cấu hình tạm\n\n## ⚠️ Tool Error Handling\n- Retry tối đa 2 lần nếu tool lỗi network\n- Nếu vẫn lỗi: báo user kèm mô tả lỗi cụ thể và gợi ý workaround${dmOverride}\n`
508
+ : `# Tool Usage Guide\n\n## General Principles\n- Prefer using the right tool/skill over guessing\n- If a tool returns an error — retry once, then report to user\n- Don't run tools repeatedly without a clear purpose\n- Always summarize tool output for user instead of dumping raw data\n\n## 📁 Skills\n- See detailed guidelines of installed skills in the [skills](./skills/) directory.\n\n## 📁 File & Workspace\n- Bot can read/write files in workspace: \`${workspacePath}\`\n\n## ⚠️ Tool Error Handling\n- Retry up to 2 times on network errors\n- If still failing: report to user with specific error description and workaround${dmOverride}\n`;
678
509
  }
679
510
  function buildTeamsDoc(options = {}) {
680
511
  const {
@@ -715,6 +546,7 @@ node ${btPath} close_tab 2`;
715
546
  * @property {string} [teamRosterFormatted]
716
547
  * @property {string} [emoji]
717
548
  * @property {boolean} [hasScheduler]
549
+ * @property {boolean} [hasImageGen]
718
550
  * @property {boolean} [hasZaloMod]
719
551
  */
720
552
 
@@ -749,6 +581,7 @@ node ${btPath} close_tab 2`;
749
581
  teamRosterFormatted = '',
750
582
  emoji = '',
751
583
  hasScheduler = false,
584
+ hasImageGen = false,
752
585
  hasZaloMod = false,
753
586
  } = opts;
754
587
 
@@ -769,20 +602,19 @@ node ${btPath} close_tab 2`;
769
602
  'HEARTBEAT.md': buildHeartbeatDoc({ isVi }),
770
603
  'BOOTSTRAP.md': buildBootstrapDoc({ isVi, botName }),
771
604
  'DREAMS.md': buildDreamsDoc({ isVi }),
772
- 'search-tool.js': buildSearchToolJs(),
773
605
  };
774
606
 
775
607
  if (isMultiBot) {
776
608
  files['TEAMS.md'] = buildTeamsDoc({ isVi, teamRosterFormatted, otherAgents });
777
609
  }
778
610
 
779
- if (hasBrowser) {
780
- const toolVariant = browserToolVariant || (soulVariant === 'wizard' ? 'wizard' : 'cli');
781
- const docVariant = browserDocVariant || (soulVariant === 'wizard' ? 'wizard' : 'cli-desktop');
782
- if (includeBrowserTool) {
783
- files['browser-tool.js'] = buildBrowserToolJs(toolVariant);
784
- }
785
- files['BROWSER.md'] = buildBrowserDoc({ isVi, variant: docVariant, workspaceRoot: workspacePath });
611
+ if (hasScheduler) {
612
+ files['skills/cronjob/SKILL.md'] = buildCronjobSkillMd(isVi);
613
+ }
614
+
615
+ if (hasImageGen) {
616
+ files['skills/infographic-generator/SKILL.md'] = buildInfographicGeneratorSkillMd();
617
+ files['skills/infographic-generator/image-generator.js'] = buildInfographicGeneratorJs();
786
618
  }
787
619
 
788
620
  return files;
@@ -797,13 +629,13 @@ node ${btPath} close_tab 2`;
797
629
  buildDreamsDoc,
798
630
  buildHeartbeatDoc,
799
631
  buildBootstrapDoc,
800
- buildSearchToolJs,
801
- buildBrowserToolJs,
802
- buildBrowserDoc,
803
632
  buildSecurityRules,
804
633
  buildAgentsDoc,
805
634
  buildToolsDoc,
806
635
  buildTeamsDoc,
636
+ buildCronjobSkillMd,
637
+ buildInfographicGeneratorSkillMd,
638
+ buildInfographicGeneratorJs,
807
639
  buildWorkspaceFileMap,
808
640
  };
809
641