levix-bot 3.3.0-rc.2 → 4.0.0-beta
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 +11 -7
- package/app.cjs +42 -45
- package/package.json +9 -1
- package/public/dashboard/assets/index-C1b7ijfu.js +391 -0
- package/public/dashboard/assets/index-pYaivHON.css +1 -0
- package/public/dashboard/index.html +19 -0
- package/public/dashboard.css +391 -66
- package/public/dashboard.js +593 -42
- package/scheduler.cjs +9 -14
- package/src/auth/use-database-auth-state.js +2 -4
- package/src/bootstrap/core.js +2 -5
- package/src/bootstrap/panel.js +10 -5
- package/src/cli.js +3 -5
- package/src/commands/autoschedule.cjs +1 -4
- package/src/commands/blacklist.cjs +22 -20
- package/src/commands/block.cjs +2 -4
- package/src/commands/calc.cjs +9 -16
- package/src/commands/digit.cjs +97 -0
- package/src/commands/gemini.cjs +147 -161
- package/src/commands/group/add.cjs +7 -16
- package/src/commands/group/antiSpam.cjs +5 -11
- package/src/commands/group/antilink.cjs +34 -28
- package/src/commands/group/approveall.cjs +3 -11
- package/src/commands/group/clearwarns.cjs +2 -5
- package/src/commands/group/demote.cjs +3 -8
- package/src/commands/group/kick.cjs +12 -15
- package/src/commands/group/media.cjs +17 -24
- package/src/commands/group/members.cjs +1 -2
- package/src/commands/group/promote.cjs +4 -12
- package/src/commands/group/removeall.cjs +2 -5
- package/src/commands/group/setpp.cjs +1 -1
- package/src/commands/group/setrules.cjs +2 -8
- package/src/commands/group/setwarn.cjs +4 -2
- package/src/commands/group/tagadmins.cjs +2 -5
- package/src/commands/group/warn.cjs +26 -24
- package/src/commands/group/warns.cjs +1 -2
- package/src/commands/group/welcome.cjs +13 -17
- package/src/commands/group.cjs +7 -10
- package/src/commands/help.cjs +6 -12
- package/src/commands/lang.cjs +104 -0
- package/src/commands/listschedules.cjs +2 -6
- package/src/commands/loop.cjs +6 -13
- package/src/commands/memory.cjs +19 -32
- package/src/commands/mod.cjs +97 -35
- package/src/commands/perm.cjs +18 -56
- package/src/commands/ping.cjs +1 -5
- package/src/commands/poll.cjs +1 -1
- package/src/commands/rand.cjs +2 -5
- package/src/commands/schedule.cjs +4 -8
- package/src/commands/score.cjs +8 -2
- package/src/commands/setprefix.cjs +2 -2
- package/src/commands/shortlink.cjs +7 -15
- package/src/commands/shutdown.cjs +1 -1
- package/src/commands/sticker.cjs +222 -0
- package/src/commands/stt.cjs +136 -68
- package/src/commands/todo.cjs +9 -15
- package/src/commands/tts.cjs +10 -16
- package/src/commands/unblock.cjs +2 -6
- package/src/commands/weather.cjs +5 -12
- package/src/config/ai-persona.md +5 -3
- package/src/config/available-models.json +1156 -0
- package/src/config/baileys.config.js +2 -1
- package/src/config/brand.esm.js +1 -10
- package/src/config/constants.js +1 -1
- package/src/config/defaults.cjs +66 -66
- package/src/config/lock.cjs +2 -2
- package/src/config/modelCapabilities.cjs +172 -0
- package/src/config/runtime-config.cjs +8 -13
- package/src/config/secrets.cjs +18 -10
- package/src/config/settings.cjs +94 -10
- package/src/config/vault.cjs +107 -0
- package/src/core/connection.js +13 -12
- package/src/core/events.js +41 -23
- package/src/core/proxy.js +7 -7
- package/src/core/session.js +91 -48
- package/src/core/socket.js +12 -15
- package/src/db/db.cjs +14 -6
- package/src/db/store.cjs +221 -87
- package/src/domain/caddy.js +1 -1
- package/src/domain/command.js +40 -19
- package/src/domain/detect.js +1 -1
- package/src/domain/system.js +2 -5
- package/src/handlers/command.handler.js +22 -43
- package/src/handlers/group.handler.js +13 -13
- package/src/handlers/message.handler.js +11 -21
- package/src/index.js +8 -4
- package/src/middleware/antispam.middleware.js +46 -23
- package/src/middleware/blacklist.middleware.js +14 -9
- package/src/middleware/forward-tracking.middleware.js +3 -2
- package/src/middleware/permissions.middleware.js +7 -10
- package/src/panel/login-throttle.cjs +43 -0
- package/src/panel/session-auth.cjs +17 -0
- package/src/routes/dashboard.api.esm.js +394 -72
- package/src/services/aiAgent.cjs +136 -49
- package/src/services/aiProviders.cjs +193 -47
- package/src/services/aiRouter.cjs +137 -0
- package/src/services/aiTools.cjs +305 -125
- package/src/services/llmDiscovery.cjs +481 -0
- package/src/services/modelRegistry.cjs +412 -0
- package/src/utils/datetime.cjs +2 -2
- package/src/utils/lid-helper.esm.js +7 -7
- package/src/utils/logger.cjs +1 -1
- package/src/utils/markdownParser.cjs +159 -0
- package/src/utils/memory.cjs +43 -13
- package/src/utils/messageContent.cjs +40 -0
- package/src/utils/normalizeJid.esm.js +7 -7
- package/src/utils/numerals.cjs +75 -0
- package/src/utils/permissions.esm.js +10 -15
- package/src/utils/providerUrl.cjs +281 -0
- package/src/utils/recurrence.cjs +7 -11
- package/src/utils/sendBotMessage.esm.js +4 -14
- package/src/utils/statusMessage.cjs +5 -13
- package/src/utils/storage.cjs +10 -0
- package/src/utils/storage.esm.js +11 -0
- package/src/utils/textDecode.cjs +4 -9
- package/src/utils/thumbnail.cjs +8 -14
- package/src/utils/typing.esm.js +12 -12
- package/views/dashboard.ejs +134 -106
package/src/commands/gemini.cjs
CHANGED
|
@@ -34,6 +34,7 @@ const {
|
|
|
34
34
|
} = require("../utils/storage-hub.cjs");
|
|
35
35
|
const { sendBotMessage, sendBotError } = require("../utils/sendBotMessage.cjs");
|
|
36
36
|
const { createStatus } = require("../utils/statusMessage.cjs");
|
|
37
|
+
const { formatMarkdownForWhatsApp } = require("../utils/markdownParser.cjs");
|
|
37
38
|
const {
|
|
38
39
|
isOwnerJidSync,
|
|
39
40
|
isBotAdminUserSync,
|
|
@@ -46,24 +47,68 @@ const {
|
|
|
46
47
|
sanitizeHistoryForFiles,
|
|
47
48
|
activeProviderKeySetting,
|
|
48
49
|
} = require("../services/aiAgent.cjs");
|
|
50
|
+
const { getProvider } = require("../services/aiRouter.cjs");
|
|
49
51
|
const { downloadContentFromMessage } = require("@whiskeysockets/baileys");
|
|
50
52
|
const fs = require("fs").promises;
|
|
51
53
|
const path = require("path");
|
|
54
|
+
const {
|
|
55
|
+
assertProviderBaseUrl,
|
|
56
|
+
assertProviderRequestUrl,
|
|
57
|
+
} = require("../utils/providerUrl.cjs");
|
|
52
58
|
|
|
53
59
|
// Per-(chat, sender) explicit context buffer for the multi-message workflow.
|
|
54
60
|
// Entries accumulate until `!gemini send` flushes them into a single call.
|
|
55
61
|
// Lives in memory only — short-lived by design.
|
|
56
|
-
const contextBuffers = new Map(); // key: "<chatJid>::<senderJid>" ->
|
|
62
|
+
const contextBuffers = new Map(); // key: "<chatJid>::<senderJid>" -> { entries, at }
|
|
63
|
+
const MAX_BUFFER_ENTRIES = 20;
|
|
64
|
+
const MAX_BUFFER_CHARS = 32_000;
|
|
65
|
+
const MAX_BUFFER_KEYS = 64;
|
|
66
|
+
const BUFFER_TTL_MS = 30 * 60 * 1000;
|
|
57
67
|
|
|
58
68
|
function bufferKey(msg) {
|
|
59
69
|
return `${msg.key.remoteJid}::${msg.key.participant || msg.key.remoteJid}`;
|
|
60
70
|
}
|
|
71
|
+
function pruneBuffers(now = Date.now()) {
|
|
72
|
+
for (const [key, slot] of contextBuffers) {
|
|
73
|
+
if (!slot?.entries?.length || now - (slot.at || 0) > BUFFER_TTL_MS) {
|
|
74
|
+
contextBuffers.delete(key);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function entryChars(entry) {
|
|
79
|
+
let size = (entry?.text?.length || 0) + (entry?.quotedText?.length || 0);
|
|
80
|
+
for (const part of [...(entry?.mediaParts || []), ...(entry?.quotedMediaParts || [])]) {
|
|
81
|
+
size += JSON.stringify(part || {}).length;
|
|
82
|
+
}
|
|
83
|
+
return size;
|
|
84
|
+
}
|
|
61
85
|
function getBuffer(msg) {
|
|
62
|
-
|
|
86
|
+
pruneBuffers();
|
|
87
|
+
return contextBuffers.get(bufferKey(msg))?.entries || [];
|
|
63
88
|
}
|
|
64
89
|
function setBuffer(msg, entries) {
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
pruneBuffers();
|
|
91
|
+
const key = bufferKey(msg);
|
|
92
|
+
if (!entries?.length) {
|
|
93
|
+
contextBuffers.delete(key);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const capped = entries.slice(-MAX_BUFFER_ENTRIES);
|
|
97
|
+
let used = 0;
|
|
98
|
+
const kept = [];
|
|
99
|
+
for (let i = capped.length - 1; i >= 0; i--) {
|
|
100
|
+
const nextSize = entryChars(capped[i]);
|
|
101
|
+
if (nextSize > MAX_BUFFER_CHARS || used + nextSize > MAX_BUFFER_CHARS) break;
|
|
102
|
+
used += nextSize;
|
|
103
|
+
kept.unshift(capped[i]);
|
|
104
|
+
}
|
|
105
|
+
if (!kept.length) return false;
|
|
106
|
+
if (contextBuffers.size >= MAX_BUFFER_KEYS && !contextBuffers.has(key)) return false;
|
|
107
|
+
contextBuffers.set(key, { entries: kept, at: Date.now() });
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
function clearAllContextBuffers() {
|
|
111
|
+
contextBuffers.clear();
|
|
67
112
|
}
|
|
68
113
|
|
|
69
114
|
// Keys come from config/settings.cjs (what the dashboard saved, else the default)
|
|
@@ -78,7 +123,9 @@ let geminiCache = { key: null, baseUrl: null, genAI: null };
|
|
|
78
123
|
function geminiClients() {
|
|
79
124
|
const key = settings.get("gemini_api_key");
|
|
80
125
|
if (!key) return { genAI: null };
|
|
81
|
-
const baseUrl =
|
|
126
|
+
const baseUrl = assertProviderBaseUrl(settings.get("gemini_base_url"), {
|
|
127
|
+
allowLoopback: true,
|
|
128
|
+
});
|
|
82
129
|
if (geminiCache.key !== key || geminiCache.baseUrl !== baseUrl) {
|
|
83
130
|
geminiCache = {
|
|
84
131
|
key,
|
|
@@ -90,58 +137,16 @@ function geminiClients() {
|
|
|
90
137
|
}
|
|
91
138
|
|
|
92
139
|
async function processIncomingMedia(parts, mediaMessage, mimeOverride = null) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
if (settings.get("ai_provider") !== "gemini") {
|
|
97
|
-
const mime = mimeOverride || mediaMessage.mimetype || "ملف";
|
|
98
|
-
parts.push({
|
|
99
|
-
text: `[تم إرفاق وسائط (${mime}) — المزود الحالي لا يستطيع قراءة الوسائط]`,
|
|
100
|
-
});
|
|
101
|
-
return null;
|
|
140
|
+
const provider = getProvider();
|
|
141
|
+
if (provider.id === "gemini") {
|
|
142
|
+
await assertProviderRequestUrl(settings.get("gemini_base_url"), { allowLoopback: true });
|
|
102
143
|
}
|
|
103
|
-
|
|
104
144
|
const { genAI } = geminiClients();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
? "image"
|
|
111
|
-
: mediaMessage.mimetype?.startsWith("video/")
|
|
112
|
-
? "video"
|
|
113
|
-
: mediaMessage.mimetype?.startsWith("audio/")
|
|
114
|
-
? "audio"
|
|
115
|
-
: "document"
|
|
116
|
-
);
|
|
117
|
-
let buffer = Buffer.from([]);
|
|
118
|
-
for await (const chunk of stream) buffer = Buffer.concat([buffer, chunk]);
|
|
119
|
-
if (!buffer.length) throw new Error("Empty media buffer");
|
|
120
|
-
|
|
121
|
-
await fs.writeFile(tempFilePath, buffer);
|
|
122
|
-
try {
|
|
123
|
-
// ai.files.upload returns the File itself, where the old fileManager
|
|
124
|
-
// wrapped it in { file }. The fileData part shape is unchanged, so history
|
|
125
|
-
// written by an older Levix still loads.
|
|
126
|
-
const uploaded = await genAI.files.upload({
|
|
127
|
-
file: tempFilePath,
|
|
128
|
-
config: {
|
|
129
|
-
mimeType: mimeOverride || mediaMessage.mimetype,
|
|
130
|
-
displayName: `media-${Date.now()}`,
|
|
131
|
-
},
|
|
132
|
-
});
|
|
133
|
-
parts.push({
|
|
134
|
-
fileData: {
|
|
135
|
-
mimeType: mimeOverride || mediaMessage.mimetype,
|
|
136
|
-
fileUri: uploaded.uri,
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
return uploaded.uri;
|
|
140
|
-
} finally {
|
|
141
|
-
try {
|
|
142
|
-
await fs.unlink(tempFilePath);
|
|
143
|
-
} catch {}
|
|
144
|
-
}
|
|
145
|
+
return provider.prepareMedia(parts, mediaMessage, mimeOverride, {
|
|
146
|
+
downloadContentFromMessage,
|
|
147
|
+
genAI,
|
|
148
|
+
tempDir: __dirname,
|
|
149
|
+
});
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
// === Multi-message context buffer helpers ===
|
|
@@ -164,15 +169,14 @@ async function captureContextEntry(msg, args) {
|
|
|
164
169
|
|
|
165
170
|
const m = msg.message || {};
|
|
166
171
|
|
|
167
|
-
const directMedia =
|
|
168
|
-
m.imageMessage || m.videoMessage || m.audioMessage || m.documentMessage;
|
|
172
|
+
const directMedia = m.imageMessage || m.videoMessage || m.audioMessage || m.documentMessage;
|
|
169
173
|
if (directMedia) {
|
|
170
174
|
try {
|
|
171
175
|
await processIncomingMedia(entry.mediaParts, directMedia);
|
|
172
176
|
} catch (err) {
|
|
173
177
|
logger.warn(
|
|
174
178
|
{ err: err?.message },
|
|
175
|
-
"[Gemini Context] direct media upload failed; entry will be text-only"
|
|
179
|
+
"[Gemini Context] direct media upload failed; entry will be text-only",
|
|
176
180
|
);
|
|
177
181
|
}
|
|
178
182
|
}
|
|
@@ -187,17 +191,14 @@ async function captureContextEntry(msg, args) {
|
|
|
187
191
|
quoted.documentMessage?.caption ||
|
|
188
192
|
"";
|
|
189
193
|
const quotedMedia =
|
|
190
|
-
quoted.imageMessage ||
|
|
191
|
-
quoted.videoMessage ||
|
|
192
|
-
quoted.audioMessage ||
|
|
193
|
-
quoted.documentMessage;
|
|
194
|
+
quoted.imageMessage || quoted.videoMessage || quoted.audioMessage || quoted.documentMessage;
|
|
194
195
|
if (quotedMedia) {
|
|
195
196
|
try {
|
|
196
197
|
await processIncomingMedia(entry.quotedMediaParts, quotedMedia);
|
|
197
198
|
} catch (err) {
|
|
198
199
|
logger.warn(
|
|
199
200
|
{ err: err?.message },
|
|
200
|
-
"[Gemini Context] quoted media upload failed; entry will continue without it"
|
|
201
|
+
"[Gemini Context] quoted media upload failed; entry will continue without it",
|
|
201
202
|
);
|
|
202
203
|
}
|
|
203
204
|
}
|
|
@@ -207,9 +208,7 @@ async function captureContextEntry(msg, args) {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
function entryHasContent(e) {
|
|
210
|
-
return Boolean(
|
|
211
|
-
e.text || e.quotedText || e.mediaParts?.length || e.quotedMediaParts?.length
|
|
212
|
-
);
|
|
211
|
+
return Boolean(e.text || e.quotedText || e.mediaParts?.length || e.quotedMediaParts?.length);
|
|
213
212
|
}
|
|
214
213
|
|
|
215
214
|
function buildPartsFromBuffer(entries) {
|
|
@@ -230,13 +229,11 @@ function summarizeEntry(e, idx) {
|
|
|
230
229
|
bits.push(`📝 ${preview}`);
|
|
231
230
|
}
|
|
232
231
|
if (e.quotedText) {
|
|
233
|
-
const preview =
|
|
234
|
-
e.quotedText.length > 40 ? e.quotedText.slice(0, 40) + "…" : e.quotedText;
|
|
232
|
+
const preview = e.quotedText.length > 40 ? e.quotedText.slice(0, 40) + "…" : e.quotedText;
|
|
235
233
|
bits.push(`↩️ "${preview}"`);
|
|
236
234
|
}
|
|
237
235
|
if (e.mediaParts?.length) bits.push(`🖼️ ميديا (${e.mediaParts.length})`);
|
|
238
|
-
if (e.quotedMediaParts?.length)
|
|
239
|
-
bits.push(`🖼️↩️ ميديا مقتبسة (${e.quotedMediaParts.length})`);
|
|
236
|
+
if (e.quotedMediaParts?.length) bits.push(`🖼️↩️ ميديا مقتبسة (${e.quotedMediaParts.length})`);
|
|
240
237
|
return `${idx + 1}. ${bits.join(" · ") || "(فارغة)"}`;
|
|
241
238
|
}
|
|
242
239
|
|
|
@@ -254,18 +251,10 @@ module.exports = {
|
|
|
254
251
|
const userName = msg.pushName;
|
|
255
252
|
const isGroup = chatId.endsWith("@g.us");
|
|
256
253
|
|
|
257
|
-
const senderId = msg.key.fromMe
|
|
258
|
-
? null
|
|
259
|
-
: isGroup
|
|
260
|
-
? msg.key.participant
|
|
261
|
-
: msg.key.remoteJid;
|
|
254
|
+
const senderId = msg.key.fromMe ? null : isGroup ? msg.key.participant : msg.key.remoteJid;
|
|
262
255
|
const isOwner = msg.key.fromMe || isOwnerJidSync(senderId);
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const isAdmin =
|
|
266
|
-
isOwner ||
|
|
267
|
-
isBotAdminUserSync(senderId) ||
|
|
268
|
-
(isGroup && isAdminInGroupSync(groupMetadata, senderId));
|
|
256
|
+
const isBotAdmin = isOwner || isBotAdminUserSync(senderId);
|
|
257
|
+
const isGroupAdmin = Boolean(isGroup && isAdminInGroupSync(groupMetadata, senderId));
|
|
269
258
|
|
|
270
259
|
// Resolve sub-command. Aliases like !del arrive via ctx.invokedName, while
|
|
271
260
|
// `!gemini del ...` arrives as args[0].
|
|
@@ -285,8 +274,8 @@ module.exports = {
|
|
|
285
274
|
const subCommand = aliasSubCmds.has(invoked)
|
|
286
275
|
? invoked
|
|
287
276
|
: inlineSubCmds.has(candidateInline)
|
|
288
|
-
|
|
289
|
-
|
|
277
|
+
? candidateInline
|
|
278
|
+
: null;
|
|
290
279
|
|
|
291
280
|
// --- conversation history management (owner only) ---
|
|
292
281
|
if (subCommand === "del" || subCommand === "resetai") {
|
|
@@ -295,7 +284,7 @@ module.exports = {
|
|
|
295
284
|
sock,
|
|
296
285
|
chatId,
|
|
297
286
|
{ text: "🚫 هذا الأمر مخصص للمالك فقط." },
|
|
298
|
-
{ replyTo: msg }
|
|
287
|
+
{ replyTo: msg },
|
|
299
288
|
);
|
|
300
289
|
await deleteChatHistoryAsync(chatId);
|
|
301
290
|
return sendBotMessage(
|
|
@@ -306,7 +295,7 @@ module.exports = {
|
|
|
306
295
|
"✅ تم مسح سجل المحادثة.\n" +
|
|
307
296
|
"_(الذاكرة الدائمة مش بتتمسح كده — استخدم `!memory clear` لو ده اللي تقصده.)_",
|
|
308
297
|
},
|
|
309
|
-
{ replyTo: msg }
|
|
298
|
+
{ replyTo: msg },
|
|
310
299
|
);
|
|
311
300
|
}
|
|
312
301
|
if (subCommand === "delall") {
|
|
@@ -315,21 +304,20 @@ module.exports = {
|
|
|
315
304
|
sock,
|
|
316
305
|
chatId,
|
|
317
306
|
{ text: "🚫 هذا الأمر مخصص للمالك فقط." },
|
|
318
|
-
{ replyTo: msg }
|
|
307
|
+
{ replyTo: msg },
|
|
319
308
|
);
|
|
320
309
|
await deleteAllChatHistoriesAsync();
|
|
321
310
|
return sendBotMessage(
|
|
322
311
|
sock,
|
|
323
312
|
chatId,
|
|
324
313
|
{ text: "✅ تم مسح كل سجلات المحادثات بنجاح." },
|
|
325
|
-
{ replyTo: msg }
|
|
314
|
+
{ replyTo: msg },
|
|
326
315
|
);
|
|
327
316
|
}
|
|
328
317
|
|
|
329
318
|
// --- image generation ---
|
|
330
319
|
if (subCommand === "generate") {
|
|
331
|
-
const quotedMsg =
|
|
332
|
-
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.conversation;
|
|
320
|
+
const quotedMsg = msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.conversation;
|
|
333
321
|
const imagePromptArgs = invoked === "generate" ? args : args.slice(1);
|
|
334
322
|
const imagePrompt = imagePromptArgs.join(" ");
|
|
335
323
|
if (!imagePrompt && !quotedMsg) {
|
|
@@ -339,32 +327,28 @@ module.exports = {
|
|
|
339
327
|
{
|
|
340
328
|
text: "اكتب وصف للصورة اللي عايزها بعد الأمر. مثال: !generate قطة ترتدي قبعة",
|
|
341
329
|
},
|
|
342
|
-
{ replyTo: msg }
|
|
330
|
+
{ replyTo: msg },
|
|
343
331
|
);
|
|
344
332
|
}
|
|
345
333
|
|
|
346
|
-
const status = await createStatus(
|
|
347
|
-
sock,
|
|
348
|
-
chatId,
|
|
349
|
-
"🎨 بجهّز الصورة...",
|
|
350
|
-
{ replyTo: msg }
|
|
351
|
-
);
|
|
334
|
+
const status = await createStatus(sock, chatId, "🎨 بجهّز الصورة...", { replyTo: msg });
|
|
352
335
|
|
|
353
336
|
try {
|
|
354
337
|
// Image generation needs a model that returns image bytes, which only
|
|
355
338
|
// the Gemini image models do — it stays on Gemini whatever the chat
|
|
356
339
|
// provider is.
|
|
357
340
|
if (!geminiClients().genAI) {
|
|
358
|
-
throw new Error(
|
|
359
|
-
"مفتاح Gemini API غير معرف — توليد الصور يعمل على Gemini فقط"
|
|
360
|
-
);
|
|
341
|
+
throw new Error("مفتاح Gemini API غير معرف — توليد الصور يعمل على Gemini فقط");
|
|
361
342
|
}
|
|
343
|
+
await assertProviderRequestUrl(settings.get("gemini_base_url"), {
|
|
344
|
+
allowLoopback: true,
|
|
345
|
+
});
|
|
362
346
|
const fullPrompt =
|
|
363
347
|
imagePrompt && quotedMsg
|
|
364
348
|
? `Prompt: ${imagePrompt} ,using quote: ${quotedMsg}`
|
|
365
349
|
: imagePrompt
|
|
366
|
-
|
|
367
|
-
|
|
350
|
+
? `Prompt: ${imagePrompt}`
|
|
351
|
+
: `Prompt: ${quotedMsg}`;
|
|
368
352
|
const response = await geminiClients().genAI.models.generateContent({
|
|
369
353
|
model: settings.get("gemini_image_model"),
|
|
370
354
|
contents: `Generate an image using this prompt: ${fullPrompt}`,
|
|
@@ -374,7 +358,7 @@ module.exports = {
|
|
|
374
358
|
// `fileData.data`, which is not a field that exists on either SDK —
|
|
375
359
|
// this path could never have produced an image.)
|
|
376
360
|
const image = (response.candidates?.[0]?.content?.parts || []).find(
|
|
377
|
-
(part) => part?.inlineData?.data
|
|
361
|
+
(part) => part?.inlineData?.data,
|
|
378
362
|
);
|
|
379
363
|
if (!image) throw new Error("الموديل رجّع رد من غير صورة");
|
|
380
364
|
const imageBuffer = Buffer.from(image.inlineData.data, "base64");
|
|
@@ -387,7 +371,7 @@ module.exports = {
|
|
|
387
371
|
image: imageBuffer,
|
|
388
372
|
caption: `🖼️ تفضل، صورة لـ: "${imagePrompt}"`,
|
|
389
373
|
},
|
|
390
|
-
{ replyTo: msg }
|
|
374
|
+
{ replyTo: msg },
|
|
391
375
|
);
|
|
392
376
|
} catch (error) {
|
|
393
377
|
logger.error({ err: error }, `Error in !generate command`);
|
|
@@ -408,21 +392,41 @@ module.exports = {
|
|
|
408
392
|
"📭 مفيش حاجة أضيفها للـ context.\n\n" +
|
|
409
393
|
"اكتب نص بعد الأمر، أو رد على رسالة، أو ابعت ميديا مع كابشن `!gemini add`.",
|
|
410
394
|
},
|
|
411
|
-
{ replyTo: msg }
|
|
395
|
+
{ replyTo: msg },
|
|
412
396
|
);
|
|
413
397
|
}
|
|
414
398
|
const buf = getBuffer(msg);
|
|
399
|
+
if (buf.length >= MAX_BUFFER_ENTRIES) {
|
|
400
|
+
return sendBotMessage(
|
|
401
|
+
sock,
|
|
402
|
+
chatId,
|
|
403
|
+
{ text: `الـ context ممتلئ (حد أقصى ${MAX_BUFFER_ENTRIES} رسائل). ابعت \`!gemini send\` أو \`!gemini clear\`.` },
|
|
404
|
+
{ replyTo: msg },
|
|
405
|
+
);
|
|
406
|
+
}
|
|
415
407
|
buf.push(entry);
|
|
416
|
-
setBuffer(msg, buf)
|
|
408
|
+
if (!setBuffer(msg, buf)) {
|
|
409
|
+
return sendBotMessage(
|
|
410
|
+
sock,
|
|
411
|
+
chatId,
|
|
412
|
+
{
|
|
413
|
+
text:
|
|
414
|
+
"الـ context كبير جدًا أو عدد الـ contexts المفتوحة وصل للحد. " +
|
|
415
|
+
"ابعت `!gemini send` أو `!gemini clear` وحاول تاني.",
|
|
416
|
+
},
|
|
417
|
+
{ replyTo: msg },
|
|
418
|
+
);
|
|
419
|
+
}
|
|
420
|
+
const stored = getBuffer(msg);
|
|
417
421
|
return sendBotMessage(
|
|
418
422
|
sock,
|
|
419
423
|
chatId,
|
|
420
424
|
{
|
|
421
425
|
text:
|
|
422
|
-
`✅ اتضافت للـ context. (إجمالي الآن: *${
|
|
426
|
+
`✅ اتضافت للـ context. (إجمالي الآن: *${stored.length}*)\n\n` +
|
|
423
427
|
"كمل بـ `!gemini add ...` ، أو ابعت كله بـ `!gemini send`.",
|
|
424
428
|
},
|
|
425
|
-
{ replyTo: msg }
|
|
429
|
+
{ replyTo: msg },
|
|
426
430
|
);
|
|
427
431
|
}
|
|
428
432
|
|
|
@@ -433,11 +437,9 @@ module.exports = {
|
|
|
433
437
|
sock,
|
|
434
438
|
chatId,
|
|
435
439
|
{
|
|
436
|
-
text: had
|
|
437
|
-
? `🗑️ تم مسح *${had}* رسالة من الـ context.`
|
|
438
|
-
: "📭 الـ context كان فاضي أصلاً.",
|
|
440
|
+
text: had ? `🗑️ تم مسح *${had}* رسالة من الـ context.` : "📭 الـ context كان فاضي أصلاً.",
|
|
439
441
|
},
|
|
440
|
-
{ replyTo: msg }
|
|
442
|
+
{ replyTo: msg },
|
|
441
443
|
);
|
|
442
444
|
}
|
|
443
445
|
|
|
@@ -448,7 +450,7 @@ module.exports = {
|
|
|
448
450
|
sock,
|
|
449
451
|
chatId,
|
|
450
452
|
{ text: "📭 الـ context فاضي. ابدأ بـ `!gemini add ...`." },
|
|
451
|
-
{ replyTo: msg }
|
|
453
|
+
{ replyTo: msg },
|
|
452
454
|
);
|
|
453
455
|
}
|
|
454
456
|
return sendBotMessage(
|
|
@@ -460,7 +462,7 @@ module.exports = {
|
|
|
460
462
|
buf.map((e, i) => summarizeEntry(e, i)).join("\n") +
|
|
461
463
|
"\n\nابعت كله بـ `!gemini send` أو امسحه بـ `!gemini clear`.",
|
|
462
464
|
},
|
|
463
|
-
{ replyTo: msg }
|
|
465
|
+
{ replyTo: msg },
|
|
464
466
|
);
|
|
465
467
|
}
|
|
466
468
|
|
|
@@ -482,7 +484,7 @@ module.exports = {
|
|
|
482
484
|
"📭 الـ context فاضي ومفيش نص في `!gemini send`.\n\n" +
|
|
483
485
|
"ابدأ بـ `!gemini add ...` الأول، أو اكتب نص مع `!gemini send ...`.",
|
|
484
486
|
},
|
|
485
|
-
{ replyTo: msg }
|
|
487
|
+
{ replyTo: msg },
|
|
486
488
|
);
|
|
487
489
|
}
|
|
488
490
|
setBuffer(msg, []);
|
|
@@ -496,18 +498,13 @@ module.exports = {
|
|
|
496
498
|
try {
|
|
497
499
|
await processIncomingMedia(parts, mediaMessage);
|
|
498
500
|
parts.push({
|
|
499
|
-
text:
|
|
500
|
-
prompt || mediaMessage.caption || "ماذا يوجد في هذه الصورة/الفيديو؟",
|
|
501
|
+
text: prompt || mediaMessage.caption || "ماذا يوجد في هذه الصورة/الفيديو؟",
|
|
501
502
|
});
|
|
502
503
|
} catch (error) {
|
|
503
504
|
logger.error({ err: error }, "Failed to upload media to Gemini.");
|
|
504
|
-
return sendBotError(
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
error,
|
|
508
|
-
"حصلت مشكلة في رفع الصورة/الفيديو",
|
|
509
|
-
{ replyTo: msg }
|
|
510
|
-
);
|
|
505
|
+
return sendBotError(sock, chatId, error, "حصلت مشكلة في رفع الصورة/الفيديو", {
|
|
506
|
+
replyTo: msg,
|
|
507
|
+
});
|
|
511
508
|
}
|
|
512
509
|
} else if (!parts.length && msg.message?.audioMessage) {
|
|
513
510
|
try {
|
|
@@ -515,13 +512,9 @@ module.exports = {
|
|
|
515
512
|
parts.push({ text: prompt || "حلل لي هذا التسجيل الصوتي." });
|
|
516
513
|
} catch (error) {
|
|
517
514
|
logger.error({ err: error }, "Failed to upload audio to Gemini.");
|
|
518
|
-
return sendBotError(
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
error,
|
|
522
|
-
"حصلت مشكلة في رفع التسجيل الصوتي",
|
|
523
|
-
{ replyTo: msg }
|
|
524
|
-
);
|
|
515
|
+
return sendBotError(sock, chatId, error, "حصلت مشكلة في رفع التسجيل الصوتي", {
|
|
516
|
+
replyTo: msg,
|
|
517
|
+
});
|
|
525
518
|
}
|
|
526
519
|
} else if (!parts.length && msg.message?.documentMessage) {
|
|
527
520
|
try {
|
|
@@ -529,9 +522,7 @@ module.exports = {
|
|
|
529
522
|
const fileName = msg.message.documentMessage.fileName || "document";
|
|
530
523
|
const text = prompt || msg.message.documentMessage.caption || "";
|
|
531
524
|
parts.push({
|
|
532
|
-
text: text
|
|
533
|
-
? `${text}\n(الملف: ${fileName})`
|
|
534
|
-
: `حلل/لخّص الملف: ${fileName}`,
|
|
525
|
+
text: text ? `${text}\n(الملف: ${fileName})` : `حلل/لخّص الملف: ${fileName}`,
|
|
535
526
|
});
|
|
536
527
|
} catch (error) {
|
|
537
528
|
logger.error({ err: error }, "Failed to upload document to Gemini.");
|
|
@@ -548,16 +539,14 @@ module.exports = {
|
|
|
548
539
|
sock,
|
|
549
540
|
chatId,
|
|
550
541
|
{ text: "يرجى كتابة سؤال أو إرسال صورة/فيديو/تسجيل صوتي/ملف مع الأمر." },
|
|
551
|
-
{ replyTo: msg }
|
|
542
|
+
{ replyTo: msg },
|
|
552
543
|
);
|
|
553
544
|
}
|
|
554
545
|
|
|
555
546
|
// Quoted message context. Skipped for `!gemini send` — those entries
|
|
556
547
|
// captured their own quotes at `add` time.
|
|
557
548
|
const quotedMsg =
|
|
558
|
-
subCommand === "send"
|
|
559
|
-
? null
|
|
560
|
-
: msg.message?.extendedTextMessage?.contextInfo?.quotedMessage;
|
|
549
|
+
subCommand === "send" ? null : msg.message?.extendedTextMessage?.contextInfo?.quotedMessage;
|
|
561
550
|
|
|
562
551
|
if (quotedMsg) {
|
|
563
552
|
let quotedMediaPart = null;
|
|
@@ -579,13 +568,9 @@ module.exports = {
|
|
|
579
568
|
quotedMediaPart = probeParts[0];
|
|
580
569
|
} catch (mediaError) {
|
|
581
570
|
logger.error({ err: mediaError }, "Failed to process quoted media");
|
|
582
|
-
await sendBotError(
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
mediaError,
|
|
586
|
-
"حصلت مشكلة في الرسالة المقتبسة",
|
|
587
|
-
{ replyTo: msg }
|
|
588
|
-
);
|
|
571
|
+
await sendBotError(sock, chatId, mediaError, "حصلت مشكلة في الرسالة المقتبسة", {
|
|
572
|
+
replyTo: msg,
|
|
573
|
+
});
|
|
589
574
|
}
|
|
590
575
|
}
|
|
591
576
|
|
|
@@ -603,17 +588,16 @@ module.exports = {
|
|
|
603
588
|
}
|
|
604
589
|
}
|
|
605
590
|
|
|
606
|
-
// Provider-aware
|
|
607
|
-
|
|
608
|
-
if (!
|
|
609
|
-
const provider = settings.get("ai_provider");
|
|
591
|
+
// Provider-aware: delegates configuration check to the active provider adapter.
|
|
592
|
+
const activeProvider = getProvider();
|
|
593
|
+
if (!activeProvider.isConfigured()) {
|
|
610
594
|
return sendBotMessage(
|
|
611
595
|
sock,
|
|
612
596
|
chatId,
|
|
613
597
|
{
|
|
614
|
-
text: `خطأ في الإعدادات: مفتاح مزود الذكاء الاصطناعي الحالي (${
|
|
598
|
+
text: `خطأ في الإعدادات: مفتاح مزود الذكاء الاصطناعي الحالي (${activeProvider.label || activeProvider.id}) غير معرف — عدّله من لوحة التحكم.`,
|
|
615
599
|
},
|
|
616
|
-
{ replyTo: msg }
|
|
600
|
+
{ replyTo: msg },
|
|
617
601
|
);
|
|
618
602
|
}
|
|
619
603
|
|
|
@@ -625,19 +609,20 @@ module.exports = {
|
|
|
625
609
|
senderId,
|
|
626
610
|
senderName: userName,
|
|
627
611
|
isOwner,
|
|
628
|
-
isAdmin,
|
|
612
|
+
isAdmin: isBotAdmin,
|
|
613
|
+
isGroupAdmin,
|
|
629
614
|
isGroup,
|
|
630
615
|
chatName: groupMetadata?.subject || null,
|
|
631
616
|
mentionedJids: contextInfo?.mentionedJid || [],
|
|
632
617
|
quotedParticipant: contextInfo?.participant || null,
|
|
618
|
+
userText: body || "",
|
|
633
619
|
};
|
|
634
620
|
|
|
635
621
|
// Mentions/replies are ids the model can't invent — hand them over so
|
|
636
622
|
// "اعمل الراجل ده أدمن" has something concrete to act on.
|
|
637
|
-
const targets = [
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
].filter(Boolean);
|
|
623
|
+
const targets = [...(agentContext.mentionedJids || []), agentContext.quotedParticipant].filter(
|
|
624
|
+
Boolean,
|
|
625
|
+
);
|
|
641
626
|
if (targets.length) {
|
|
642
627
|
parts.unshift({
|
|
643
628
|
text: `(معرفات مذكورة في الرسالة: ${[...new Set(targets)].join(", ")})`,
|
|
@@ -645,7 +630,7 @@ module.exports = {
|
|
|
645
630
|
}
|
|
646
631
|
|
|
647
632
|
// The one message that will carry the whole run.
|
|
648
|
-
const status = await createStatus(sock, chatId, "🤖
|
|
633
|
+
const status = await createStatus(sock, chatId, "🤖 جاري التفكير...", {
|
|
649
634
|
replyTo: msg,
|
|
650
635
|
});
|
|
651
636
|
|
|
@@ -676,21 +661,22 @@ module.exports = {
|
|
|
676
661
|
const cleaned = sanitizeHistoryForFiles(oldHistory);
|
|
677
662
|
await saveChatHistoryAsync(chatId, cleaned);
|
|
678
663
|
logger.info(
|
|
679
|
-
`[Gemini] sanitized history for chat ${chatId}: ${oldHistory.length} -> ${cleaned.length}; retrying once
|
|
664
|
+
`[Gemini] sanitized history for chat ${chatId}: ${oldHistory.length} -> ${cleaned.length}; retrying once`,
|
|
680
665
|
);
|
|
681
666
|
result = await attempt(cleaned);
|
|
682
667
|
}
|
|
683
668
|
|
|
684
|
-
const text =
|
|
685
|
-
result.text?.trim() ||
|
|
686
|
-
"مفيش رد جه من الموديل. جرّب تصيغ السؤال بشكل تاني.";
|
|
669
|
+
const text = result.text?.trim() || "مفيش رد جه من الموديل. جرّب تصيغ السؤال بشكل تاني.";
|
|
687
670
|
// Appended only when Gemini really grounded the answer on a search —
|
|
688
671
|
// formatSources() returns "" for an answer the model gave from its own
|
|
689
672
|
// knowledge, so there is never a Sources block with nothing behind it.
|
|
690
|
-
|
|
673
|
+
const responseText = formatMarkdownForWhatsApp(`${text}${formatSources(result.sources)}`);
|
|
674
|
+
await status.finish(responseText);
|
|
691
675
|
} catch (error) {
|
|
692
676
|
logger.error({ err: error }, "Error in !gemini command");
|
|
693
677
|
await status.fail(error, "حصلت مشكلة وأنا بكلم الذكاء الاصطناعي");
|
|
694
678
|
}
|
|
695
679
|
},
|
|
696
680
|
};
|
|
681
|
+
|
|
682
|
+
module.exports.clearAllContextBuffers = clearAllContextBuffers;
|
|
@@ -4,8 +4,7 @@ const normalizeJid = require("../../utils/normalizeJid.esm.js").default;
|
|
|
4
4
|
|
|
5
5
|
module.exports = {
|
|
6
6
|
name: "add",
|
|
7
|
-
description:
|
|
8
|
-
"Adds a member to the group or sends an invite if privacy settings block it.",
|
|
7
|
+
description: "Adds a member to the group or sends an invite if privacy settings block it.",
|
|
9
8
|
usage: "add [admin] <رقم الموبايل>",
|
|
10
9
|
chat: "group",
|
|
11
10
|
userAdminRequired: true,
|
|
@@ -22,25 +21,21 @@ module.exports = {
|
|
|
22
21
|
try {
|
|
23
22
|
// --- ✅ THIS IS THE MISSING LOGIC TO DEFINE targetJid ---
|
|
24
23
|
// 1. Identify the Target User
|
|
25
|
-
const mentionedJid =
|
|
26
|
-
msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
|
|
24
|
+
const mentionedJid = msg.message?.extendedTextMessage?.contextInfo?.mentionedJid?.[0];
|
|
27
25
|
let contactJid;
|
|
28
26
|
|
|
29
27
|
// Check for contact card in a quoted message
|
|
30
28
|
const quotedVcard =
|
|
31
|
-
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage
|
|
32
|
-
?.contactMessage?.vcard;
|
|
29
|
+
msg.message?.extendedTextMessage?.contextInfo?.quotedMessage?.contactMessage?.vcard;
|
|
33
30
|
if (quotedVcard) {
|
|
34
|
-
contactJid =
|
|
35
|
-
quotedVcard.match(/waid=([0-9]+)/)?.[1] + "@s.whatsapp.net";
|
|
31
|
+
contactJid = quotedVcard.match(/waid=([0-9]+)/)?.[1] + "@s.whatsapp.net";
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
// If not found, check for contact card in the main message
|
|
39
35
|
if (!contactJid || !contactJid.includes("@")) {
|
|
40
36
|
const mainVcard = msg.message?.contactMessage?.vcard;
|
|
41
37
|
if (mainVcard) {
|
|
42
|
-
contactJid =
|
|
43
|
-
mainVcard.match(/waid=([0-9]+)/)?.[1] + "@s.whatsapp.net";
|
|
38
|
+
contactJid = mainVcard.match(/waid=([0-9]+)/)?.[1] + "@s.whatsapp.net";
|
|
44
39
|
}
|
|
45
40
|
}
|
|
46
41
|
|
|
@@ -70,7 +65,7 @@ module.exports = {
|
|
|
70
65
|
|
|
71
66
|
// Pre-check if the user is already in the group
|
|
72
67
|
const isAlreadyMember = groupMetadata.participants.some(
|
|
73
|
-
(p) => normalizeJid(p.id) === normalizeJid(targetJid)
|
|
68
|
+
(p) => normalizeJid(p.id) === normalizeJid(targetJid),
|
|
74
69
|
);
|
|
75
70
|
if (isAlreadyMember) {
|
|
76
71
|
return await sock.sendMessage(groupId, {
|
|
@@ -83,11 +78,7 @@ module.exports = {
|
|
|
83
78
|
text: `جاري محاولة إضافة @${targetJid.split("@")[0]}...`,
|
|
84
79
|
mentions: [targetJid],
|
|
85
80
|
});
|
|
86
|
-
const response = await sock.groupParticipantsUpdate(
|
|
87
|
-
groupId,
|
|
88
|
-
[targetJid],
|
|
89
|
-
"add"
|
|
90
|
-
);
|
|
81
|
+
const response = await sock.groupParticipantsUpdate(groupId, [targetJid], "add");
|
|
91
82
|
const status = response[0].status;
|
|
92
83
|
|
|
93
84
|
if (status === "200") {
|