openclaw-zalo-mod 2.30.0 → 2.31.2
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 +1 -1
- package/README.vi.md +1 -1
- package/dashboard.js +22 -2
- package/index.html +1 -1
- package/index.js +232 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/context/conversation-buffer.js +5 -1
- package/src/integration/zalo-mod-engine.js +5 -1
- package/src/storage/database.js +15 -0
- package/src/storage/media-backfill.js +57 -0
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://openclaw.ai)
|
|
7
|
-
[](./CHANGELOG.md)
|
|
8
8
|
|
|
9
9
|
**[🇺🇸 English](./README.md)**
|
|
10
10
|
|
package/README.vi.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
6
|
[](https://openclaw.ai)
|
|
7
|
-
[](./CHANGELOG.md)
|
|
8
8
|
|
|
9
9
|
**[🇺🇸 English](./README.md)**
|
|
10
10
|
|
package/dashboard.js
CHANGED
|
@@ -10,7 +10,7 @@ const modalBody = document.getElementById('modalBody');
|
|
|
10
10
|
const modalCancel = document.getElementById('modalCancel');
|
|
11
11
|
const modalConfirm = document.getElementById('modalConfirm');
|
|
12
12
|
const token = window.ZALO_DASHBOARD_TOKEN || '';
|
|
13
|
-
const pluginVersion = '2.
|
|
13
|
+
const pluginVersion = '2.31.2';
|
|
14
14
|
let state = null;
|
|
15
15
|
let activeGroupId = '';
|
|
16
16
|
let lang = localStorage.getItem('zaloDashboardLang') || 'vi';
|
|
@@ -169,6 +169,24 @@ function setSection(id) {
|
|
|
169
169
|
if (id === 'contacts') renderCrmContacts();
|
|
170
170
|
if (id === 'leads') renderCrmLeads();
|
|
171
171
|
if (id === 'tasks') renderCrmTasks();
|
|
172
|
+
// Trang đọc từ `state` cache trong trình duyệt — nhưng dữ liệu phía server có thể đã đổi mà
|
|
173
|
+
// KHÔNG đi qua tab này: owner nhắn "đồng bộ nhóm" là agent chạy sync-groups qua zalo_mod_action
|
|
174
|
+
// (cùng handler với nút UI). Đo thật 30/08/2026 trên bot "Em Mơ": server/API trả 15 nhóm mà tab
|
|
175
|
+
// mở sẵn vẫn vẽ 9 nhóm cũ — nhìn y như "đồng bộ không ăn". Refetch NỀN khi mở các trang
|
|
176
|
+
// state-driven; loadState() tự kết thúc bằng renderState() nên gọi là đủ. Chỉ các trang này —
|
|
177
|
+
// trang on-demand (chat, journal…) không refetch kẻo re-render nuốt nội dung đang gõ.
|
|
178
|
+
if (id === 'groups' || id === 'overview' || id === 'members') refreshStateQuiet();
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Refetch state ở nền khi chuyển trang. Cờ in-flight để bấm qua lại nhanh không dồn request;
|
|
182
|
+
// lỗi mạng nuốt im — chuyển trang không được nổ toast vì một lần refetch nền hỏng.
|
|
183
|
+
let _stateRefreshQuietInFlight = false;
|
|
184
|
+
function refreshStateQuiet() {
|
|
185
|
+
if (_stateRefreshQuietInFlight) return;
|
|
186
|
+
_stateRefreshQuietInFlight = true;
|
|
187
|
+
loadState()
|
|
188
|
+
.catch(() => {})
|
|
189
|
+
.finally(() => { _stateRefreshQuietInFlight = false; });
|
|
172
190
|
}
|
|
173
191
|
// Re-render whichever on-demand section is currently active (these render on
|
|
174
192
|
// tab-open via setSection, not inside renderState). Called when the selected bot
|
|
@@ -7047,7 +7065,9 @@ function chatMsgHtml(m, { isGroup, lastDay, isNew = false }) {
|
|
|
7047
7065
|
const html = `${sep}<div class="chat-msg${m.fromSelf ? ' me' : ''}${isNew ? ' chat-msg-new' : ''}" data-msg-id="${crmEsc(m.id)}">
|
|
7048
7066
|
${/* Tên người gửi chỉ có nghĩa trong nhóm — DM thì hai bên đã rõ, in thêm chỉ tổ rối. */''}
|
|
7049
7067
|
${isGroup && !m.fromSelf ? `<div class="chat-msg-who">${crmEsc(m.senderName || m.senderId)}</div>` : ''}
|
|
7050
|
-
|
|
7068
|
+
${/* Co anh roi thi khong in kem chu "[Media attachment]" — day la chu do host sinh ra khi
|
|
7069
|
+
tin khong co phan chu, giu lai chi lam bong bong roi. */''}
|
|
7070
|
+
<div class="chat-bubble">${media}${crmEsc(media && /^\[(media|file|image|sticker)[^\]]*\]$/i.test((m.text || '').trim()) ? '' : (m.text || ''))}</div>
|
|
7051
7071
|
<div class="chat-msg-time">${chatTime(m.sentAt)}</div>
|
|
7052
7072
|
</div>`;
|
|
7053
7073
|
return { html, day };
|
package/index.html
CHANGED
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
log.</span>
|
|
128
128
|
</div>
|
|
129
129
|
<div class="plugin-meta">
|
|
130
|
-
<strong>zalo-mod <span id="pluginVersion">v2.
|
|
130
|
+
<strong>zalo-mod <span id="pluginVersion">v2.31.2</span></strong>
|
|
131
131
|
<span>Được làm ❤️ bởi tuanminhole</span>
|
|
132
132
|
<div class="socials" aria-label="Author links">
|
|
133
133
|
<a href="https://www.facebook.com/holeminhtuan.it/" target="_blank" rel="noreferrer"
|
package/index.js
CHANGED
|
@@ -25,6 +25,7 @@ import http from 'node:http';
|
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import { definePluginEntry } from 'openclaw/plugin-sdk/plugin-entry';
|
|
28
|
+
import { ghepLaiMedia } from './src/storage/media-backfill.js';
|
|
28
29
|
import { createZaloModEngine } from './src/integration/zalo-mod-engine.js';
|
|
29
30
|
import { handleCrmAction } from './src/crm/crm-api.js';
|
|
30
31
|
import { buildZaloPeople } from './src/crm/zalo-people.js';
|
|
@@ -89,6 +90,22 @@ async function _readBotNameFromIdentity(workspaceDir) {
|
|
|
89
90
|
} catch { return null; }
|
|
90
91
|
}
|
|
91
92
|
|
|
93
|
+
/**
|
|
94
|
+
* openclaw 2026.8.x chuyển agents.list (mảng) thành agents.entries (object theo id) và schema
|
|
95
|
+
* mới CẤM `list` nằm trong file. Đọc được CẢ HAI dạng — thiếu nhánh entries thì trên 2026.8
|
|
96
|
+
* plugin tưởng project không có agent nào: workspace resolve về mặc định sai chỗ, binding
|
|
97
|
+
* zalo-connect không tự gắn, skill cài lạc thư mục (đo 02/09/2026 trên vps_c-thu).
|
|
98
|
+
*/
|
|
99
|
+
function agentListFromConfig(config) {
|
|
100
|
+
const ag = config?.agents || {};
|
|
101
|
+
if (Array.isArray(ag.list) && ag.list.length) return ag.list;
|
|
102
|
+
const entries = ag.entries;
|
|
103
|
+
if (entries && typeof entries === 'object' && !Array.isArray(entries)) {
|
|
104
|
+
return Object.entries(entries).map(([id, v]) => ({ id, ...(v && typeof v === 'object' ? v : {}) }));
|
|
105
|
+
}
|
|
106
|
+
return Array.isArray(ag.list) ? ag.list : [];
|
|
107
|
+
}
|
|
108
|
+
|
|
92
109
|
|
|
93
110
|
/**
|
|
94
111
|
* Auto-patch openclaw.json — chỉ đảm bảo entry có `enabled` + `hooks` (+ bindings/channels).
|
|
@@ -156,7 +173,7 @@ async function _patchOpenclawConfig(openclawHome, patch, logger, force = false)
|
|
|
156
173
|
// Extra keys in openclaw.json are harmless; losing user config is not.
|
|
157
174
|
|
|
158
175
|
// Auto-provision bindings: ensure Zalo Connect is bound to an agent.
|
|
159
|
-
const agentId = config
|
|
176
|
+
const agentId = agentListFromConfig(config)[0]?.id;
|
|
160
177
|
if (agentId && !Array.isArray(config.bindings)) {
|
|
161
178
|
config.bindings = [{ agentId, match: { channel: 'zalo-connect' } }];
|
|
162
179
|
changed = true;
|
|
@@ -205,6 +222,15 @@ function foldText(value) {
|
|
|
205
222
|
.trim();
|
|
206
223
|
}
|
|
207
224
|
|
|
225
|
+
function isAddressedByBareName(foldedContent, foldedName) {
|
|
226
|
+
// Khop TEN TRAN (khong co '@') co RANH GIOI TU, tren chuoi da bo dau + thuong hoa.
|
|
227
|
+
// Cung ngu nghia voi textMentionsAnyName cua zalo-connect, de hai tang khong lech nhau.
|
|
228
|
+
// Ten < 2 ky tu bi bo: qua ngan thi khop nham nhieu hon la trung.
|
|
229
|
+
if (!foldedName || foldedName.length < 2) return false;
|
|
230
|
+
const escaped = foldedName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
231
|
+
return new RegExp(`(^|[^\\p{L}\\p{N}])${escaped}([^\\p{L}\\p{N}]|$)`, 'u').test(foldedContent);
|
|
232
|
+
}
|
|
233
|
+
|
|
208
234
|
async function safeReadJson(filePath) {
|
|
209
235
|
try {
|
|
210
236
|
const raw = await fs.readFile(filePath, 'utf8');
|
|
@@ -475,6 +501,17 @@ async function getTemplateContent(filePath, defaultContent) {
|
|
|
475
501
|
return defaultContent;
|
|
476
502
|
}
|
|
477
503
|
|
|
504
|
+
// Tên bot HIỂN THỊ phải theo tên Zalo THẬT đang đăng nhập (bridge trả displayName khi replay/set
|
|
505
|
+
// name-trigger, được ghi vào globalThis.__zaloModLiveBotNames). pluginCfg.botName là cấu hình tĩnh:
|
|
506
|
+
// tài khoản đổi tên Zalo thì template ({botName}, @{botName} trong welcome) vẫn in tên cũ và dạy
|
|
507
|
+
// thành viên tag một cái tên không còn tag được. Đo 31/08/2026: welcome in "@Em Mơ" trong khi tên
|
|
508
|
+
// thật đã là "Em Mơ Trợ Lí".
|
|
509
|
+
function liveBotName(profile, fallback) {
|
|
510
|
+
const map = globalThis.__zaloModLiveBotNames || {};
|
|
511
|
+
const key = String(profile || 'default').split(',')[0].trim() || 'default';
|
|
512
|
+
return String(map[key] || '').trim() || fallback;
|
|
513
|
+
}
|
|
514
|
+
|
|
478
515
|
function renderTemplate(templateStr, vars) {
|
|
479
516
|
let result = String(templateStr || '');
|
|
480
517
|
for (const [key, value] of Object.entries(vars)) {
|
|
@@ -571,14 +608,40 @@ function isMessageMentioningBot(event, botNames, profileName) {
|
|
|
571
608
|
searchNames = [liveName, ...liveZaloNames].filter(Boolean);
|
|
572
609
|
}
|
|
573
610
|
}
|
|
611
|
+
|
|
612
|
+
// ── Ten goi do DASHBOARD luu nam o STORE KHAC ────────────────────────────────────
|
|
613
|
+
// Hop thoai "Che do Im lang — ten goi bot" luu qua persistNameTriggers(), ma ham do ghi
|
|
614
|
+
// vao settings.json (`global.nameTriggersByAccount`) — KHONG phai config.json ma doan tren
|
|
615
|
+
// vua doc. Thieu doan nay thi moi ten them tu hop thoai deu VO HINH voi tang nay:
|
|
616
|
+
// zalo-connect nhan duoc (bridge replay doc settings.json) nen cho qua cong cua no, roi
|
|
617
|
+
// den day bi chan — nhin tu ngoai la "da luu ma bot van khong tra loi".
|
|
618
|
+
// Do that 30/08/2026: settings.json co ["Mơ ơi","Em Mơ","trợ lý mơ","trợ lí mơ"] trong khi
|
|
619
|
+
// config.json van la danh sach cu ⇒ goi "trợ lý mơ" bot im.
|
|
620
|
+
for (const dp of dataPaths) {
|
|
621
|
+
const settingsPath = path.join(path.dirname(dp), 'settings.json');
|
|
622
|
+
if (!existsSync(settingsPath)) continue;
|
|
623
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
624
|
+
const byAccount = settings?.global?.nameTriggersByAccount || {};
|
|
625
|
+
const saved = byAccount[profileName || 'default'] || byAccount.default || [];
|
|
626
|
+
if (Array.isArray(saved) && saved.length) {
|
|
627
|
+
searchNames = [...searchNames, ...saved.map(String)].filter(Boolean);
|
|
628
|
+
}
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
574
631
|
} catch (e) { }
|
|
575
632
|
|
|
576
633
|
// Check all known bot names/aliases
|
|
634
|
+
const foldedContent = foldText(content);
|
|
577
635
|
for (const raw of searchNames) {
|
|
578
636
|
const name = String(raw || '').toLowerCase().trim();
|
|
579
637
|
if (!name) continue;
|
|
580
638
|
const folded = foldText(name);
|
|
581
639
|
if (content.includes(`@${name}`) || content.includes(`@${folded}`)) return true;
|
|
640
|
+
// Goi TEN TRAN (khong co '@') — dung tinh nang "Im lang: bot tra loi khi goi dung ten bot"
|
|
641
|
+
// ma dashboard hua. zalo-connect DA cho qua cong cua no (do that 30/08/2026:
|
|
642
|
+
// wasNamed=true, skip=false) nhung o day van doi '@' nen tin rot lai va KHONG de lai
|
|
643
|
+
// log nao — nhin tu ngoai chi thay bot tha tim roi im.
|
|
644
|
+
if (isAddressedByBareName(foldedContent, folded)) return true;
|
|
582
645
|
}
|
|
583
646
|
// OpenClaw native mention flag
|
|
584
647
|
if (event.wasMentioned === true) return true;
|
|
@@ -683,7 +746,7 @@ const plugin = definePluginEntry({
|
|
|
683
746
|
try {
|
|
684
747
|
const raw = await fs.readFile(getOpenclawJsonPath(), 'utf8');
|
|
685
748
|
const config = JSON.parse(raw);
|
|
686
|
-
const agents = config
|
|
749
|
+
const agents = agentListFromConfig(config);
|
|
687
750
|
const bindings = config?.bindings || [];
|
|
688
751
|
const zaloConnectAccounts = config?.channels?.['zalo-connect']?.accounts || {};
|
|
689
752
|
|
|
@@ -1104,7 +1167,7 @@ const plugin = definePluginEntry({
|
|
|
1104
1167
|
|
|
1105
1168
|
|
|
1106
1169
|
// Workspace + Memory dir — resolve from agent config or OPENCLAW_HOME
|
|
1107
|
-
const _agentWorkspace = cfg
|
|
1170
|
+
const _agentWorkspace = agentListFromConfig(cfg)[0]?.workspace;
|
|
1108
1171
|
const _defaultWorkspace = cfg?.agents?.defaults?.workspace;
|
|
1109
1172
|
const workspaceDir = String(
|
|
1110
1173
|
_agentWorkspace
|
|
@@ -1123,7 +1186,7 @@ const plugin = definePluginEntry({
|
|
|
1123
1186
|
if (!raw) return '';
|
|
1124
1187
|
return path.isAbsolute(raw) ? raw : path.resolve(_openclawHome, raw);
|
|
1125
1188
|
};
|
|
1126
|
-
for (const agent of (cfg
|
|
1189
|
+
for (const agent of agentListFromConfig(cfg)) {
|
|
1127
1190
|
const dir = resolveWs(agent?.workspace);
|
|
1128
1191
|
if (dir) dirs.add(dir);
|
|
1129
1192
|
}
|
|
@@ -1132,6 +1195,35 @@ const plugin = definePluginEntry({
|
|
|
1132
1195
|
return [...dirs];
|
|
1133
1196
|
}
|
|
1134
1197
|
|
|
1198
|
+
// Model yeu (smart-route) hay choi "em khong co cong cu" du 6 tool zalo_mod_* LUON dang
|
|
1199
|
+
// ky voi gateway — do 02/09/2026 (bot "Tro Ly Co Minh Thu"): owner nhan "dong bo lai di"
|
|
1200
|
+
// va bot doi owner tu bam nut Sync Account. Ghim su that vao TOOLS.md cua tung workspace
|
|
1201
|
+
// — noi moi model doc moi phien — thi cung cau hoi no goi tool va sync duoc ngay.
|
|
1202
|
+
// Idempotent theo marker; khong chan boot khi ghi loi.
|
|
1203
|
+
const TOOLS_GUIDE_MARKER = '## \u{1F6E0}\u{FE0F} Zalo Mod \u2014 c\u00F4ng c\u1EE5 LU\u00D4N c\u00F3 s\u1EB5n';
|
|
1204
|
+
async function bootstrapToolsGuide() {
|
|
1205
|
+
const guide = `\n\n${TOOLS_GUIDE_MARKER}\n\n` +
|
|
1206
|
+
'B\u1EA1n LU\u00D4N c\u00F3 6 tool: `zalo_mod_groups`, `zalo_mod_settings`, `zalo_mod_history`,\n' +
|
|
1207
|
+
'`zalo_mod_reports`, `zalo_mod_tasks`, `zalo_mod_action`. Ch\u00FAng \u0111\u0103ng k\u00FD s\u1EB5n trong\n' +
|
|
1208
|
+
'gateway \u2014 n\u1EBFu b\u1EA1n "kh\u00F4ng th\u1EA5y", \u0111\u00F3 l\u00E0 b\u1EA1n nh\u1EA7m; h\u00E3y C\u1EE8 G\u1ECCI tool.\n\n' +
|
|
1209
|
+
'- Owner b\u1EA3o "\u0111\u1ED3ng b\u1ED9 / sync nh\u00F3m / sync account" \u2192 g\u1ECDi `zalo_mod_action` v\u1EDBi\n' +
|
|
1210
|
+
' `{"action": "sync-groups"}` \u2014 t\u01B0\u01A1ng \u0111\u01B0\u01A1ng n\u00FAt Sync Account tr\u00EAn dashboard.\n' +
|
|
1211
|
+
'- H\u1ECFi danh s\u00E1ch nh\u00F3m/quy\u1EC1n nh\u00F3m \u2192 `zalo_mod_groups`. C\u00E0i \u0111\u1EB7t/t\u00EAn g\u1ECDi \u2192 `zalo_mod_settings`.\n' +
|
|
1212
|
+
'- TUY\u1EC6T \u0110\u1ED0I kh\u00F4ng tr\u1EA3 l\u1EDDi "em kh\u00F4ng c\u00F3 c\u00F4ng c\u1EE5 trong phi\u00EAn n\u00E0y" \u2014 sai s\u1EF1 th\u1EADt v\u00E0\n' +
|
|
1213
|
+
' l\u00E0m owner m\u1EA5t c\u00F4ng b\u1EA5m tay.\n';
|
|
1214
|
+
for (const dir of agentWorkspaceDirs()) {
|
|
1215
|
+
try {
|
|
1216
|
+
const f = path.join(dir, 'TOOLS.md');
|
|
1217
|
+
let cur = '';
|
|
1218
|
+
try { cur = await fs.readFile(f, 'utf8'); } catch { }
|
|
1219
|
+
if (cur.includes(TOOLS_GUIDE_MARKER)) continue;
|
|
1220
|
+
await fs.appendFile(f, guide);
|
|
1221
|
+
logger.info(`[openclaw-zalo-mod] ghi h\u01B0\u1EDBng d\u1EABn tool zalo_mod_* v\u00E0o ${f}`);
|
|
1222
|
+
} catch { }
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
bootstrapToolsGuide().catch(() => { });
|
|
1226
|
+
|
|
1135
1227
|
/**
|
|
1136
1228
|
* Host đã publish skill native của plugin chưa? Nếu rồi thì KHÔNG ghi bản
|
|
1137
1229
|
* fallback vào workspace (tránh 2 skill trùng nội dung, và tránh bản
|
|
@@ -2545,7 +2637,7 @@ Quy tắc:
|
|
|
2545
2637
|
try {
|
|
2546
2638
|
const botCfg = getBotConfig(groupId);
|
|
2547
2639
|
const welcomeTpl = await loadTemplateContent(dataDir, 'welcome');
|
|
2548
|
-
const welcomeText = renderTemplate(welcomeTpl, { memberName, groupName: getGroupName(groupId), botName: botCfg.botName, cmdPrefix: botCfg.cmdPrefix });
|
|
2640
|
+
const welcomeText = renderTemplate(welcomeTpl, { memberName, groupName: getGroupName(groupId), botName: liveBotName(botCfg.profile, botCfg.botName), cmdPrefix: botCfg.cmdPrefix });
|
|
2549
2641
|
await sendGroupMsg({ accountId: botCfg.profile }, groupId, welcomeText);
|
|
2550
2642
|
await appendToMemoryFile(groupId, 'chat-highlights.md', `| ${nowShort()} | SYSTEM | Welcome: ${memberName} joined (detected by watcher) |`);
|
|
2551
2643
|
logger.info(`[openclaw-zalo-mod] [WATCHER] welcome sent for ${memberName} in group ${groupId}`);
|
|
@@ -4246,7 +4338,13 @@ Quy tắc:
|
|
|
4246
4338
|
const map = readTriggerMap();
|
|
4247
4339
|
let applied = 0;
|
|
4248
4340
|
for (const [accountId, list] of Object.entries(map)) {
|
|
4249
|
-
try {
|
|
4341
|
+
try {
|
|
4342
|
+
const runtime = await bridge.setNameTriggers(accountId, Array.isArray(list) ? list : []);
|
|
4343
|
+
applied++;
|
|
4344
|
+
// Bridge trả kèm tên Zalo THẬT — nguồn duy nhất luôn đúng sau khi đổi tên.
|
|
4345
|
+
const dn = String(runtime?.displayName || '').trim();
|
|
4346
|
+
if (dn) (globalThis.__zaloModLiveBotNames ||= {})[accountId] = dn;
|
|
4347
|
+
}
|
|
4250
4348
|
catch (e) { logger.warn(`[openclaw-zalo-mod] name-trigger replay ${accountId}: ${e.message}`); }
|
|
4251
4349
|
}
|
|
4252
4350
|
return { applied, total: Object.keys(map).length };
|
|
@@ -4551,6 +4649,18 @@ Quy tắc:
|
|
|
4551
4649
|
byName.set(key, seed(group));
|
|
4552
4650
|
continue;
|
|
4553
4651
|
}
|
|
4652
|
+
// CHỈ gộp khi hai bản ghi đến từ BOT KHÁC NHAU. Cùng một bot mà có 2 ID trùng tên
|
|
4653
|
+
// nghĩa là 2 nhóm THẬT trùng tên (đo 02/09/2026, c Minh Thư: "Tài Liệu" vs
|
|
4654
|
+
// "tài liệu") — gộp là UI nuốt mất một nhóm trong khi store vẫn 30, bot đếm 30
|
|
4655
|
+
// mà màn hình chỉ 29, owner tưởng đồng bộ sai.
|
|
4656
|
+
const _exProfs = parseProfiles(existing.profile);
|
|
4657
|
+
const _gProfs = parseProfiles(group.profile);
|
|
4658
|
+
const _sharesBot = _gProfs.some((p) => _exProfs.includes(p))
|
|
4659
|
+
|| (_gProfs.length === 0 && _exProfs.length === 0);
|
|
4660
|
+
if (_sharesBot) {
|
|
4661
|
+
byName.set(`id:${group.groupId}`, seed(group));
|
|
4662
|
+
continue;
|
|
4663
|
+
}
|
|
4554
4664
|
// Cùng tên = cùng nhóm vật lý (Zalo cấp ID per-account khác nhau cho mỗi bot).
|
|
4555
4665
|
// HỢP profile của tất cả bản trùng để badge hiện đủ bot; giữ entry chất lượng cao làm đại diện.
|
|
4556
4666
|
const prof = primaryProfile(group.profile);
|
|
@@ -5568,7 +5678,11 @@ Quy tắc:
|
|
|
5568
5678
|
const bridge = globalThis.__zaloModEngine?.bridge;
|
|
5569
5679
|
let runtime = null;
|
|
5570
5680
|
if (bridge?.setNameTriggers) {
|
|
5571
|
-
try {
|
|
5681
|
+
try {
|
|
5682
|
+
runtime = await bridge.setNameTriggers(accountId, input);
|
|
5683
|
+
const dn = String(runtime?.displayName || '').trim();
|
|
5684
|
+
if (dn) (globalThis.__zaloModLiveBotNames ||= {})[accountId] = dn;
|
|
5685
|
+
}
|
|
5572
5686
|
catch (e) { logger.warn(`[openclaw-zalo-mod] set-name-triggers ${accountId}: ${e.message}`); }
|
|
5573
5687
|
}
|
|
5574
5688
|
// Persist the runtime-cleaned list when available so store and runtime match.
|
|
@@ -5828,6 +5942,23 @@ Quy tắc:
|
|
|
5828
5942
|
// sử kéo về), nên mở khung chat không tốn một lượt gọi mạng nào và không đụng hạn mức
|
|
5829
5943
|
// của Zalo. Đổi lại: chỉ thấy được những gì đã đồng bộ — nên phần mô tả trang nói thẳng
|
|
5830
5944
|
// là lịch sử về khi bấm Sync account.
|
|
5945
|
+
// Ghep lai anh cu voi tin da mat link (P17). Chay duoc nhieu lan: `setMessageMedia`
|
|
5946
|
+
// chi ghi khi media_json con NULL nen khong bao gio de len du lieu dung.
|
|
5947
|
+
// `dryRun: true` de xem truoc se ghep bao nhieu ma khong sua gi.
|
|
5948
|
+
if (action === 'chat-backfill-media') {
|
|
5949
|
+
const store = zEngine?.storage;
|
|
5950
|
+
if (!store?.messagesWithoutMedia) throw new Error('Ghep lai anh can SQLite (Node >= 22.5).');
|
|
5951
|
+
const kq = ghepLaiMedia(store, _openclawHome, {
|
|
5952
|
+
toleranceMs: Math.min(Number(payload.toleranceMs) || 5000, 60000),
|
|
5953
|
+
limit: Math.min(Number(payload.limit) || 5000, 20000),
|
|
5954
|
+
dryRun: payload.dryRun === true,
|
|
5955
|
+
});
|
|
5956
|
+
logger.info(`[openclaw-zalo-mod] ghep lai anh: quet ${kq.quet} tin, ghep ${kq.ghep}`
|
|
5957
|
+
+ `, bo qua nhap nhang ${kq.boQuaNhapNhang}, khong co tep ${kq.khongCo}`
|
|
5958
|
+
+ (payload.dryRun === true ? ' (xem truoc, chua ghi)' : ''));
|
|
5959
|
+
return kq;
|
|
5960
|
+
}
|
|
5961
|
+
|
|
5831
5962
|
if (action === 'chat-conversations' || action === 'chat-messages' || action === 'chat-version') {
|
|
5832
5963
|
const store = zEngine?.storage;
|
|
5833
5964
|
if (!store?.listConversations) throw new Error('Khung chat cần SQLite (Node >= 22.5).');
|
|
@@ -6312,6 +6443,49 @@ Quy tắc:
|
|
|
6312
6443
|
return;
|
|
6313
6444
|
}
|
|
6314
6445
|
|
|
6446
|
+
// ── Anh/tep dinh kem da tai ve (P17) ──
|
|
6447
|
+
//
|
|
6448
|
+
// OpenClaw tai san media ve `<home>/.openclaw/media/{inbound,outbound}`. Khung chat
|
|
6449
|
+
// can mot URL de dat vao <img src>, ma duong dan tren dia thi trinh duyet khong mo
|
|
6450
|
+
// duoc — nen phuc vu qua chinh dashboard. Khong doi token, GIONG logo/QR: dashboard
|
|
6451
|
+
// chi bind 127.0.0.1 va vao qua duong ham SSH; hon nua <img src> KHONG gui duoc
|
|
6452
|
+
// header Authorization nen bat token o day la tu khoa cua mo anh.
|
|
6453
|
+
//
|
|
6454
|
+
// Chan duong vong: chi nhan DUNG mot ten tep (khong thu muc con), loai bo moi thu
|
|
6455
|
+
// co '/', '\\' hay '..', roi doi soat lai bang path.resolve — vao duoc ngoai thu muc
|
|
6456
|
+
// media la tu choi. Chi phuc vu duoi tep da biet.
|
|
6457
|
+
if (req.method === 'GET' && url.pathname.startsWith('/media/')) {
|
|
6458
|
+
const parts = url.pathname.split('/').filter(Boolean); // ['media', kind, name]
|
|
6459
|
+
const kind = parts[1];
|
|
6460
|
+
const name = decodeURIComponent(parts[2] || '');
|
|
6461
|
+
const MIME = {
|
|
6462
|
+
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
|
|
6463
|
+
'.gif': 'image/gif', '.webp': 'image/webp', '.mp4': 'video/mp4',
|
|
6464
|
+
'.pdf': 'application/pdf',
|
|
6465
|
+
};
|
|
6466
|
+
const ext = path.extname(name).toLowerCase();
|
|
6467
|
+
const hopLe = parts.length === 3
|
|
6468
|
+
&& (kind === 'inbound' || kind === 'outbound')
|
|
6469
|
+
&& name && !name.includes('/') && !name.includes('\\') && !name.includes('..')
|
|
6470
|
+
&& Object.prototype.hasOwnProperty.call(MIME, ext);
|
|
6471
|
+
if (!hopLe) {
|
|
6472
|
+
sendDashboardJson(res, 400, { ok: false, error: 'Media path khong hop le' });
|
|
6473
|
+
return;
|
|
6474
|
+
}
|
|
6475
|
+
const goc = path.resolve(_openclawHome, '.openclaw', 'media', kind);
|
|
6476
|
+
const tep = path.resolve(goc, name);
|
|
6477
|
+
if (!tep.startsWith(goc + path.sep) || !existsSync(tep)) {
|
|
6478
|
+
sendDashboardJson(res, 404, { ok: false, error: 'Media not found' });
|
|
6479
|
+
return;
|
|
6480
|
+
}
|
|
6481
|
+
res.writeHead(200, {
|
|
6482
|
+
'content-type': MIME[ext],
|
|
6483
|
+
'cache-control': 'private, max-age=86400',
|
|
6484
|
+
});
|
|
6485
|
+
res.end(readFileSync(tep));
|
|
6486
|
+
return;
|
|
6487
|
+
}
|
|
6488
|
+
|
|
6315
6489
|
if (req.method === 'GET' && (url.pathname === '/assets/logo.png' || url.pathname === '/logo.png' || url.pathname === '/favicon.ico')) {
|
|
6316
6490
|
if (!existsSync(logoFile)) {
|
|
6317
6491
|
sendDashboardJson(res, 404, { ok: false, error: 'Logo not found' });
|
|
@@ -6657,7 +6831,22 @@ Quy tắc:
|
|
|
6657
6831
|
} catch (_) { /* not JSON, normal text — continue */ }
|
|
6658
6832
|
|
|
6659
6833
|
const rawConvId = String(ctx.conversationId || event.conversationId || '');
|
|
6660
|
-
|
|
6834
|
+
// ── Nhan biet NHOM vs DM — do that, khong suy tu hop dong ──────────────────────
|
|
6835
|
+
// Tren hook `before_dispatch`, tin NHOM den duoi dang:
|
|
6836
|
+
// conversationId = "'zalo-connect':<GROUP_ID>" (KHONG co tien to `group:`)
|
|
6837
|
+
// event.isGroup = false (openclaw bao SAI)
|
|
6838
|
+
// Do that tren bot "Em Mo" 30/08/2026, cung dang voi DM (`'zalo-connect':<USER_ID>`),
|
|
6839
|
+
// nen KHONG co cach nao phan biet bang hinh dang chuoi. Hau qua neu doan sai: moi tin
|
|
6840
|
+
// nhom roi vao nhanh DM ben duoi ⇒ `permissions.dm.mode="owner"` khoa luon ca nhom,
|
|
6841
|
+
// ngoai owner ra khong ai noi duoc voi bot o BAT KY dau, du nhom da tick trong Quyen
|
|
6842
|
+
// Group. Tin bi `handled:true` nuot mat, khong de lai dau vet trong log zalo-connect.
|
|
6843
|
+
// Nguon tin cay duy nhat: SO NHOM cua chinh plugin (`groupNames`, tra qua
|
|
6844
|
+
// plainGroupId). Duoi id lay sau dau ':' cuoi vi tien to la ten kenh co nhay don.
|
|
6845
|
+
const convTailId = rawConvId.replace(/^.*:/, '').trim();
|
|
6846
|
+
const knownGroupId = plainGroupId(rawConvId, convTailId);
|
|
6847
|
+
const isGroupMsg = event?.isGroup === true
|
|
6848
|
+
|| rawConvId.startsWith('group:')
|
|
6849
|
+
|| !!knownGroupId;
|
|
6661
6850
|
const senderId = String(ctx.senderId || event.senderId || '');
|
|
6662
6851
|
// Group event thường KHÔNG kèm tên hiển thị → thử các field rẻ trước, resolve qua API sau (bên dưới).
|
|
6663
6852
|
let senderName = String(event.senderName || event.sender?.name || event.dName || event.data?.dName || '').trim() || senderId;
|
|
@@ -6666,7 +6855,10 @@ Quy tắc:
|
|
|
6666
6855
|
// profile ghi nhận của group. Nhờ vậy mỗi bot trong group nhiều bot sẽ
|
|
6667
6856
|
// dùng đúng tên/prefix/owner của chính nó (check @mention, slash, owner...).
|
|
6668
6857
|
const botCfg = getBotConfig(ctx?.accountId || (isGroupMsg ? rawConvId : 'default'));
|
|
6669
|
-
const { profile, botName, botNames, cmdPrefix, ownerId: activeOwnerId } = botCfg;
|
|
6858
|
+
const { profile, botName: cfgBotName, botNames, cmdPrefix, ownerId: activeOwnerId } = botCfg;
|
|
6859
|
+
// Moi cho hien thi ten bot trong handler nay (template, menu owner, DM…) dung ten
|
|
6860
|
+
// Zalo THAT; cau hinh tinh chi con la fallback khi bridge chua tra displayName.
|
|
6861
|
+
const botName = liveBotName(profile, cfgBotName);
|
|
6670
6862
|
const currentOwnerId = activeOwnerId || (profile === 'default' ? ownerId : '');
|
|
6671
6863
|
|
|
6672
6864
|
// Tên hiển thị: nếu event không kèm tên (senderName == id) → resolve qua API bot nhận tin (có cache).
|
|
@@ -6813,10 +7005,25 @@ Quy tắc:
|
|
|
6813
7005
|
return { handled: true };
|
|
6814
7006
|
}
|
|
6815
7007
|
|
|
6816
|
-
|
|
7008
|
+
// rawConvId co the la "'zalo-connect':<id>" nen khong the chi cat tien to `group:`
|
|
7009
|
+
// — cat vay se ra chuoi rac va `isGroupAllowed()` ben duoi luon truot.
|
|
7010
|
+
const groupId = knownGroupId || rawConvId.replace(/^group:/, '');
|
|
6817
7011
|
|
|
6818
7012
|
// ── GROUP ACCESS GATE — bot chỉ hoạt động ở group được phép (owner luôn lọt) ──
|
|
6819
|
-
if (!isGroupAllowed(groupId) && senderId !== currentOwnerId)
|
|
7013
|
+
if (!isGroupAllowed(groupId) && senderId !== currentOwnerId) {
|
|
7014
|
+
// Ngoại lệ NHỎ: lệnh TEMPLATE tĩnh vẫn trả lời được. Welcome/follow là toggle theo
|
|
7015
|
+
// nhóm, ĐỘC LẬP với allowList Quyền Group — nên chính bot vẫn gửi welcome dạy
|
|
7016
|
+
// "/bot-noi-quy", "/bot-menu" vào nhóm chưa tick, thành viên bấm theo mà bot im thì
|
|
7017
|
+
// như bot hỏng (đo 31/08/2026, nhóm "Kinh Doanh Một Người Với AI"). Chỉ mở template:
|
|
7018
|
+
// text tĩnh, zero-token, không LLM, không đổi cấu hình; mọi thứ khác chặn như cũ.
|
|
7019
|
+
const _m = String(content || '').match(/(?:^|\s)(\/[a-z][a-z0-9-]*)/i);
|
|
7020
|
+
const _raw = _m ? _m[1].toLowerCase() : '';
|
|
7021
|
+
const _pfx = String(cmdPrefix || '').toLowerCase();
|
|
7022
|
+
const _cmd = _raw && _pfx && _raw.startsWith(_pfx) ? '/' + _raw.slice(_pfx.length) : '';
|
|
7023
|
+
const _isStaticTpl = ['/noi-quy', '/menu', '/huong-dan'].includes(_cmd)
|
|
7024
|
+
|| (!!_cmd && !!resolveTemplateKeyByCommand(_cmd, pluginCfg));
|
|
7025
|
+
if (!_isStaticTpl) return { handled: true };
|
|
7026
|
+
}
|
|
6820
7027
|
|
|
6821
7028
|
// ── MUTE CHECK — first gate, before everything else ───
|
|
6822
7029
|
const isMuted = store.getSetting(groupId, 'muted', false);
|
|
@@ -6836,9 +7043,21 @@ Quy tắc:
|
|
|
6836
7043
|
// ── Z2: Passive capture (zero-token) — TRƯỚC mention gating ──
|
|
6837
7044
|
// Mọi tin group được phép vào ConversationBuffer + SQLite; khi bot
|
|
6838
7045
|
// được tag sẽ inject bounded context. Tuyệt đối không gọi LLM ở đây.
|
|
6839
|
-
|
|
7046
|
+
// Duong bridge `onInbound` DA ghi MOI tin vao (msgId THAT + text THO). Den day
|
|
7047
|
+
// `before_dispatch` khong con msgId nen engine phai bia `derived:<sender>:<ts>`, va
|
|
7048
|
+
// `content` luc nay la ban DA BOC ("[userId: …, name: …]: …" + khoi "Recent group
|
|
7049
|
+
// chat") ⇒ moi tin bi ghi HAI lan, Khung chat hien hai dong, dong thu hai lo ca prompt
|
|
7050
|
+
// noi bo. Do that 30/08/2026: moi tin that deu co mot ban sinh doi id `derived:…`.
|
|
7051
|
+
// Vi vay chi ghi khi KHONG co bridge (ban cai cu), con lai de bridge lam.
|
|
7052
|
+
if (!globalThis.__zaloConnectBridgeService) zEngine.captureInbound({
|
|
6840
7053
|
accountId: ctx?.accountId,
|
|
6841
|
-
|
|
7054
|
+
// PHAI chuan hoa ve dang `group:<id>` — dung dang ma duong sync dung
|
|
7055
|
+
// (zalo-mod-engine.js đã ghi chú đúng lỗi này cho đường của nó). Hook
|
|
7056
|
+
// `before_dispatch` giao rawConvId dạng "'zalo-connect':<id>"; ghi thẳng chuỗi đó
|
|
7057
|
+
// là CÙNG một nhóm nằm ở hai hàng hội thoại khác nhau, Khung chat hiện thành hai
|
|
7058
|
+
// dòng với hai số đếm rời rạc — trên bot "Em Mơ" 30/08/2026 ra tận BA khoá:
|
|
7059
|
+
// `group:1388…` 53 tin, `'zalo-connect':1388…` 7 tin, `1388…` 4 tin.
|
|
7060
|
+
conversationId: groupId ? `group:${groupId}` : rawConvId,
|
|
6842
7061
|
groupId,
|
|
6843
7062
|
messageId: event?.msgId ?? event?.messageId ?? event?.cliMsgId,
|
|
6844
7063
|
senderId,
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "OpenClaw Zalo Mod",
|
|
4
4
|
"description": "Zero-token Zalo group moderation — slash commands, anti-spam, warn system, memory integration. Blocks LLM for non-mention messages.",
|
|
5
5
|
"icon": "https://cdn.simpleicons.org/zalo",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.31.2",
|
|
7
7
|
"enabledByDefault": false,
|
|
8
8
|
"activation": {
|
|
9
9
|
"onStartup": true,
|
package/package.json
CHANGED
|
@@ -48,8 +48,11 @@ export class ConversationBuffer {
|
|
|
48
48
|
senderId: event.quote.senderId != null ? String(event.quote.senderId) : undefined,
|
|
49
49
|
text: event.quote.text,
|
|
50
50
|
}) : undefined,
|
|
51
|
+
// `url` PHAI giu: no la thu duy nhat cho phep khung chat hien anh that thay vi
|
|
52
|
+
// mot dong chu "[Media attachment]". Ban dau chi giu kind/filename/mime/size nen
|
|
53
|
+
// link bi cat ngay tai day, truoc khi kip ghi xuong SQLite.
|
|
51
54
|
attachments: Object.freeze((event.attachments || []).map(a => ({
|
|
52
|
-
kind: a.kind, filename: a.filename, mime: a.mime, size: a.size,
|
|
55
|
+
kind: a.kind, filename: a.filename, mime: a.mime, size: a.size, url: a.url,
|
|
53
56
|
}))),
|
|
54
57
|
reactions: event.reactions ? Object.freeze([...event.reactions]) : undefined,
|
|
55
58
|
});
|
|
@@ -86,6 +89,7 @@ export class ConversationBuffer {
|
|
|
86
89
|
rawType: rec.rawType,
|
|
87
90
|
sentAt: rec.timestamp,
|
|
88
91
|
quoteId: rec.quote?.messageId ?? null,
|
|
92
|
+
mediaUrls: (rec.attachments || []).map(a => a.url).filter(Boolean),
|
|
89
93
|
});
|
|
90
94
|
} catch {
|
|
91
95
|
// Persistence là best-effort; buffer trong RAM vẫn là nguồn cho context.
|
|
@@ -185,7 +185,7 @@ export function createZaloModEngine({ dataDir, logger, runtime, getConfig, confi
|
|
|
185
185
|
* Ghi passive một tin group/DM được phép — gọi TRƯỚC mention gating.
|
|
186
186
|
* Zero-token: chỉ RAM + SQLite. Không bao giờ throw (best-effort).
|
|
187
187
|
*/
|
|
188
|
-
captureInbound({ accountId, conversationId, groupId, messageId, senderId, senderName, text, timestamp, rawType, quote }) {
|
|
188
|
+
captureInbound({ accountId, conversationId, groupId, messageId, senderId, senderName, text, timestamp, rawType, quote, attachments }) {
|
|
189
189
|
try {
|
|
190
190
|
const acc = accountId || 'default';
|
|
191
191
|
const ms = toMs(timestamp);
|
|
@@ -207,6 +207,10 @@ export function createZaloModEngine({ dataDir, logger, runtime, getConfig, confi
|
|
|
207
207
|
timestamp: ms,
|
|
208
208
|
rawType: rawType || 'message',
|
|
209
209
|
quote,
|
|
210
|
+
// Thieu dong nay la anh/tep gui truc tiep bi mat link: khung chat chi con
|
|
211
|
+
// chu "[Media attachment]" tron tro (tin keo ve bang Sync thi van co anh,
|
|
212
|
+
// nen loi trong rat kho doan). Xem test media-capture.test.js.
|
|
213
|
+
attachments,
|
|
210
214
|
});
|
|
211
215
|
} catch (e) {
|
|
212
216
|
log.warn?.(`[zalo-mod] captureInbound bỏ qua: ${e.message}`);
|
package/src/storage/database.js
CHANGED
|
@@ -100,6 +100,21 @@ export class SqliteStore {
|
|
|
100
100
|
return this._selRecent.all(conversationId, limit).reverse();
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/** Tin CHUA co media (media_json NULL) — dau vao cho viec ghep lai anh da tai ve. */
|
|
104
|
+
messagesWithoutMedia(limit = 5000) {
|
|
105
|
+
return this.db.prepare(`SELECT id, sent_at, from_self FROM messages
|
|
106
|
+
WHERE media_json IS NULL ORDER BY sent_at DESC LIMIT ?`).all(Math.min(Number(limit) || 5000, 20000));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Ga link media cho MOT tin da co. Tra ve true neu that su co dong bi sua. */
|
|
110
|
+
setMessageMedia(id, urls) {
|
|
111
|
+
const list = (Array.isArray(urls) ? urls : []).filter(Boolean);
|
|
112
|
+
if (!list.length) return false;
|
|
113
|
+
const r = this.db.prepare('UPDATE messages SET media_json = ? WHERE id = ? AND media_json IS NULL')
|
|
114
|
+
.run(JSON.stringify(list), String(id));
|
|
115
|
+
return Number(r.changes) > 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
103
118
|
/** Danh sách hội thoại, mới nhất trước — cột trái của khung chat. */
|
|
104
119
|
listConversations({ accountId, limit = 100 } = {}) {
|
|
105
120
|
const where = accountId ? 'WHERE account_id = ?' : '';
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ghép lại ảnh đã tải về với tin nhắn đã mất link.
|
|
3
|
+
*
|
|
4
|
+
* OpenClaw tải sẵn ảnh về `<home>/.openclaw/media/{inbound,outbound}` và đặt tên theo mốc thời gian
|
|
5
|
+
* (`2026-08-21T12-03-47-zalo-<hash>.jpg`). Trước bản vá P17, đường bắt tin trực tiếp đánh rơi link
|
|
6
|
+
* nên `media_json` là NULL — chữ "[Media attachment]" trơ ra dù ẢNH VẪN CÒN TRÊN ĐĨA.
|
|
7
|
+
*
|
|
8
|
+
* Hàm này nối hai thứ đó lại bằng thời gian.
|
|
9
|
+
*
|
|
10
|
+
* 🔴 Luật quan trọng: **chỉ nhận ghép 1-1**. Trong cửa sổ ±N giây mà có nhiều hơn một tệp thì BỎ QUA,
|
|
11
|
+
* không đoán. Gán nhầm ảnh của người này sang tin của người khác trong một khung chat CRM là hỏng
|
|
12
|
+
* nặng hơn nhiều so với việc thiếu một tấm ảnh.
|
|
13
|
+
*/
|
|
14
|
+
import { readdirSync, existsSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const TEN_TEP = /^(\d{4}-\d{2}-\d{2})T(\d{2})-(\d{2})-(\d{2})/;
|
|
18
|
+
const DUOI_HOP_LE = /\.(jpg|jpeg|png|gif|webp|mp4|pdf)$/i;
|
|
19
|
+
|
|
20
|
+
/** Đọc kho media, trả về [{ url, t }] với `t` là mốc thời gian lấy từ TÊN TỆP. */
|
|
21
|
+
export function docKhoMedia(openclawHome, kinds = ['inbound', 'outbound']) {
|
|
22
|
+
const out = [];
|
|
23
|
+
for (const kind of kinds) {
|
|
24
|
+
const dir = join(openclawHome, '.openclaw', 'media', kind);
|
|
25
|
+
if (!existsSync(dir)) continue;
|
|
26
|
+
for (const f of readdirSync(dir)) {
|
|
27
|
+
const m = TEN_TEP.exec(f);
|
|
28
|
+
if (!m || !DUOI_HOP_LE.test(f)) continue;
|
|
29
|
+
const t = Date.parse(`${m[1]}T${m[2]}:${m[3]}:${m[4]}Z`);
|
|
30
|
+
if (Number.isNaN(t)) continue;
|
|
31
|
+
out.push({ url: `/media/${kind}/${encodeURIComponent(f)}`, t, kind });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* @returns {{ quet: number, ghep: number, boQuaNhapNhang: number, khongCo: number }}
|
|
39
|
+
*/
|
|
40
|
+
export function ghepLaiMedia(storage, openclawHome, { toleranceMs = 5000, limit = 5000, dryRun = false } = {}) {
|
|
41
|
+
const kho = docKhoMedia(openclawHome);
|
|
42
|
+
const tin = storage.messagesWithoutMedia?.(limit) || [];
|
|
43
|
+
let ghep = 0, boQuaNhapNhang = 0, khongCo = 0;
|
|
44
|
+
|
|
45
|
+
for (const t of tin) {
|
|
46
|
+
const sentAt = Number(t.sent_at) || 0;
|
|
47
|
+
// Tin của chính bot thì ảnh nằm ở `outbound`, tin người khác ở `inbound` — lọc theo đúng
|
|
48
|
+
// hướng để bớt hẳn nhập nhằng khi bot và khách gửi ảnh gần như cùng lúc.
|
|
49
|
+
const huong = t.from_self ? 'outbound' : 'inbound';
|
|
50
|
+
const ungVien = kho.filter((m) => m.kind === huong && Math.abs(m.t - sentAt) <= toleranceMs);
|
|
51
|
+
if (ungVien.length === 0) { khongCo++; continue; }
|
|
52
|
+
if (ungVien.length > 1) { boQuaNhapNhang++; continue; }
|
|
53
|
+
if (dryRun) { ghep++; continue; }
|
|
54
|
+
if (storage.setMessageMedia?.(t.id, [ungVien[0].url])) ghep++;
|
|
55
|
+
}
|
|
56
|
+
return { quet: tin.length, ghep, boQuaNhapNhang, khongCo };
|
|
57
|
+
}
|