levix-bot 3.3.0-rc.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +13 -9
  2. package/app.cjs +49 -46
  3. package/package.json +9 -1
  4. package/public/dashboard/assets/index-BBBXWzM0.js +391 -0
  5. package/public/dashboard/assets/index-pYaivHON.css +1 -0
  6. package/public/dashboard/index.html +19 -0
  7. package/public/dashboard.css +391 -66
  8. package/public/dashboard.js +593 -42
  9. package/scheduler.cjs +9 -14
  10. package/src/auth/use-database-auth-state.js +2 -4
  11. package/src/bootstrap/core.js +2 -5
  12. package/src/bootstrap/panel.js +10 -5
  13. package/src/cli.js +3 -5
  14. package/src/commands/autoschedule.cjs +1 -4
  15. package/src/commands/blacklist.cjs +22 -20
  16. package/src/commands/block.cjs +2 -4
  17. package/src/commands/calc.cjs +9 -16
  18. package/src/commands/digit.cjs +97 -0
  19. package/src/commands/gemini.cjs +147 -161
  20. package/src/commands/group/add.cjs +7 -16
  21. package/src/commands/group/antiSpam.cjs +5 -11
  22. package/src/commands/group/antilink.cjs +34 -28
  23. package/src/commands/group/approveall.cjs +3 -11
  24. package/src/commands/group/clearwarns.cjs +2 -5
  25. package/src/commands/group/demote.cjs +3 -8
  26. package/src/commands/group/kick.cjs +12 -15
  27. package/src/commands/group/media.cjs +17 -24
  28. package/src/commands/group/members.cjs +1 -2
  29. package/src/commands/group/promote.cjs +4 -12
  30. package/src/commands/group/removeall.cjs +2 -5
  31. package/src/commands/group/setpp.cjs +1 -1
  32. package/src/commands/group/setrules.cjs +2 -8
  33. package/src/commands/group/setwarn.cjs +4 -2
  34. package/src/commands/group/tagadmins.cjs +2 -5
  35. package/src/commands/group/warn.cjs +26 -24
  36. package/src/commands/group/warns.cjs +1 -2
  37. package/src/commands/group/welcome.cjs +13 -17
  38. package/src/commands/group.cjs +7 -10
  39. package/src/commands/help.cjs +6 -12
  40. package/src/commands/lang.cjs +104 -0
  41. package/src/commands/listschedules.cjs +2 -6
  42. package/src/commands/loop.cjs +6 -13
  43. package/src/commands/memory.cjs +19 -32
  44. package/src/commands/mod.cjs +97 -35
  45. package/src/commands/perm.cjs +18 -56
  46. package/src/commands/ping.cjs +1 -5
  47. package/src/commands/poll.cjs +1 -1
  48. package/src/commands/rand.cjs +2 -5
  49. package/src/commands/schedule.cjs +4 -8
  50. package/src/commands/score.cjs +8 -2
  51. package/src/commands/setprefix.cjs +2 -2
  52. package/src/commands/shortlink.cjs +7 -15
  53. package/src/commands/shutdown.cjs +1 -1
  54. package/src/commands/sticker.cjs +222 -0
  55. package/src/commands/stt.cjs +136 -68
  56. package/src/commands/todo.cjs +9 -15
  57. package/src/commands/tts.cjs +10 -16
  58. package/src/commands/unblock.cjs +2 -6
  59. package/src/commands/weather.cjs +5 -12
  60. package/src/config/ai-persona.md +5 -3
  61. package/src/config/available-models.json +1156 -0
  62. package/src/config/baileys.config.js +2 -1
  63. package/src/config/brand.esm.js +1 -10
  64. package/src/config/constants.js +1 -1
  65. package/src/config/defaults.cjs +66 -66
  66. package/src/config/lock.cjs +2 -2
  67. package/src/config/modelCapabilities.cjs +172 -0
  68. package/src/config/runtime-config.cjs +8 -13
  69. package/src/config/secrets.cjs +18 -10
  70. package/src/config/settings.cjs +94 -10
  71. package/src/config/vault.cjs +107 -0
  72. package/src/core/connection.js +13 -12
  73. package/src/core/events.js +41 -23
  74. package/src/core/proxy.js +7 -7
  75. package/src/core/session.js +91 -48
  76. package/src/core/socket.js +12 -15
  77. package/src/db/db.cjs +14 -6
  78. package/src/db/store.cjs +221 -87
  79. package/src/domain/caddy.js +1 -1
  80. package/src/domain/command.js +40 -19
  81. package/src/domain/detect.js +1 -1
  82. package/src/domain/system.js +2 -5
  83. package/src/handlers/command.handler.js +22 -43
  84. package/src/handlers/group.handler.js +13 -13
  85. package/src/handlers/message.handler.js +11 -21
  86. package/src/index.js +8 -4
  87. package/src/middleware/antispam.middleware.js +46 -23
  88. package/src/middleware/blacklist.middleware.js +14 -9
  89. package/src/middleware/forward-tracking.middleware.js +3 -2
  90. package/src/middleware/permissions.middleware.js +7 -10
  91. package/src/panel/login-throttle.cjs +43 -0
  92. package/src/panel/session-auth.cjs +17 -0
  93. package/src/routes/dashboard.api.esm.js +394 -72
  94. package/src/services/aiAgent.cjs +136 -49
  95. package/src/services/aiProviders.cjs +193 -47
  96. package/src/services/aiRouter.cjs +137 -0
  97. package/src/services/aiTools.cjs +305 -128
  98. package/src/services/llmDiscovery.cjs +481 -0
  99. package/src/services/modelRegistry.cjs +412 -0
  100. package/src/utils/datetime.cjs +2 -2
  101. package/src/utils/lid-helper.esm.js +7 -7
  102. package/src/utils/logger.cjs +1 -1
  103. package/src/utils/markdownParser.cjs +159 -0
  104. package/src/utils/memory.cjs +43 -13
  105. package/src/utils/messageContent.cjs +40 -0
  106. package/src/utils/normalizeJid.esm.js +7 -7
  107. package/src/utils/numerals.cjs +75 -0
  108. package/src/utils/permissions.esm.js +10 -15
  109. package/src/utils/providerUrl.cjs +281 -0
  110. package/src/utils/recurrence.cjs +7 -11
  111. package/src/utils/sendBotMessage.esm.js +4 -14
  112. package/src/utils/statusMessage.cjs +5 -13
  113. package/src/utils/storage.cjs +10 -0
  114. package/src/utils/storage.esm.js +11 -0
  115. package/src/utils/textDecode.cjs +4 -9
  116. package/src/utils/thumbnail.cjs +8 -14
  117. package/src/utils/typing.esm.js +12 -12
  118. package/views/dashboard.ejs +134 -106
@@ -0,0 +1,481 @@
1
+ // LLM provider model discovery.
2
+ //
3
+ // Live account availability comes only from each provider's authenticated
4
+ // models API. The seed catalog is recommendations / capability fallback.
5
+
6
+ const logger = require("../utils/logger.cjs");
7
+ const { assertProviderBaseUrl, safeProviderFetch } = require("../utils/providerUrl.cjs");
8
+ const {
9
+ getSeedModels,
10
+ resolveCapabilities,
11
+ getCachedDiscovery,
12
+ setCachedDiscovery,
13
+ isConversationalLlm,
14
+ cleanModelId,
15
+ } = require("./modelRegistry.cjs");
16
+
17
+ const REQUEST_TIMEOUT_MS = 20000;
18
+
19
+ class DiscoveryError extends Error {
20
+ constructor(message, { code = 500, type = "server_error" } = {}) {
21
+ super(message);
22
+ this.name = "DiscoveryError";
23
+ this.code = code;
24
+ this.type = type;
25
+ }
26
+ }
27
+
28
+ class BaseDiscoveryAdapter {
29
+ constructor(providerId) {
30
+ this.providerId = providerId;
31
+ }
32
+
33
+ validateCredentials(apiKey) {
34
+ if (!apiKey || typeof apiKey !== "string" || !apiKey.trim()) {
35
+ throw new DiscoveryError(`API key is required for live ${this.providerId} model discovery.`, {
36
+ code: 400,
37
+ type: "missing_credentials",
38
+ });
39
+ }
40
+ return apiKey.trim();
41
+ }
42
+
43
+ async discover(_options) {
44
+ throw new Error("discover() must be implemented by subclass");
45
+ }
46
+ }
47
+
48
+ async function requestJson(url, { headers, timeoutMs = REQUEST_TIMEOUT_MS } = {}) {
49
+ const controller = new AbortController();
50
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
51
+ try {
52
+ const resp = await safeProviderFetch(url, {
53
+ method: "GET",
54
+ headers: { Accept: "application/json", ...headers },
55
+ signal: controller.signal,
56
+ }, { allowLoopback: true });
57
+ return resp;
58
+ } catch (err) {
59
+ if (err && err.name === "AbortError") {
60
+ throw new DiscoveryError("The models request timed out.", {
61
+ code: 504,
62
+ type: "network_error",
63
+ });
64
+ }
65
+ throw new DiscoveryError("Network error reaching the provider models endpoint.", {
66
+ code: 0,
67
+ type: "network_error",
68
+ });
69
+ } finally {
70
+ clearTimeout(timer);
71
+ }
72
+ }
73
+
74
+ async function readJsonBody(resp) {
75
+ try {
76
+ return await resp.json();
77
+ } catch {
78
+ throw new DiscoveryError("Invalid JSON response from the provider models endpoint.", {
79
+ code: 502,
80
+ type: "server_error",
81
+ });
82
+ }
83
+ }
84
+
85
+ function httpError(providerLabel, status, { invalidHint, permissionHint } = {}) {
86
+ if (status === 401) {
87
+ throw new DiscoveryError(
88
+ invalidHint ||
89
+ `Invalid or expired ${providerLabel} API credential. Please verify and re-enter your key.`,
90
+ { code: 401, type: "invalid_credentials" },
91
+ );
92
+ }
93
+ if (status === 403) {
94
+ throw new DiscoveryError(
95
+ permissionHint || `Your ${providerLabel} credential does not have permission to list models.`,
96
+ { code: 403, type: "permission_denied" },
97
+ );
98
+ }
99
+ if (status === 429) {
100
+ throw new DiscoveryError(
101
+ `${providerLabel} rate limit or account quota exceeded. This does not mean the API key is invalid.`,
102
+ { code: 429, type: "rate_limited" },
103
+ );
104
+ }
105
+ if (status >= 500) {
106
+ throw new DiscoveryError(
107
+ `${providerLabel} is temporarily unavailable. Please try again later.`,
108
+ { code: status, type: "server_error" },
109
+ );
110
+ }
111
+ throw new DiscoveryError(`${providerLabel} API returned status ${status}.`, {
112
+ code: status,
113
+ type: "server_error",
114
+ });
115
+ }
116
+
117
+ function enrichDiscoveredModel({ id, displayName, provider, providerCaps, status }) {
118
+ const resolved = resolveCapabilities(provider, id, providerCaps);
119
+ return {
120
+ id,
121
+ displayName: resolved.displayName || displayName || id,
122
+ provider,
123
+ recommended: resolved.recommended,
124
+ capabilities: resolved.capabilities,
125
+ capabilitySource: resolved.capabilitySource,
126
+ status: status || resolved.status || "active",
127
+ live: true,
128
+ };
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // OpenAI (and OpenAI-compatible) adapter
133
+ // ---------------------------------------------------------------------------
134
+ class OpenAIDiscoveryAdapter extends BaseDiscoveryAdapter {
135
+ constructor() {
136
+ super("openai");
137
+ }
138
+
139
+ async discover({ apiKey, baseUrl = "https://api.openai.com/v1" }) {
140
+ const key = this.validateCredentials(apiKey);
141
+ const rootUrl = assertProviderBaseUrl(baseUrl || "https://api.openai.com/v1", {
142
+ allowLoopback: true,
143
+ });
144
+ const resp = await requestJson(`${rootUrl}/models`, {
145
+ headers: { Authorization: `Bearer ${key}` },
146
+ });
147
+
148
+ if (!resp.ok) {
149
+ httpError("OpenAI", resp.status);
150
+ }
151
+
152
+ const data = await readJsonBody(resp);
153
+ const rawList = Array.isArray(data?.data) ? data.data : [];
154
+
155
+ return rawList
156
+ .map((m) => ({ id: cleanModelId(m.id), raw: m }))
157
+ .filter((m) => isConversationalLlm("openai", m.id))
158
+ .map((m) =>
159
+ enrichDiscoveredModel({
160
+ id: m.id,
161
+ displayName: m.id,
162
+ provider: "openai",
163
+ }),
164
+ );
165
+ }
166
+ }
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Anthropic adapter
170
+ // ---------------------------------------------------------------------------
171
+ class AnthropicDiscoveryAdapter extends BaseDiscoveryAdapter {
172
+ constructor() {
173
+ super("anthropic");
174
+ }
175
+
176
+ extractProviderCaps(raw) {
177
+ const providerCaps = raw.capabilities || raw.features;
178
+ const hasExplicit =
179
+ (providerCaps && typeof providerCaps === "object") ||
180
+ raw.supports_vision !== undefined ||
181
+ raw.supports_audio !== undefined ||
182
+ Array.isArray(raw.input_modalities);
183
+
184
+ if (!hasExplicit) return null;
185
+
186
+ const modalities = Array.isArray(raw.input_modalities) ? raw.input_modalities : [];
187
+ const pick = (...vals) => {
188
+ for (const v of vals) {
189
+ if (v !== undefined && v !== null) return Boolean(v);
190
+ }
191
+ return undefined;
192
+ };
193
+
194
+ const vision = pick(
195
+ raw.supports_vision,
196
+ modalities.length ? modalities.includes("image") : undefined,
197
+ providerCaps?.image_input,
198
+ providerCaps?.vision,
199
+ );
200
+ const audioInput = pick(
201
+ raw.supports_audio,
202
+ modalities.length ? modalities.includes("audio") : undefined,
203
+ providerCaps?.audio_input,
204
+ );
205
+ const pdfInput = pick(providerCaps?.document_input, providerCaps?.pdf_input);
206
+ const videoInput = pick(providerCaps?.video_input);
207
+ const stt = pick(providerCaps?.stt);
208
+
209
+ const caps = { textInput: true, textOutput: true };
210
+ if (vision !== undefined) caps.vision = vision;
211
+ if (audioInput !== undefined) caps.audioInput = audioInput;
212
+ if (pdfInput !== undefined) caps.pdfInput = pdfInput;
213
+ if (videoInput !== undefined) caps.videoInput = videoInput;
214
+ if (stt !== undefined) caps.stt = stt;
215
+ return caps;
216
+ }
217
+
218
+ async discover({ apiKey, baseUrl = "https://api.anthropic.com" }) {
219
+ const key = this.validateCredentials(apiKey);
220
+ const rootUrl = assertProviderBaseUrl(baseUrl || "https://api.anthropic.com", {
221
+ allowLoopback: true,
222
+ });
223
+
224
+ const allModels = [];
225
+ let hasMore = true;
226
+ let afterId = null;
227
+ let pageCount = 0;
228
+ const maxPages = 8;
229
+
230
+ while (hasMore && pageCount < maxPages) {
231
+ pageCount += 1;
232
+ const queryParams = new URLSearchParams({ limit: "100" });
233
+ if (afterId) queryParams.set("after_id", afterId);
234
+
235
+ const resp = await requestJson(`${rootUrl}/v1/models?${queryParams.toString()}`, {
236
+ headers: {
237
+ "x-api-key": key,
238
+ "anthropic-version": "2023-06-01",
239
+ },
240
+ });
241
+
242
+ if (!resp.ok) {
243
+ httpError("Anthropic", resp.status);
244
+ }
245
+
246
+ const data = await readJsonBody(resp);
247
+ const rawList = Array.isArray(data?.data) ? data.data : [];
248
+ for (const item of rawList) {
249
+ const id = cleanModelId(item.id);
250
+ if (!isConversationalLlm("anthropic", id, { type: item.type })) continue;
251
+ allModels.push(
252
+ enrichDiscoveredModel({
253
+ id,
254
+ displayName: item.display_name || id,
255
+ provider: "anthropic",
256
+ providerCaps: this.extractProviderCaps(item),
257
+ }),
258
+ );
259
+ }
260
+
261
+ hasMore = Boolean(data?.has_more);
262
+ afterId = data?.last_id || (rawList.length ? rawList[rawList.length - 1].id : null);
263
+ if (!afterId) hasMore = false;
264
+ }
265
+
266
+ return allModels;
267
+ }
268
+ }
269
+
270
+ // ---------------------------------------------------------------------------
271
+ // Google Gemini adapter
272
+ // Official REST: GET /v1beta/models with x-goog-api-key (same auth the SDK uses,
273
+ // covering both legacy API keys and newer authorization keys).
274
+ // ---------------------------------------------------------------------------
275
+ class GeminiDiscoveryAdapter extends BaseDiscoveryAdapter {
276
+ constructor() {
277
+ super("gemini");
278
+ }
279
+
280
+ handleGeminiHttpError(status, errBody) {
281
+ const reason = String(errBody?.error?.status || errBody?.error?.message || "");
282
+ if (
283
+ status === 401 ||
284
+ reason.includes("API_KEY_INVALID") ||
285
+ reason.includes("UNAUTHENTICATED")
286
+ ) {
287
+ throw new DiscoveryError(
288
+ "Invalid or expired Google Gemini API credential. Please verify your key at Google AI Studio.",
289
+ { code: 401, type: "invalid_credentials" },
290
+ );
291
+ }
292
+ if (status === 403 || reason.includes("PERMISSION_DENIED")) {
293
+ throw new DiscoveryError(
294
+ "Your Google Gemini credential does not have permission to list models.",
295
+ { code: 403, type: "permission_denied" },
296
+ );
297
+ }
298
+ if (status === 400 && reason.includes("API_KEY_INVALID")) {
299
+ throw new DiscoveryError(
300
+ "Invalid or expired Google Gemini API credential. Please verify your key at Google AI Studio.",
301
+ { code: 401, type: "invalid_credentials" },
302
+ );
303
+ }
304
+ httpError("Google Gemini", status === 400 ? 400 : status);
305
+ }
306
+
307
+ async discover({ apiKey, baseUrl = "https://generativelanguage.googleapis.com" }) {
308
+ const key = this.validateCredentials(apiKey);
309
+ const rootUrl = assertProviderBaseUrl(
310
+ baseUrl || "https://generativelanguage.googleapis.com",
311
+ { allowLoopback: true },
312
+ );
313
+
314
+ const rawList = [];
315
+ let pageToken = "";
316
+ let pageCount = 0;
317
+ const maxPages = 8;
318
+
319
+ while (pageCount < maxPages) {
320
+ pageCount += 1;
321
+ const query = new URLSearchParams({ pageSize: "100" });
322
+ if (pageToken) query.set("pageToken", pageToken);
323
+
324
+ const resp = await requestJson(`${rootUrl}/v1beta/models?${query.toString()}`, {
325
+ headers: { "x-goog-api-key": key },
326
+ });
327
+
328
+ if (!resp.ok) {
329
+ let errBody = null;
330
+ try {
331
+ errBody = await resp.json();
332
+ } catch {
333
+ // ignore parse error
334
+ }
335
+ this.handleGeminiHttpError(resp.status, errBody);
336
+ }
337
+
338
+ const data = await readJsonBody(resp);
339
+ const page = Array.isArray(data?.models) ? data.models : [];
340
+ rawList.push(...page);
341
+ pageToken = data?.nextPageToken || "";
342
+ if (!pageToken) break;
343
+ }
344
+
345
+ return rawList
346
+ .map((m) => {
347
+ const id = cleanModelId(m.name || m.id);
348
+ const methods = m.supportedGenerationMethods || m.supportedActions || [];
349
+ return { id, methods, raw: m };
350
+ })
351
+ .filter((m) => isConversationalLlm("gemini", m.id, { methods: m.methods }))
352
+ .map((m) =>
353
+ enrichDiscoveredModel({
354
+ id: m.id,
355
+ displayName: m.raw.displayName || m.id,
356
+ provider: "gemini",
357
+ status: "active",
358
+ }),
359
+ );
360
+ }
361
+ }
362
+
363
+ const ADAPTERS = {
364
+ openai: new OpenAIDiscoveryAdapter(),
365
+ anthropic: new AnthropicDiscoveryAdapter(),
366
+ gemini: new GeminiDiscoveryAdapter(),
367
+ };
368
+
369
+ function seedOnlyResult(provKey, message) {
370
+ return {
371
+ success: true,
372
+ live: false,
373
+ requiresApiKey: true,
374
+ fallback: true,
375
+ provider: provKey,
376
+ models: getSeedModels(provKey),
377
+ message,
378
+ };
379
+ }
380
+
381
+ async function discoverProviderModels(optionsOrProvider = "gemini", maybeOptions = {}) {
382
+ let options = {};
383
+ if (typeof optionsOrProvider === "string") {
384
+ options = { provider: optionsOrProvider, ...maybeOptions };
385
+ } else if (optionsOrProvider && typeof optionsOrProvider === "object") {
386
+ options = optionsOrProvider;
387
+ }
388
+
389
+ const { provider = "gemini", apiKey = "", baseUrl = "", refresh = false } = options;
390
+ const provKey = String(provider || "gemini").toLowerCase();
391
+ const adapter = ADAPTERS[provKey];
392
+
393
+ if (!adapter) {
394
+ return {
395
+ success: false,
396
+ live: false,
397
+ requiresApiKey: false,
398
+ fallback: false,
399
+ provider: provKey,
400
+ error: {
401
+ code: 400,
402
+ type: "unsupported_provider",
403
+ message: `Unsupported LLM provider: ${provider}`,
404
+ },
405
+ models: [],
406
+ };
407
+ }
408
+
409
+ if (!apiKey || !String(apiKey).trim()) {
410
+ return seedOnlyResult(
411
+ provKey,
412
+ `Please connect your ${provKey} API key to discover models available to your account.`,
413
+ );
414
+ }
415
+
416
+ const cleanKey = String(apiKey).trim();
417
+
418
+ if (!refresh) {
419
+ const cached = getCachedDiscovery(provKey, cleanKey, baseUrl);
420
+ if (cached) {
421
+ return {
422
+ success: true,
423
+ live: true,
424
+ requiresApiKey: false,
425
+ fallback: false,
426
+ provider: provKey,
427
+ models: cached,
428
+ cached: true,
429
+ };
430
+ }
431
+ }
432
+
433
+ try {
434
+ const liveModels = await adapter.discover({
435
+ apiKey: cleanKey,
436
+ baseUrl,
437
+ });
438
+
439
+ setCachedDiscovery(provKey, cleanKey, baseUrl, liveModels);
440
+
441
+ return {
442
+ success: true,
443
+ live: true,
444
+ requiresApiKey: false,
445
+ fallback: false,
446
+ provider: provKey,
447
+ models: liveModels,
448
+ cached: false,
449
+ };
450
+ } catch (err) {
451
+ const code = typeof err.code === "number" ? err.code : 500;
452
+ const type = err.type || "server_error";
453
+ const message = err.message || `Failed to fetch models from ${provKey}`;
454
+
455
+ logger.warn(
456
+ { provider: provKey, code, type },
457
+ `[LLM Discovery] Live model fetch failed: ${message}`,
458
+ );
459
+
460
+ // Fallback seeds are labelled as recommendations, never as account-available.
461
+ return {
462
+ success: false,
463
+ live: false,
464
+ requiresApiKey: false,
465
+ fallback: true,
466
+ provider: provKey,
467
+ error: { code, type, message },
468
+ models: getSeedModels(provKey),
469
+ };
470
+ }
471
+ }
472
+
473
+ module.exports = {
474
+ DiscoveryError,
475
+ BaseDiscoveryAdapter,
476
+ OpenAIDiscoveryAdapter,
477
+ AnthropicDiscoveryAdapter,
478
+ GeminiDiscoveryAdapter,
479
+ discoverProviderModels,
480
+ ADAPTERS,
481
+ };