create-openclaw-bot 5.8.16 → 5.8.18
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 +3 -3
- package/README.vi.md +2 -2
- package/dist/server/local-server.js +237 -107
- package/dist/setup/data/header.js +1 -1
- package/dist/setup/shared/docker-gen.js +7 -0
- package/dist/setup/shared/workspace-gen.js +16 -821
- package/dist/web/app.js +30 -9
- package/package.json +1 -1
|
@@ -387,827 +387,22 @@ description: Schedule recurring tasks using the cron tool.
|
|
|
387
387
|
- Skip internal doc lookups such as \`cron-jobs.mdx\`; rely on the available tools and complete the scheduling task directly.`;
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
function buildInfographicGeneratorSkillMd(botName = 'Williams') {
|
|
391
|
-
return
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
## 📐 2. QUY ĐỊNH KÍCH THƯỚC & TỶ LỆ (ASPECT RATIO)
|
|
408
|
-
|
|
409
|
-
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:
|
|
410
|
-
|
|
411
|
-
- **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.
|
|
412
|
-
- **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).
|
|
413
|
-
- **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.
|
|
414
|
-
|
|
415
|
-
---
|
|
416
|
-
|
|
417
|
-
## ✍️ 3. QUY ĐỊNH FOOTER BẮT BUỘC
|
|
418
|
-
|
|
419
|
-
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:
|
|
420
|
-
|
|
421
|
-
- **Nội dung chữ bắt buộc**: \`"designed by ${botName} - trợ lý của tuanminhhole"\`
|
|
422
|
-
- **Cách mô tả trong prompt**: Thêm vào cuối prompt mô tả chi tiết:
|
|
423
|
-
_\`"At the bottom center of the image, there is a clean and tiny centered footer text that reads: 'designed by ${botName} - trợ lý của tuanminhhole'"\`_
|
|
424
|
-
|
|
425
|
-
---
|
|
426
|
-
|
|
427
|
-
## 🎨 4. BA PHONG CÁCH THIẾT KẾ CHỦ ĐẠO
|
|
428
|
-
|
|
429
|
-
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:
|
|
430
|
-
|
|
431
|
-
### Phong cách 1: Tin tức báo chí / News Editorial
|
|
432
|
-
|
|
433
|
-
- **Đặ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.
|
|
434
|
-
- **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.
|
|
435
|
-
- **Minh họa**: Icon dạng vector phẳng (flat vector icons), tối giản, chuyên nghiệp.
|
|
436
|
-
- **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.\`
|
|
437
|
-
|
|
438
|
-
### Phong cách 2: Cẩm nang/Hướng dẫn chi tiết
|
|
439
|
-
|
|
440
|
-
- **Đặ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.
|
|
441
|
-
- **Phông chữ**: Font chữ tròn, thân thiện, rõ ràng.
|
|
442
|
-
- **Minh họa**: Icon hoạt hình 2D sống động, nhiều màu sắc.
|
|
443
|
-
- **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.\`
|
|
444
|
-
|
|
445
|
-
### Phong cách 3: Layout Neo-Brutalism hoạt hình
|
|
446
|
-
|
|
447
|
-
- **Đặ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.
|
|
448
|
-
- **Phông chữ**: Font chữ in đậm, cá tính và không chân.
|
|
449
|
-
- **Minh họa**: Mascot và các icon phẳng nét vẽ dày cá tính.
|
|
450
|
-
- **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.\`
|
|
451
|
-
|
|
452
|
-
---
|
|
453
|
-
|
|
454
|
-
## 🔤 5. QUY TẮC PHÒNG TRÁNH LỖI FONT TIẾNG VIỆT
|
|
455
|
-
|
|
456
|
-
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:
|
|
457
|
-
|
|
458
|
-
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).
|
|
459
|
-
_Ví dụ: "in clean bold Arial font", "using modern Montserrat typeface"._
|
|
460
|
-
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.
|
|
461
|
-
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.
|
|
462
|
-
_Ví dụ: \`At the top, the main title in bold Arial font reads: 'BÍ KÍP TRÁNH NÓNG MÙA HÈ'\`._
|
|
463
|
-
|
|
464
|
-
---
|
|
465
|
-
|
|
466
|
-
## 📝 6. MẪU PROMPT CHUNG CHO BOT LLM (TÙY CHỈNH THEO YÊU CẦU)
|
|
467
|
-
|
|
468
|
-
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).
|
|
469
|
-
|
|
470
|
-
### A. Công thức Prompt Tiếng Anh (Khuyên Dùng cho API)
|
|
471
|
-
|
|
472
|
-
\`\`\`text
|
|
473
|
-
An infographic poster with [Tỷ lệ khung hình] and [Loại nền].
|
|
474
|
-
Art style is modern illustration style mixed with hand-drawn elements.
|
|
475
|
-
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]'.
|
|
476
|
-
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].
|
|
477
|
-
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ủ đề].
|
|
478
|
-
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 Ô]'.
|
|
479
|
-
The text throughout the image must be clean, legible, and easy to read.
|
|
480
|
-
At the bottom center of the image, there is a clean and tiny centered footer text that reads: 'designed by ${botName} - trợ lý của tuanminhhole'.
|
|
481
|
-
High-resolution, high quality, professional infographic poster, no spelling mistakes.
|
|
482
|
-
\`\`\`
|
|
483
|
-
|
|
484
|
-
### B. Công thức Prompt Tiếng Việt (Phong cách gốc giống Ảnh mẫu)
|
|
485
|
-
|
|
486
|
-
\`\`\`text
|
|
487
|
-
Infographic [Khung hình/tỷ lệ], nền [Loại nền].
|
|
488
|
-
Phong cách minh họa hiện đại pha hand-drawn.
|
|
489
|
-
Tiêu đề lớn '[TIÊU ĐỀ TIẾNG VIỆT LỚN]'.
|
|
490
|
-
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].
|
|
491
|
-
Màu sắc hài hòa [Mô tả tông màu phù hợp].
|
|
492
|
-
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]'.
|
|
493
|
-
Chữ rõ ràng, dễ đọc, không sai chính tả.
|
|
494
|
-
Cạnh dưới canh giữa có chữ nhỏ: 'designed by ${botName} - trợ lý của tuanminhhole'.
|
|
495
|
-
Ảnh chất lượng cao, sắc nét.
|
|
496
|
-
\`\`\`
|
|
497
|
-
|
|
498
|
-
## Related
|
|
499
|
-
- [Hành động](../../TOOLS.md)`;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
function buildInfographicGeneratorJs() {
|
|
503
|
-
return `const fs = require('fs');
|
|
504
|
-
const path = require('path');
|
|
505
|
-
|
|
506
|
-
const prompt = process.argv[2];
|
|
507
|
-
const outputPath = process.argv[3] || 'image.png';
|
|
508
|
-
|
|
509
|
-
if (!prompt) {
|
|
510
|
-
console.error('Usage: node image-generator.js "<prompt>" [output_path]');
|
|
511
|
-
process.exit(1);
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
// Find openclaw.json path dynamically by walking up
|
|
515
|
-
let openclawJsonPath = '';
|
|
516
|
-
let currentDir = process.cwd();
|
|
517
|
-
for (let i = 0; i < 5; i++) {
|
|
518
|
-
const candidate = path.join(currentDir, 'openclaw.json');
|
|
519
|
-
if (fs.existsSync(candidate)) {
|
|
520
|
-
openclawJsonPath = candidate;
|
|
521
|
-
break;
|
|
522
|
-
}
|
|
523
|
-
const candidateInDot = path.join(currentDir, '.openclaw', 'openclaw.json');
|
|
524
|
-
if (fs.existsSync(candidateInDot)) {
|
|
525
|
-
openclawJsonPath = candidateInDot;
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
528
|
-
const parent = path.dirname(currentDir);
|
|
529
|
-
if (parent === currentDir) break;
|
|
530
|
-
currentDir = parent;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Resolve API Key and Base URL from openclaw.json
|
|
534
|
-
let apiKey = 'sk-50599bc9642941c0-obzd49-1940044a'; // default fallback key
|
|
535
|
-
let baseUrl = 'http://9router:20128/v1'; // default fallback URL
|
|
536
|
-
if (openclawJsonPath) {
|
|
537
|
-
try {
|
|
538
|
-
const config = JSON.parse(fs.readFileSync(openclawJsonPath, 'utf8'));
|
|
539
|
-
const provider = config.models?.providers?.['9router'];
|
|
540
|
-
if (provider) {
|
|
541
|
-
if (provider.apiKey) apiKey = provider.apiKey;
|
|
542
|
-
if (provider.baseUrl) baseUrl = provider.baseUrl;
|
|
543
|
-
}
|
|
544
|
-
} catch (e) {}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
const modelPriorityPatterns = [
|
|
548
|
-
/recraft-?v3/i,
|
|
549
|
-
/flux-pro-?(v1\\.1-)?ultra/i,
|
|
550
|
-
/flux-kontext-max/i,
|
|
551
|
-
/flux-pro-?(v1\\.1)?/i,
|
|
552
|
-
/flux-kontext-pro/i,
|
|
553
|
-
/recraft-?v2/i,
|
|
554
|
-
/recraft/i,
|
|
555
|
-
/ideogram-?v2/i,
|
|
556
|
-
/ideogram/i,
|
|
557
|
-
/runway.*turbo/i,
|
|
558
|
-
/runway/i,
|
|
559
|
-
/flux-?(1-)?dev/i,
|
|
560
|
-
/dall-e-3/i,
|
|
561
|
-
/stable-image-ultra/i,
|
|
562
|
-
/sd3\\.5-large-turbo/i,
|
|
563
|
-
/sd3\\.5-large/i,
|
|
564
|
-
/stable-diffusion-v35/i,
|
|
565
|
-
/sd3\\.5/i,
|
|
566
|
-
/stable-image-core/i,
|
|
567
|
-
/stable-diffusion-3/i,
|
|
568
|
-
/sd3/i,
|
|
569
|
-
/sd3\\.5-medium/i,
|
|
570
|
-
/flux-?(1-)?schnell/i,
|
|
571
|
-
/grok/i,
|
|
572
|
-
/gpt/i,
|
|
573
|
-
/minimax/i,
|
|
574
|
-
/gemini-3\\.1/i,
|
|
575
|
-
/gemini-3/i,
|
|
576
|
-
/gemini-2\\.5/i,
|
|
577
|
-
/gemini/i,
|
|
578
|
-
/sdxl/i,
|
|
579
|
-
/stable-diffusion/i,
|
|
580
|
-
/sdwebui/i,
|
|
581
|
-
/comfyui/i,
|
|
582
|
-
];
|
|
583
|
-
|
|
584
|
-
(async () => {
|
|
585
|
-
try {
|
|
586
|
-
// Query active image generation models to choose the best one
|
|
587
|
-
let selectedModel = '';
|
|
588
|
-
try {
|
|
589
|
-
const modelsResponse = await fetch(\`\${baseUrl}/models/image\`, {
|
|
590
|
-
headers: {
|
|
591
|
-
'Authorization': \`Bearer \${apiKey}\`
|
|
592
|
-
}
|
|
593
|
-
});
|
|
594
|
-
const modelsData = await modelsResponse.json();
|
|
595
|
-
if (modelsData && Array.isArray(modelsData.data) && modelsData.data.length > 0) {
|
|
596
|
-
const modelIds = modelsData.data.map(m => m.id);
|
|
597
|
-
for (const pattern of modelPriorityPatterns) {
|
|
598
|
-
const found = modelIds.find(id => pattern.test(id));
|
|
599
|
-
if (found) {
|
|
600
|
-
selectedModel = found;
|
|
601
|
-
break;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
if (!selectedModel) {
|
|
605
|
-
selectedModel = modelIds[0];
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
} catch (e) {
|
|
609
|
-
console.warn('[ImageGen] Failed to auto-resolve active models, using fallback:', e.message);
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
if (!selectedModel) {
|
|
613
|
-
selectedModel = 'gemini/gemini-3.1-flash-image-preview'; // default fallback
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
console.log(\`[ImageGen] Generating: "\${prompt}" using model "\${selectedModel}"...\`);
|
|
617
|
-
const response = await fetch(\`\${baseUrl}/images/generations\`, {
|
|
618
|
-
method: 'POST',
|
|
619
|
-
headers: {
|
|
620
|
-
'Content-Type': 'application/json',
|
|
621
|
-
'Authorization': \`Bearer \${apiKey}\`
|
|
622
|
-
},
|
|
623
|
-
body: JSON.stringify({
|
|
624
|
-
model: selectedModel,
|
|
625
|
-
prompt: prompt,
|
|
626
|
-
n: 1,
|
|
627
|
-
size: 'auto',
|
|
628
|
-
response_format: 'b64_json'
|
|
629
|
-
})
|
|
630
|
-
});
|
|
631
|
-
const data = await response.json();
|
|
632
|
-
if (data.error) {
|
|
633
|
-
console.error('[ImageGen] API Error:', data.error.message || data.error);
|
|
634
|
-
process.exit(1);
|
|
635
|
-
}
|
|
636
|
-
if (data.data && data.data[0] && data.data[0].b64_json) {
|
|
637
|
-
const buf = Buffer.from(data.data[0].b64_json, 'base64');
|
|
638
|
-
const absoluteOutputPath = path.isAbsolute(outputPath) ? outputPath : path.join(process.cwd(), outputPath);
|
|
639
|
-
fs.writeFileSync(absoluteOutputPath, buf);
|
|
640
|
-
console.log(\`[ImageGen] Saved image to: \${outputPath}\`);
|
|
641
|
-
} else {
|
|
642
|
-
console.error('[ImageGen] No image data returned');
|
|
643
|
-
process.exit(1);
|
|
644
|
-
}
|
|
645
|
-
} catch (e) {
|
|
646
|
-
console.error('[ImageGen] Fetch Error:', e.message);
|
|
647
|
-
process.exit(1);
|
|
648
|
-
}
|
|
649
|
-
})();
|
|
650
|
-
`;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function buildStickerMentionSkillMd(botName = 'Williams') {
|
|
654
|
-
return `---
|
|
655
|
-
name: sticker-mention
|
|
656
|
-
description: Tự động tag người gửi trong group chat và gửi sticker Zalo theo từ khóa.
|
|
657
|
-
---
|
|
658
|
-
|
|
659
|
-
## Tự động Tag và gửi sticker
|
|
660
|
-
|
|
661
|
-
- **Trong group chat (Tự động Tag)**: Hệ thống đã cài đặt cơ chế tự động tag người gửi tin nhắn gần nhất bằng cách chèn \`@Tên\` ở đầu câu trả lời. ${botName} không cần tự chèn \`@Tên\` của người gửi ở đầu câu nữa (hệ thống sẽ tự động làm). Nhưng nếu muốn nhắc đến một người khác hoặc tag ở giữa câu, hãy viết \`@TênHiểnThị\`.
|
|
662
|
-
|
|
663
|
-
## 🎭 Rules sử dụng Sticker Zalo
|
|
664
|
-
|
|
665
|
-
Để câu trả lời thêm phần sinh động và cà khịa tự nhiên, ${botName} có thể gửi kèm Sticker bằng cách viết mã \`[Sticker: <từ_khóa>]\` ở **CUỐI** tin nhắn.
|
|
666
|
-
|
|
667
|
-
Cơ chế hoạt động: Zalo sẽ tự động tìm kiếm sticker theo \`<từ_khóa>\` và gửi đi. Hãy sử dụng từ khóa ngắn gọn, rõ ràng bằng tiếng Việt hoặc tiếng Anh phù hợp với ngữ cảnh và cảm xúc hiện tại.
|
|
668
|
-
|
|
669
|
-
Ví dụ các từ khóa gợi ý:
|
|
670
|
-
|
|
671
|
-
- \`love\` hoặc \`ôm tim\` (khi cảm ơn, bắn tim, thể hiện tình cảm)
|
|
672
|
-
- \`ca khia\` hoặc \`leu leu\` (khi cà khịa, lêu lêu trêu chọc user)
|
|
673
|
-
- \`haha\` (khi cười vui vẻ, đập bàn cười)
|
|
674
|
-
- \`khóc\` hoặc \`sad\` (khi buồn bã, khóc ròng, tội nghiệp)
|
|
675
|
-
- \`tuc gian\` hoặc \`angry\` (khi tức giận, bị trêu chọc)
|
|
676
|
-
- \`thank you\` (khi cảm ơn hoặc chào tạm biệt)
|
|
677
|
-
- \`hi\` hoặc \`chào\` (khi bắt đầu trò chuyện)
|
|
678
|
-
|
|
679
|
-
_Lưu ý: Chỉ chèn tối đa 1 Sticker ở cuối tin nhắn khi thực sự phù hợp với ngữ cảnh (ví dụ chào hỏi, lêu lêu, khóc lóc hoặc cà khịa). Không lạm dụng._
|
|
680
|
-
|
|
681
|
-
## Related
|
|
682
|
-
- [Hành động](../../TOOLS.md)`;
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
function buildStickerMentionJs() {
|
|
686
|
-
return `/**
|
|
687
|
-
* Patch @openclaw/zalouser to support outgoing @mentions and stickers in group messages.
|
|
688
|
-
*
|
|
689
|
-
* Run: docker exec openclaw-williams node /mnt/project/patch-mentions.js
|
|
690
|
-
*/
|
|
691
|
-
const fs = require('fs');
|
|
692
|
-
const path = require('path');
|
|
693
|
-
|
|
694
|
-
const searchDirs = [
|
|
695
|
-
'/root/project/.openclaw/extensions/zalouser/dist',
|
|
696
|
-
'/root/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist',
|
|
697
|
-
'/home/node/project/.openclaw/extensions/zalouser/dist',
|
|
698
|
-
'/home/node/project/.openclaw/npm/node_modules/@openclaw/zalouser/dist'
|
|
699
|
-
];
|
|
700
|
-
|
|
701
|
-
// Scan dynamic projects inside npm/projects/
|
|
702
|
-
const projectBases = ['/root/project/.openclaw', '/home/node/project/.openclaw'];
|
|
703
|
-
for (const base of projectBases) {
|
|
704
|
-
const projectsDir = path.join(base, 'npm', 'projects');
|
|
705
|
-
if (fs.existsSync(projectsDir)) {
|
|
706
|
-
try {
|
|
707
|
-
const dirs = fs.readdirSync(projectsDir);
|
|
708
|
-
for (const projectDir of dirs) {
|
|
709
|
-
const candidateDist = path.join(projectsDir, projectDir, 'node_modules', '@openclaw', 'zalouser', 'dist');
|
|
710
|
-
if (fs.existsSync(candidateDist)) {
|
|
711
|
-
searchDirs.push(candidateDist);
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
} catch (_) {}
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
const filesToPatch = [];
|
|
719
|
-
for (const dir of searchDirs) {
|
|
720
|
-
if (fs.existsSync(dir)) {
|
|
721
|
-
const files = fs.readdirSync(dir);
|
|
722
|
-
const found = files.find(f => f.startsWith('zalo-js-') && f.endsWith('.js') && !f.endsWith('.bak'));
|
|
723
|
-
if (found) {
|
|
724
|
-
filesToPatch.push(path.join(dir, found));
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
if (filesToPatch.length === 0) {
|
|
730
|
-
console.error('[patch-mentions] Error: Zalo JS files not found.');
|
|
731
|
-
process.exit(1);
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
if (process.argv.includes('--restore')) {
|
|
735
|
-
let restoredCount = 0;
|
|
736
|
-
for (const FILE of filesToPatch) {
|
|
737
|
-
const BACKUP = FILE + '.bak';
|
|
738
|
-
if (fs.existsSync(BACKUP)) {
|
|
739
|
-
console.log('[patch-mentions] Restoring ' + FILE + ' from backup...');
|
|
740
|
-
fs.copyFileSync(BACKUP, FILE);
|
|
741
|
-
restoredCount++;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
console.log('[patch-mentions] Restored ' + restoredCount + ' file(s) successfully.');
|
|
745
|
-
process.exit(0);
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
for (const FILE of filesToPatch) {
|
|
749
|
-
const BACKUP = FILE + '.bak';
|
|
750
|
-
if (fs.existsSync(BACKUP)) {
|
|
751
|
-
console.log('[patch-mentions] Restoring ' + FILE + ' from backup to ensure clean patch...');
|
|
752
|
-
fs.copyFileSync(BACKUP, FILE);
|
|
753
|
-
} else {
|
|
754
|
-
console.log('[patch-mentions] Creating backup for ' + FILE);
|
|
755
|
-
fs.copyFileSync(FILE, BACKUP);
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
let code = fs.readFileSync(FILE, 'utf8');
|
|
759
|
-
|
|
760
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
761
|
-
// PATCH 1: Helper Functions, Cache & Sticker Delay
|
|
762
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
763
|
-
const HELPER = \`
|
|
764
|
-
// ── [PATCH] Auto-Mention & Sticker Resolution for outgoing group messages ──
|
|
765
|
-
const _mentionMemberCache = new Map();
|
|
766
|
-
const _lastGroupSender = new Map();
|
|
767
|
-
const _MENTION_CACHE_TTL = 5 * 60 * 1000;
|
|
768
|
-
|
|
769
|
-
function _saveActiveSenderToCache(threadId, name, uid) {
|
|
770
|
-
if (!threadId || !name || !uid) return;
|
|
771
|
-
let cached = _mentionMemberCache.get(threadId);
|
|
772
|
-
if (!cached) {
|
|
773
|
-
cached = { ts: Date.now(), membersMap: {} };
|
|
774
|
-
_mentionMemberCache.set(threadId, cached);
|
|
775
|
-
}
|
|
776
|
-
cached.membersMap[name.toString().trim().toLowerCase()] = uid.toString();
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
async function _sendStickerAfterDelay(api, threadId, type, stickerConfig) {
|
|
780
|
-
if (!stickerConfig) return;
|
|
781
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
782
|
-
try {
|
|
783
|
-
let finalSticker = null;
|
|
784
|
-
if (stickerConfig.keyword) {
|
|
785
|
-
let kw = stickerConfig.keyword.trim().toLowerCase();
|
|
786
|
-
const kwMap = {
|
|
787
|
-
"cười": "haha",
|
|
788
|
-
"cuoi": "haha",
|
|
789
|
-
"há há": "haha",
|
|
790
|
-
"ha ha": "haha",
|
|
791
|
-
"kaka": "haha",
|
|
792
|
-
"ôm tim": "love",
|
|
793
|
-
"om tim": "love",
|
|
794
|
-
"tim": "love",
|
|
795
|
-
"yêu": "love",
|
|
796
|
-
"yeu": "love",
|
|
797
|
-
"cà khịa": "ca khia",
|
|
798
|
-
"lêu lêu": "leu leu",
|
|
799
|
-
"tức giận": "tuc gian",
|
|
800
|
-
"tức": "tuc gian",
|
|
801
|
-
"giận": "tuc gian",
|
|
802
|
-
"angry": "tuc gian",
|
|
803
|
-
"buồn": "sad",
|
|
804
|
-
"chào": "hi",
|
|
805
|
-
"hello": "hi",
|
|
806
|
-
"cúi đầu": "thank you",
|
|
807
|
-
"cảm ơn": "thank you",
|
|
808
|
-
"cảm on": "thank you",
|
|
809
|
-
"cam on": "thank you"
|
|
810
|
-
};
|
|
811
|
-
if (kwMap[kw]) {
|
|
812
|
-
console.log('[patch-mentions] Mapping keyword "' + kw + '" to "' + kwMap[kw] + '"');
|
|
813
|
-
kw = kwMap[kw];
|
|
814
|
-
}
|
|
815
|
-
console.log('[patch-mentions] Searching sticker for keyword:', kw);
|
|
816
|
-
const stickerIds = await api.getStickers(kw);
|
|
817
|
-
if (stickerIds && stickerIds.length > 0) {
|
|
818
|
-
const details = await api.getStickersDetail(stickerIds.slice(0, 5));
|
|
819
|
-
if (details && details.length > 0) {
|
|
820
|
-
const selected = details[0];
|
|
821
|
-
finalSticker = {
|
|
822
|
-
id: selected.id,
|
|
823
|
-
cateId: selected.cateId,
|
|
824
|
-
type: selected.type
|
|
825
|
-
};
|
|
826
|
-
console.log('[patch-mentions] Resolved keyword to sticker:', JSON.stringify(finalSticker));
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
if (!finalSticker) {
|
|
830
|
-
console.warn('[patch-mentions] No sticker found for keyword:', kw);
|
|
831
|
-
return;
|
|
832
|
-
}
|
|
833
|
-
} else {
|
|
834
|
-
finalSticker = {
|
|
835
|
-
id: stickerConfig.id,
|
|
836
|
-
cateId: stickerConfig.cateId,
|
|
837
|
-
type: stickerConfig.type
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
await api.sendSticker(finalSticker, threadId, type);
|
|
842
|
-
console.log('[patch-mentions] Sticker sent successfully:', finalSticker.id, 'with type:', finalSticker.type);
|
|
843
|
-
} catch (err) {
|
|
844
|
-
console.error("[patch-mentions] Failed to send sticker:", err);
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
async function _resolveAutoMentions(api, threadId, text, isGroup) {
|
|
849
|
-
if (!isGroup || !text) return null;
|
|
850
|
-
|
|
851
|
-
let membersMap = null;
|
|
852
|
-
let cached = _mentionMemberCache.get(threadId);
|
|
853
|
-
if (cached && (Date.now() - cached.ts < _MENTION_CACHE_TTL)) {
|
|
854
|
-
membersMap = cached.membersMap;
|
|
855
|
-
} else {
|
|
856
|
-
try {
|
|
857
|
-
const info = (await api.getGroupInfo(threadId)).gridInfoMap;
|
|
858
|
-
const gInfo = info ? info[threadId] : null;
|
|
859
|
-
if (gInfo && Array.isArray(gInfo.currentMems)) {
|
|
860
|
-
membersMap = {};
|
|
861
|
-
for (const member of gInfo.currentMems) {
|
|
862
|
-
if (!member) continue;
|
|
863
|
-
const id = member.id || member.uid;
|
|
864
|
-
const name = member.dName || member.zaloName;
|
|
865
|
-
if (id && name) {
|
|
866
|
-
membersMap[name.toString().trim().toLowerCase()] = id.toString();
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
_mentionMemberCache.set(threadId, {
|
|
870
|
-
ts: Date.now(),
|
|
871
|
-
membersMap
|
|
872
|
-
});
|
|
873
|
-
}
|
|
874
|
-
} catch (e) {
|
|
875
|
-
console.error("[patch-mentions] Failed to fetch group members for mention resolution:", e);
|
|
876
|
-
if (cached) membersMap = cached.membersMap;
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
if (!membersMap) return null;
|
|
881
|
-
|
|
882
|
-
// Add "all" and "cả nhóm" to membersMap pointing to "-1"
|
|
883
|
-
membersMap["all"] = "-1";
|
|
884
|
-
membersMap["cả nhóm"] = "-1";
|
|
885
|
-
|
|
886
|
-
// Sort names by length descending
|
|
887
|
-
const sortedNames = Object.keys(membersMap).sort((a, b) => b.length - a.length);
|
|
888
|
-
|
|
889
|
-
const mentions = [];
|
|
890
|
-
let maskedText = text;
|
|
891
|
-
|
|
892
|
-
for (const name of sortedNames) {
|
|
893
|
-
const escapedName = name.replace(/[-\\\\/\\\\\\\\^\$*+?.()|[\\\\]{}]/g, '\\\\\\\\\$&');
|
|
894
|
-
const regex = new RegExp('@' + escapedName + '(?!\\\\\\\\p{L}|\\\\\\\\p{N})', 'gui');
|
|
895
|
-
|
|
896
|
-
let match;
|
|
897
|
-
while ((match = regex.exec(maskedText)) !== null) {
|
|
898
|
-
const pos = match.index;
|
|
899
|
-
const matchedString = match[0];
|
|
900
|
-
const len = matchedString.length;
|
|
901
|
-
const uid = membersMap[name];
|
|
902
|
-
|
|
903
|
-
mentions.push({ pos, len, uid });
|
|
904
|
-
maskedText = maskedText.substring(0, pos) + ' '.repeat(len) + maskedText.substring(pos + len);
|
|
905
|
-
regex.lastIndex = 0;
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
mentions.sort((a, b) => a.pos - b.pos);
|
|
910
|
-
return mentions.length > 0 ? mentions : null;
|
|
911
|
-
}
|
|
912
|
-
\`;
|
|
913
|
-
|
|
914
|
-
// Insert HELPER before toInboundMessage
|
|
915
|
-
const TO_INBOUND_ANCHOR = 'function toInboundMessage(message, ownUserId) {';
|
|
916
|
-
if (!code.includes(TO_INBOUND_ANCHOR)) {
|
|
917
|
-
console.error('[patch-mentions] Error: TO_INBOUND_ANCHOR not found!');
|
|
918
|
-
process.exit(1);
|
|
919
|
-
}
|
|
920
|
-
code = code.replace(TO_INBOUND_ANCHOR, () => HELPER + '\\n' + TO_INBOUND_ANCHOR);
|
|
921
|
-
|
|
922
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
923
|
-
// PATCH 2: Save active sender and track last sender of group
|
|
924
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
925
|
-
const INBOUND_RESOLVE_ANCHOR = \`\\tconst senderId = toNumberId(data.uidFrom);
|
|
926
|
-
\\tconst threadId = isGroup ? toNumberId(data.idTo) : toNumberId(data.uidFrom) || toNumberId(data.idTo);\`;
|
|
927
|
-
|
|
928
|
-
const INBOUND_RESOLVE_PATCH = \`\\tconst senderId = toNumberId(data.uidFrom);
|
|
929
|
-
\\tconst threadId = isGroup ? toNumberId(data.idTo) : toNumberId(data.uidFrom) || toNumberId(data.idTo);
|
|
930
|
-
\\t// [PATCH] Save sender info and track last active sender (excluding bot itself)
|
|
931
|
-
\\tif (isGroup && senderId && data.dName && threadId && senderId.toString() !== ownUserId?.toString()) {
|
|
932
|
-
\\t\\t_saveActiveSenderToCache(threadId, data.dName, senderId);
|
|
933
|
-
\\t\\t_lastGroupSender.set(threadId, {
|
|
934
|
-
\\t\\t\\tuid: senderId.toString(),
|
|
935
|
-
\\t\\t\\tname: data.dName.toString().trim(),
|
|
936
|
-
\\t\\t\\ttimestamp: Date.now()
|
|
937
|
-
\\t\\t});
|
|
938
|
-
\\t}\`;
|
|
939
|
-
|
|
940
|
-
if (!code.includes(INBOUND_RESOLVE_ANCHOR)) {
|
|
941
|
-
console.error('[patch-mentions] Error: INBOUND_RESOLVE_ANCHOR not found!');
|
|
942
|
-
process.exit(1);
|
|
943
|
-
}
|
|
944
|
-
code = code.replace(INBOUND_RESOLVE_ANCHOR, () => INBOUND_RESOLVE_PATCH);
|
|
945
|
-
|
|
946
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
947
|
-
// PATCH 3: Add sticker config parser to sendZaloTextMessage start
|
|
948
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
949
|
-
const SEND_TEXT_FUNC_ANCHOR = 'async function sendZaloTextMessage(threadId, text, options = {}) {';
|
|
950
|
-
const SEND_TEXT_FUNC_PATCH = \`async function sendZaloTextMessage(threadId, text, options = {}) {
|
|
951
|
-
// [PATCH] Parse sticker config and strip from text
|
|
952
|
-
let stickerConfig = null;
|
|
953
|
-
const stickerRegex = /\\\\[Sticker:\\\\s*([^\\\\]]+)\\\\]/i;
|
|
954
|
-
if (text && typeof text === 'string') {
|
|
955
|
-
const match = text.match(stickerRegex);
|
|
956
|
-
if (match) {
|
|
957
|
-
const parts = match[1].split(':');
|
|
958
|
-
if (parts.length >= 2) {
|
|
959
|
-
stickerConfig = {
|
|
960
|
-
id: parts[0].trim(),
|
|
961
|
-
cateId: parts[1].trim(),
|
|
962
|
-
type: parts[2] ? parseInt(parts[2].trim(), 10) : 30
|
|
963
|
-
};
|
|
964
|
-
} else {
|
|
965
|
-
stickerConfig = {
|
|
966
|
-
keyword: parts[0].trim()
|
|
967
|
-
};
|
|
968
|
-
}
|
|
969
|
-
text = text.replace(stickerRegex, '').trim();
|
|
970
|
-
}
|
|
971
|
-
}\`;
|
|
972
|
-
|
|
973
|
-
if (!code.includes(SEND_TEXT_FUNC_ANCHOR)) {
|
|
974
|
-
console.error('[patch-mentions] Error: SEND_TEXT_FUNC_ANCHOR not found!');
|
|
975
|
-
process.exit(1);
|
|
976
|
-
}
|
|
977
|
-
code = code.replace(SEND_TEXT_FUNC_ANCHOR, () => SEND_TEXT_FUNC_PATCH);
|
|
978
|
-
|
|
979
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
980
|
-
// PATCH 4: Intercept sendZaloTextMessage and resolve auto-tagging
|
|
981
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
982
|
-
|
|
983
|
-
// 4a. In mediaUrl path (const media to const textStyles):
|
|
984
|
-
const MEDIA_URL_RESOLVE_ANCHOR = \`\\t\\t\\t\\tconst media = await loadOutboundMediaFromUrl(options.mediaUrl.trim(), {
|
|
985
|
-
\\t\\t\\t\\t\\tmediaLocalRoots: options.mediaLocalRoots,
|
|
986
|
-
\\t\\t\\t\\t\\tmediaReadFile: options.mediaReadFile
|
|
987
|
-
\\t\\t\\t\\t});
|
|
988
|
-
\\t\\t\\t\\tconst fileName = resolveMediaFileName({
|
|
989
|
-
\\t\\t\\t\\t\\tmediaUrl: options.mediaUrl,
|
|
990
|
-
\\t\\t\\t\\t\\tfileName: media.fileName,
|
|
991
|
-
\\t\\t\\t\\t\\tcontentType: media.contentType,
|
|
992
|
-
\\t\\t\\t\\t\\tkind: media.kind
|
|
993
|
-
\\t\\t\\t\\t});
|
|
994
|
-
\\t\\t\\t\\tconst payloadText = (text || options.caption || "").slice(0, 2e3);
|
|
995
|
-
\\t\\t\\t\\tconst textStyles = clampTextStyles(payloadText, options.textStyles);\`;
|
|
996
|
-
|
|
997
|
-
const MEDIA_URL_RESOLVE_PATCH = \`\\t\\t\\t\\tconst media = await loadOutboundMediaFromUrl(options.mediaUrl.trim(), {
|
|
998
|
-
\\t\\t\\t\\t\\tmediaLocalRoots: options.mediaLocalRoots,
|
|
999
|
-
\\t\\t\\t\\t\\tmediaReadFile: options.mediaReadFile
|
|
1000
|
-
\\t\\t\\t\\t});
|
|
1001
|
-
\\t\\t\\t\\tconst fileName = resolveMediaFileName({
|
|
1002
|
-
\\t\\t\\t\\t\\tmediaUrl: options.mediaUrl,
|
|
1003
|
-
\\t\\t\\t\\t\\tfileName: media.fileName,
|
|
1004
|
-
\\t\\t\\t\\t\\tcontentType: media.contentType,
|
|
1005
|
-
\\t\\t\\t\\t\\tkind: media.kind
|
|
1006
|
-
\\t\\t\\t\\t});
|
|
1007
|
-
\\t\\t\\t\\tlet payloadText = (text || options.caption || "").slice(0, 2e3);
|
|
1008
|
-
\\t\\t\\t\\tconst textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
1009
|
-
\\t\\t\\t\\tlet mentions = null;
|
|
1010
|
-
\\t\\t\\t\\tif (options.isGroup) {
|
|
1011
|
-
\\t\\t\\t\\t\\tmentions = await _resolveAutoMentions(api, trimmedThreadId, payloadText, options.isGroup);
|
|
1012
|
-
\\t\\t\\t\\t\\tconst lastSender = _lastGroupSender.get(trimmedThreadId);
|
|
1013
|
-
\\t\\t\\t\\t\\tif (lastSender && (Date.now() - lastSender.timestamp < 5 * 60 * 1000)) {
|
|
1014
|
-
\\t\\t\\t\\t\\t\\tconst alreadyTagged = mentions && mentions.some(m => m.uid === lastSender.uid);
|
|
1015
|
-
\\t\\t\\t\\t\\t\\tif (!alreadyTagged) {
|
|
1016
|
-
\\t\\t\\t\\t\\t\\t\\tconst tagText = \\\`@\\\${lastSender.name} \\\`;
|
|
1017
|
-
\\t\\t\\t\\t\\t\\t\\tpayloadText = tagText + payloadText;
|
|
1018
|
-
\\t\\t\\t\\t\\t\\t\\tconst newMention = {
|
|
1019
|
-
\\t\\t\\t\\t\\t\\t\\t\\tpos: 0,
|
|
1020
|
-
\\t\\t\\t\\t\\t\\t\\t\\tlen: tagText.length - 1,
|
|
1021
|
-
\\t\\t\\t\\t\\t\\t\\t\\tuid: lastSender.uid
|
|
1022
|
-
\\t\\t\\t\\t\\t\\t\\t};
|
|
1023
|
-
\\t\\t\\t\\t\\t\\t\\tif (mentions) {
|
|
1024
|
-
\\t\\t\\t\\t\\t\\t\\t\\tmentions = mentions.map(m => ({ ...m, pos: m.pos + tagText.length }));
|
|
1025
|
-
\\t\\t\\t\\t\\t\\t\\t\\tmentions.unshift(newMention);
|
|
1026
|
-
\\t\\t\\t\\t\\t\\t\\t} else {
|
|
1027
|
-
\\t\\t\\t\\t\\t\\t\\t\\tmentions = [newMention];
|
|
1028
|
-
\\t\\t\\t\\t\\t\\t\\t}
|
|
1029
|
-
\\t\\t\\t\\t\\t\\t\\tif (textStyles) {
|
|
1030
|
-
\\t\\t\\t\\t\\t\\t\\t\\tfor (const style of textStyles) {
|
|
1031
|
-
\\t\\t\\t\\t\\t\\t\\t\\t\\tif (style && typeof style.start === 'number') {
|
|
1032
|
-
\\t\\t\\t\\t\\t\\t\\t\\t\\t\\tstyle.start += tagText.length;
|
|
1033
|
-
\\t\\t\\t\\t\\t\\t\\t\\t\\t}
|
|
1034
|
-
\\t\\t\\t\\t\\t\\t\\t\\t}
|
|
1035
|
-
\\t\\t\\t\\t\\t\\t\\t}
|
|
1036
|
-
\\t\\t\\t\\t\\t\\t}
|
|
1037
|
-
\\t\\t\\t\\t\\t}
|
|
1038
|
-
\\t\\t\\t\\t}\`;
|
|
1039
|
-
|
|
1040
|
-
if (!code.includes(MEDIA_URL_RESOLVE_ANCHOR)) {
|
|
1041
|
-
console.error('[patch-mentions] Error: MEDIA_URL_RESOLVE_ANCHOR not found!');
|
|
1042
|
-
process.exit(1);
|
|
1043
|
-
}
|
|
1044
|
-
code = code.replace(MEDIA_URL_RESOLVE_ANCHOR, () => MEDIA_URL_RESOLVE_PATCH);
|
|
1045
|
-
|
|
1046
|
-
// 4b. Inject mentions to api.sendMessage with attachments & send sticker:
|
|
1047
|
-
const MEDIA_SEND_MESSAGE_ANCHOR = \`\\t\\t\\t\\tconst messageId = extractSendMessageId(await api.sendMessage({
|
|
1048
|
-
\\t\\t\\t\\t\\tmsg: payloadText,
|
|
1049
|
-
\\t\\t\\t\\t\\t...textStyles ? { styles: textStyles } : {},
|
|
1050
|
-
\\t\\t\\t\\t\\tattachments: [{
|
|
1051
|
-
\\t\\t\\t\\t\\t\\tdata: media.buffer,
|
|
1052
|
-
\\t\\t\\t\\t\\t\\tfilename: fileName.includes(".") ? fileName : \\\`\\\${fileName}.bin\\\`,
|
|
1053
|
-
\\t\\t\\t\\t\\t\\tmetadata: { totalSize: media.buffer.length }
|
|
1054
|
-
\\t\\t\\t\\t\\t}]
|
|
1055
|
-
\\t\\t\\t\\t}, trimmedThreadId, type));
|
|
1056
|
-
\\t\\t\\t\\treturn {
|
|
1057
|
-
\\t\\t\\t\\t\\tok: true,
|
|
1058
|
-
\\t\\t\\t\\t\\tmessageId,
|
|
1059
|
-
\\t\\t\\t\\t\\treceipt: createZalouserSendReceipt({
|
|
1060
|
-
\\t\\t\\t\\t\\t\\tmessageId,
|
|
1061
|
-
\\t\\t\\t\\t\\t\\tthreadId: trimmedThreadId,
|
|
1062
|
-
\\t\\t\\t\\t\\t\\tkind: "media"
|
|
1063
|
-
\\t\\t\\t\\t\\t})
|
|
1064
|
-
\\t\\t\\t\\t};\`;
|
|
1065
|
-
|
|
1066
|
-
const MEDIA_SEND_MESSAGE_PATCH = \`\\t\\t\\t\\tconst messageId = extractSendMessageId(await api.sendMessage({
|
|
1067
|
-
\\t\\t\\t\\t\\tmsg: payloadText,
|
|
1068
|
-
\\t\\t\\t\\t\\t...textStyles ? { styles: textStyles } : {},
|
|
1069
|
-
\\t\\t\\t\\t\\t...(mentions ? { mentions } : {}),
|
|
1070
|
-
\\t\\t\\t\\t\\tattachments: [{
|
|
1071
|
-
\\t\\t\\t\\t\\t\\tdata: media.buffer,
|
|
1072
|
-
\\t\\t\\t\\t\\t\\tfilename: fileName.includes(".") ? fileName : \\\`\\\${fileName}.bin\\\`,
|
|
1073
|
-
\\t\\t\\t\\t\\t\\tmetadata: { totalSize: media.buffer.length }
|
|
1074
|
-
\\t\\t\\t\\t\\t}]
|
|
1075
|
-
\\t\\t\\t\\t}, trimmedThreadId, type));
|
|
1076
|
-
\\t\\t\\t\\tif (stickerConfig) {
|
|
1077
|
-
\\t\\t\\t\\t\\tawait _sendStickerAfterDelay(api, trimmedThreadId, type, stickerConfig);
|
|
1078
|
-
\\t\\t\\t\\t}
|
|
1079
|
-
\\t\\t\\t\\treturn {
|
|
1080
|
-
\\t\\t\\t\\t\\tok: true,
|
|
1081
|
-
\\t\\t\\t\\t\\tmessageId,
|
|
1082
|
-
\\t\\t\\t\\t\\treceipt: createZalouserSendReceipt({
|
|
1083
|
-
\\t\\t\\t\\t\\t\\tmessageId,
|
|
1084
|
-
\\t\\t\\t\\t\\t\\tthreadId: trimmedThreadId,
|
|
1085
|
-
\\t\\t\\t\\t\\t\\tkind: "media"
|
|
1086
|
-
\\t\\t\\t\\t\\t})
|
|
1087
|
-
\\t\\t\\t\\t};\`;
|
|
1088
|
-
|
|
1089
|
-
if (!code.includes(MEDIA_SEND_MESSAGE_ANCHOR)) {
|
|
1090
|
-
console.error('[patch-mentions] Error: MEDIA_SEND_MESSAGE_ANCHOR not found!');
|
|
1091
|
-
process.exit(1);
|
|
1092
|
-
}
|
|
1093
|
-
code = code.replace(MEDIA_SEND_MESSAGE_ANCHOR, () => MEDIA_SEND_MESSAGE_PATCH);
|
|
1094
|
-
|
|
1095
|
-
// 4c. In plain-text path (auto-tag & send sticker):
|
|
1096
|
-
const PLAIN_TEXT_SEND_ANCHOR = \`\\t\\t\\tconst payloadText = text.slice(0, 2e3);
|
|
1097
|
-
\\t\\t\\tconst textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
1098
|
-
\\t\\t\\tconst messageId = extractSendMessageId(await api.sendMessage(textStyles ? {
|
|
1099
|
-
\\t\\t\\t\\tmsg: payloadText,
|
|
1100
|
-
\\t\\t\\t\\tstyles: textStyles
|
|
1101
|
-
\\t\\t\\t} : payloadText, trimmedThreadId, type));
|
|
1102
|
-
\\t\\t\\treturn {
|
|
1103
|
-
\\t\\t\\t\\tok: true,
|
|
1104
|
-
\\t\\t\\t\\tmessageId,
|
|
1105
|
-
\\t\\t\\t\\treceipt: createZalouserSendReceipt({
|
|
1106
|
-
\\t\\t\\t\\t\\tmessageId,
|
|
1107
|
-
\\t\\t\\t\\t\\tthreadId: trimmedThreadId,
|
|
1108
|
-
\\t\\t\\t\\t\\tkind: "text"
|
|
1109
|
-
\\t\\t\\t\\t})
|
|
1110
|
-
\\t\\t\\t};\`;
|
|
1111
|
-
|
|
1112
|
-
const PLAIN_TEXT_SEND_PATCH = \`\\t\\t\\tlet payloadText = text.slice(0, 2e3);
|
|
1113
|
-
\\t\\t\\tconst textStyles = clampTextStyles(payloadText, options.textStyles);
|
|
1114
|
-
\\t\\t\\tlet mentions = null;
|
|
1115
|
-
\\t\\t\\tif (options.isGroup) {
|
|
1116
|
-
\\t\\t\\t\\tmentions = await _resolveAutoMentions(api, trimmedThreadId, payloadText, options.isGroup);
|
|
1117
|
-
\\t\\t\\t\\tconst lastSender = _lastGroupSender.get(trimmedThreadId);
|
|
1118
|
-
\\t\\t\\t\\tif (lastSender && (Date.now() - lastSender.timestamp < 5 * 60 * 1000)) {
|
|
1119
|
-
\\t\\t\\t\\t\\tconst alreadyTagged = mentions && mentions.some(m => m.uid === lastSender.uid);
|
|
1120
|
-
\\t\\t\\t\\t\\tif (!alreadyTagged) {
|
|
1121
|
-
\\t\\t\\t\\t\\t\\tconst tagText = \\\`@\\\${lastSender.name} \\\`;
|
|
1122
|
-
\\t\\t\\t\\t\\t\\tpayloadText = tagText + payloadText;
|
|
1123
|
-
\\t\\t\\t\\t\\t\\tconst newMention = {
|
|
1124
|
-
\\t\\t\\t\\t\\t\\t\\tpos: 0,
|
|
1125
|
-
\\t\\t\\t\\t\\t\\t\\tlen: tagText.length - 1,
|
|
1126
|
-
\\t\\t\\t\\t\\t\\t\\tuid: lastSender.uid
|
|
1127
|
-
\\t\\t\\t\\t\\t\\t};
|
|
1128
|
-
\\t\\t\\t\\t\\t\\tif (mentions) {
|
|
1129
|
-
\\t\\t\\t\\t\\t\\t\\tmentions = mentions.map(m => ({ ...m, pos: m.pos + tagText.length }));
|
|
1130
|
-
\\t\\t\\t\\t\\t\\t\\tmentions.unshift(newMention);
|
|
1131
|
-
\\t\\t\\t\\t\\t\\t} else {
|
|
1132
|
-
\\t\\t\\t\\t\\t\\t\\tmentions = [newMention];
|
|
1133
|
-
\\t\\t\\t\\t\\t\\t}
|
|
1134
|
-
\\t\\t\\t\\t\\t\\tif (textStyles) {
|
|
1135
|
-
\\t\\t\\t\\t\\t\\t\\tfor (const style of textStyles) {
|
|
1136
|
-
\\t\\t\\t\\t\\t\\t\\t\\tif (style && typeof style.start === 'number') {
|
|
1137
|
-
\\t\\t\\t\\t\\t\\t\\t\\t\\tstyle.start += tagText.length;
|
|
1138
|
-
\\t\\t\\t\\t\\t\\t\\t\\t}
|
|
1139
|
-
\\t\\t\\t\\t\\t\\t\\t}
|
|
1140
|
-
\\t\\t\\t\\t\\t\\t}
|
|
1141
|
-
\\t\\t\\t\\t\\t}
|
|
1142
|
-
\\t\\t\\t\\t}
|
|
1143
|
-
\\t\\t\\t}
|
|
1144
|
-
\\t\\t\\tconst messageObj = {
|
|
1145
|
-
\\t\\t\\t\\tmsg: payloadText,
|
|
1146
|
-
\\t\\t\\t\\t...(textStyles ? { styles: textStyles } : {}),
|
|
1147
|
-
\\t\\t\\t\\t...(mentions ? { mentions } : {})
|
|
1148
|
-
\\t\\t\\t};
|
|
1149
|
-
\\t\\t\\tconst messageId = extractSendMessageId(await api.sendMessage(messageObj, trimmedThreadId, type));
|
|
1150
|
-
\\t\\t\\tif (stickerConfig) {
|
|
1151
|
-
\\t\\t\\t\\tawait _sendStickerAfterDelay(api, trimmedThreadId, type, stickerConfig);
|
|
1152
|
-
\\t\\t\\t}
|
|
1153
|
-
\\t\\t\\treturn {
|
|
1154
|
-
\\t\\t\\t\\tok: true,
|
|
1155
|
-
\\t\\t\\t\\tmessageId,
|
|
1156
|
-
\\t\\t\\t\\treceipt: createZalouserSendReceipt({
|
|
1157
|
-
\\t\\t\\t\\t\\tmessageId,
|
|
1158
|
-
\\t\\t\\t\\t\\tthreadId: trimmedThreadId,
|
|
1159
|
-
\\t\\t\\t\\t\\tkind: "text"
|
|
1160
|
-
\\t\\t\\t\\t})
|
|
1161
|
-
\\t\\t\\t};\`;
|
|
1162
|
-
|
|
1163
|
-
if (!code.includes(PLAIN_TEXT_SEND_ANCHOR)) {
|
|
1164
|
-
console.error('[patch-mentions] Error: PLAIN_TEXT_SEND_ANCHOR not found!');
|
|
1165
|
-
process.exit(1);
|
|
1166
|
-
}
|
|
1167
|
-
code = code.replace(PLAIN_TEXT_SEND_ANCHOR, () => PLAIN_TEXT_SEND_PATCH);
|
|
1168
|
-
|
|
1169
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1170
|
-
// PATCH 5: Intercept sendZaloTextMessage with sticker search command
|
|
1171
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
1172
|
-
const WITH_ZALO_API_ANCHOR = \`\\treturn await withZaloApi(profile, async (api) => {
|
|
1173
|
-
\\t\\tconst type = options.isGroup ? ThreadType.Group : ThreadType.User;\`;
|
|
1174
|
-
|
|
1175
|
-
const WITH_ZALO_API_PATCH = \`\\treturn await withZaloApi(profile, async (api) => {
|
|
1176
|
-
\\t\\t// [PATCH] Temporary sticker search command interceptor
|
|
1177
|
-
\\t\\tif (text && text.startsWith('/search-sticker ')) {
|
|
1178
|
-
\\t\\t\\tconst keyword = text.replace('/search-sticker ', '').trim();
|
|
1179
|
-
\\t\\t\\ttry {
|
|
1180
|
-
\\t\\t\\t\\tconst stickerIds = await api.getStickers(keyword);
|
|
1181
|
-
\\t\\t\\t\\tif (!stickerIds || stickerIds.length === 0) {
|
|
1182
|
-
\\t\\t\\t\\t\\ttext = "Không tìm thấy sticker nào cho từ khóa này.";
|
|
1183
|
-
\\t\\t\\t\\t} else {
|
|
1184
|
-
\\t\\t\\t\\t\\tconst details = await api.getStickersDetail(stickerIds.slice(0, 20));
|
|
1185
|
-
\\t\\t\\t\\t\\tconst list = details.map(d => \\\`- \\\${d.text || 'sticker'}: [Sticker: \\\${d.id}:\\\${d.cateId}] (id: \\\${d.id}, cateId: \\\${d.cateId})\\\`).join('\\\\n');
|
|
1186
|
-
\\t\\t\\t\\t\\ttext = "Kết quả tìm kiếm sticker cho '" + keyword + "':\\\\n" + list;
|
|
1187
|
-
\\t\\t\\t\\t}
|
|
1188
|
-
\\t\\t\\t} catch (e) {
|
|
1189
|
-
\\t\\t\\t\\ttext = "Lỗi khi tìm sticker: " + e.message;
|
|
1190
|
-
\\t\\t\\t}
|
|
1191
|
-
\\t\\t}
|
|
1192
|
-
\\t\\tconst type = options.isGroup ? ThreadType.Group : ThreadType.User;\`;
|
|
1193
|
-
|
|
1194
|
-
if (!code.includes(WITH_ZALO_API_ANCHOR)) {
|
|
1195
|
-
console.error('[patch-mentions] Error: WITH_ZALO_API_ANCHOR not found!');
|
|
1196
|
-
process.exit(1);
|
|
1197
|
-
}
|
|
1198
|
-
code = code.replace(WITH_ZALO_API_ANCHOR, () => WITH_ZALO_API_PATCH);
|
|
1199
|
-
|
|
1200
|
-
// Write modified file
|
|
1201
|
-
fs.writeFileSync(FILE, code, 'utf8');
|
|
1202
|
-
console.log('[patch-mentions] File successfully patched: ' + FILE);
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
`;
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
390
|
+
function buildInfographicGeneratorSkillMd(botName = 'Williams') {
|
|
391
|
+
return '';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function buildInfographicGeneratorJs() {
|
|
395
|
+
return '';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function buildStickerMentionSkillMd(botName = 'Williams') {
|
|
399
|
+
return '';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function buildStickerMentionJs() {
|
|
403
|
+
return '';
|
|
404
|
+
}
|
|
405
|
+
|
|
1211
406
|
function buildSecurityRules(isVi = true) {
|
|
1212
407
|
if (isVi) {
|
|
1213
408
|
return `\n\n## 🔐 Quy Tắc Bảo Mật — BẮT BUỘC (Red Lines)\n\n**GIỚI HẠN FILE & HỆ THỐNG:**\n- ✅ CHỈ làm việc trong thư mục project (workspace).\n- ❌ KHÔNG cung cấp file nội bộ của \`.openclaw\` (config.json, registry.json, task-memory.json, workspace-memory...)\n- ❌ KHÔNG đọc, sao chép, hoặc truy cập bất kỳ file nào ngoài thư mục project\n- ❌ KHÔNG quét hoặc liệt kê các thư mục hệ thống: Documents, Desktop, Downloads, AppData\n- ❌ KHÔNG truy cập registry, system32, hoặc Program Files\n- ❌ KHÔNG cài đặt phần mềm, driver, hoặc service ngoài Docker\n\n**API KEY & CREDENTIALS:**\n- ❌ KHÔNG BAO GIỜ hiển thị API key, token, hoặc mật khẩu trong chat\n- ❌ KHÔNG viết API key trực tiếp vào mã nguồn\n- ❌ KHÔNG commit file credentials lên Git\n- ✅ LUÔN lưu credentials trong file .env riêng\n- ✅ LUÔN dùng biến môi trường thay vì hardcode\n\n**VÍ CRYPTO & TÀI SẢN SỐ:**\n- ❌ TUYỆT ĐỐI KHÔNG truy cập, đọc, hoặc quét các thư mục ví crypto\n- ❌ KHÔNG quét clipboard (có thể chứa seed phrases)\n- ❌ KHÔNG truy cập browser profile, cookie, hoặc mật khẩu đã lưu\n- ❌ KHÔNG cài đặt npm package lạ (chỉ openclaw và plugin chính thức)\n\n**DOCKER:**\n- ✅ Chỉ mount đúng thư mục cần thiết (config + workspace)\n- ❌ KHÔNG mount nguyên ổ đĩa (C:/ hoặc D:/)\n- ❌ KHÔNG chạy container với --privileged\n- ✅ Giới hạn port expose (chỉ 18789)`;
|