levix-bot 2.2.0 → 2.3.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.
package/README.md CHANGED
@@ -1,125 +1,196 @@
1
- # Levix
1
+ <p align="center">
2
+ <img src="public/brand/banner.webp" alt="Levix - Personal WhatsApp Bot" width="100%">
3
+ </p>
4
+
5
+ <p align="center">
6
+ <a href="https://github.com/Abdodiab2005/levix/actions/workflows/ci.yml"><img src="https://github.com/Abdodiab2005/levix/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
7
+ <a href="https://www.npmjs.com/package/levix-bot"><img src="https://img.shields.io/npm/v/levix-bot?color=2563eb" alt="npm version"></a>
8
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-10b981" alt="MIT License"></a>
9
+ <img src="https://img.shields.io/badge/Node.js-24%2B-339933?logo=nodedotjs&logoColor=white" alt="Node.js 24+">
10
+ </p>
11
+
12
+ <p align="center">
13
+ <strong>Free, open-source, self-hosted WhatsApp automation that stays under your control.</strong>
14
+ </p>
15
+
16
+ <p align="center">
17
+ <a href="https://levix.leviro.net">Website</a> ·
18
+ <a href="SETUP.md">Setup guide</a> ·
19
+ <a href="https://github.com/Abdodiab2005/levix/releases/latest">Downloads</a> ·
20
+ <a href="https://github.com/Abdodiab2005/levix/issues">Issues</a>
21
+ </p>
22
+
23
+ Levix is a personal WhatsApp bot with 55 commands, group moderation, scheduled
24
+ messages, an AI agent, media tools, and a web control panel. You run it on your
25
+ own computer or server, keep its database and session files yourself, and can
26
+ change almost everything while it is running.
27
+
28
+ There is no paid tier, hosted account, external database, or configuration file
29
+ to maintain. Levix is released under the MIT License.
30
+
31
+ > [!WARNING]
32
+ > Levix uses [Baileys](https://github.com/WhiskeySockets/Baileys), an unofficial
33
+ > WhatsApp Web client, and is not affiliated with or endorsed by WhatsApp or
34
+ > Meta. Unofficial automation can lead to temporary restrictions or an account
35
+ > ban. Do not use Levix for spam or unsolicited bulk messaging. If an outage or
36
+ > account restriction is unacceptable, use the official WhatsApp Business
37
+ > Platform instead.
38
+
39
+ ## Quick start
40
+
41
+ ### Linux server
42
+
43
+ The public installer installs the latest stable release and configures Levix as
44
+ a service:
2
45
 
3
- A self-hosted personal WhatsApp bot: 55 commands, group moderation, an AI agent
4
- on Google Gemini, scheduled messages, and a web control panel that can change
5
- almost all of it while the bot runs.
46
+ ```bash
47
+ curl -fsSL https://levix.leviro.net/install.sh | bash
48
+ ```
49
+
50
+ You can [read the installer](deploy/install.sh) before running it.
6
51
 
7
- Built by Abdelrhman Diab (Leviro).
52
+ ### npm
53
+
54
+ Requires Node.js 24 or newer:
8
55
 
9
56
  ```bash
10
57
  npm install -g levix-bot
11
58
  levix
12
59
  ```
13
60
 
14
- The npm package is named `levix-bot`; the installed CLI command stays `levix`.
61
+ ### Docker
15
62
 
16
- On a desktop that opens the panel in your browser by itself. Pick a password,
17
- go to **Connection**, press **Start session** and scan the QR — see
18
- [SETUP.md](SETUP.md) for the longer version, Docker, and running it as a
19
- service.
63
+ ```bash
64
+ git clone https://github.com/Abdodiab2005/levix
65
+ cd levix
66
+ docker compose up -d
67
+ docker compose logs levix
68
+ ```
20
69
 
21
- Starting Levix does not connect it to WhatsApp. The panel, the database and the
22
- commands come up on their own, and the WhatsApp session is started from the
23
- Connection screen when you want it.
70
+ Standalone Linux, macOS ARM64, and Windows binaries are available from the
71
+ [latest GitHub release](https://github.com/Abdodiab2005/levix/releases/latest).
24
72
 
25
- ```
26
- levix start Levix with the web panel
27
- levix headless start Levix with no web UI at all
28
- levix where print the data directory
29
- levix reset-password reset the panel password
30
- levix domain [name] point a domain at Levix (may need sudo)
31
- ```
73
+ When Levix starts:
32
74
 
33
- ---
75
+ 1. Open the panel URL printed in the terminal.
76
+ 2. Choose a panel password. A remote first-time setup also asks for the printed
77
+ setup code.
78
+ 3. Open **Connection**, press **Start session**, and scan the QR from WhatsApp.
79
+ 4. Send `!ping` in a chat.
34
80
 
35
- ## What it does
81
+ A successfully linked WhatsApp session resumes automatically after a process,
82
+ Docker, or systemd restart. See [SETUP.md](SETUP.md) for domains, reverse
83
+ proxies, headless mode, backups, and troubleshooting.
36
84
 
37
- **Commands** — `!ping`, `!help`, `!calc`, `!poll`, `!rand`, `!loop`, `!todo`,
38
- `!notes`, `!debt`, `!weather`, `!prayer`, `!tts`, `!stt`, `!shortlink`,
39
- `!qr`, `!score`… every one of them can be renamed, re-permissioned or turned
40
- off from the panel.
85
+ ## What you get
41
86
 
42
- **Group moderation** — welcome messages, anti-link, anti-spam, media rules,
43
- warnings with auto-kick, rules and notes, plus the usual `!group kick`,
44
- `promote`, `demote`, `add`, `tagadmins`.
87
+ | Area | What Levix provides |
88
+ | --- | --- |
89
+ | Commands | 55 commands for utilities, notes, reminders, polls, media, speech, weather, prayer times, and more |
90
+ | Group moderation | Welcome messages, anti-link, anti-spam, media rules, warnings, auto-kick, roles, rules, and notes |
91
+ | AI agent | Pick Google Gemini, OpenAI / any OpenAI-compatible server, or Anthropic from the panel; each provider has its own endpoint, API key, and model, with tools for web search, page reading, memory, and role management |
92
+ | Scheduling | One-off, daily, and weekly messages with durable jobs, delivery status, and manual retry |
93
+ | Control panel | Live connection state, command settings, roles, permissions, keys, memory, schedules, and logs |
94
+ | Media | Text, images, video, audio, QR codes, text-to-speech, and speech-to-text |
95
+ | Deployment | npm, Docker, systemd installer, standalone binaries, headless mode, and safe domain setup |
96
+ | Storage | One SQLite database and one data directory for settings, sessions, memory, and logs |
45
97
 
46
- **An AI agent, not a chat box** — `!gemini` can search the web, open a page and
47
- read it, save something to its long-term memory and hand out bot roles, across
48
- several tool rounds, narrating the whole run inside a single message it keeps
49
- editing. Gemini's own Google Search is available to it too, so questions about
50
- current things get grounded answers with their sources listed. Its personality
51
- is a Markdown file you can edit from the panel.
98
+ Every command can be enabled, disabled, renamed, and assigned permissions from
99
+ the panel without restarting the bot.
52
100
 
53
- **Long-term memory** — "remember that…" writes to `memory/global.md` or a
54
- per-chat file. Plain Markdown, hand-editable, injected into every prompt.
101
+ ### AI that can act
55
102
 
56
- **Scheduled messages** — one-off (`!schedule`) or recurring (`!autoschedule`),
57
- including daily and weekly schedules with Arabic or English day names. Jobs
58
- survive restarts; their latest delivery result is shown in the panel, where a
59
- failed message can be retried manually.
103
+ The `!gemini` command is an agent rather than a plain chat box. It can run
104
+ several tool rounds, search the web, read a page, save long-term memories, and
105
+ manage bot roles. It narrates progress by editing one WhatsApp message and lists
106
+ sources when it uses web search.
60
107
 
61
- **Optional proxy** — Settings → WhatsApp proxy routes the WhatsApp connection
62
- through an HTTP, HTTPS or SOCKS5 proxy, including the media it sends and
63
- receives. Nothing else changes: the control panel and the AI still connect
64
- directly.
108
+ AI is optional. The rest of Levix works without an AI key.
65
109
 
66
- **A connection you control** — the panel starts, stops and unlinks the
67
- WhatsApp session, and shows what it is actually doing: waiting for a scan,
68
- connected, reconnecting (5s, 10s, 15s, 20s, 25s), or stopped. Reconnects happen
69
- in the bot whether or not a browser is open, and a WhatsApp connection that will
70
- not come back never takes the panel down with it.
110
+ ### Persistent schedules and memory
71
111
 
72
- **A panel you can also do without** — `levix headless` runs the bot with no
73
- Express, no socket.io and no port open at all; having no screen to press Start
74
- on, it connects by itself and prints its QR straight to the terminal.
112
+ One-off and recurring schedules survive restarts and record their latest
113
+ delivery outcome. Long-term memory is stored as plain Markdown in
114
+ `memory/global.md` or per-chat files, so it remains readable and editable
115
+ outside the panel.
75
116
 
76
- **A domain, without a takeover** — `levix domain bot.example.com` looks at what
77
- the server already runs and works with it: it adds one nginx or Caddy site and
78
- validates the whole configuration before reloading, and on a panel-managed or
79
- containerised host it changes nothing and prints the exact reverse-proxy
80
- settings instead.
117
+ ### A connection you control
81
118
 
82
- ---
119
+ The panel can start, stop, reconnect, and unlink WhatsApp while showing the
120
+ actual state: idle, waiting for a scan, connected, reconnecting, or stopped.
121
+ Reconnect attempts continue inside the bot even when no browser is open.
83
122
 
84
- ## How it is put together
123
+ An optional HTTP, HTTPS, or SOCKS5 proxy can route WhatsApp traffic, including
124
+ media, without changing how the panel or AI connects.
85
125
 
86
- - **Node 24+**, ES modules and CommonJS side by side (see `CLAUDE.md`).
87
- - **[Baileys v7](https://github.com/WhiskeySockets/Baileys)** for WhatsApp,
88
- including the LID system.
89
- - **SQLite through `node:sqlite`** — Node's own module, so the whole datastore
90
- is one file and zero dependencies. No database server, nothing to compile.
91
- - **Express + EJS** for the panel. No framework, no CDN, no build step.
92
- - **Google Gemini** for the AI, with an optional Groq fallback.
126
+ ## CLI
93
127
 
94
- Three things follow from that, and they are the point of the design:
128
+ ```text
129
+ levix start Levix with the web panel
130
+ levix headless start Levix with no web UI or open port
131
+ levix where print the data directory
132
+ levix reset-password reset the panel password
133
+ levix domain [name] configure a domain safely (may need sudo)
134
+ ```
135
+
136
+ A fresh panel install waits for you to start the first WhatsApp pairing.
137
+ `levix headless` has no button to press, so it starts the session itself and
138
+ prints the QR in the terminal when needed.
139
+
140
+ ## Data and configuration
141
+
142
+ Everything mutable lives in one data directory:
143
+
144
+ - npm installation: `~/.levix`
145
+ - source checkout: `./data`
146
+ - Docker: the `levix-data` volume
95
147
 
96
- **The database is the source of truth, the panel is how you edit it.** There is
97
- no `.env` file and no config file — the prefix, every permission, every API key
98
- and every tuning value is a row in `bot_settings`, and every read goes through
99
- an accessor at call time so a change applies to the next message.
148
+ Copying this directory is a full backup, including the SQLite database, WhatsApp
149
+ session, memory, and logs.
100
150
 
101
- **Secrets are generated, not configured.** The session signing key is made on
102
- first start. The panel password is chosen once, in the browser, and stored as
103
- an scrypt hash.
151
+ The database is the source of truth. The prefix, permissions, API keys, server
152
+ settings, and feature options are stored in `bot_settings` and read at use
153
+ time. There is no `.env` or application configuration file to keep in sync.
104
154
 
105
- **Everything mutable lives in one directory** — database, WhatsApp session,
106
- memory files, logs. `levix where` prints it; copying it is a full backup.
155
+ Secrets are generated rather than shipped with defaults. The panel password is
156
+ stored as an scrypt hash, and the session-signing key is generated on first
157
+ start.
107
158
 
108
- ---
159
+ ## Architecture
109
160
 
110
- ## For developers
161
+ - **Node.js 24+** with ES modules and CommonJS where needed.
162
+ - **Baileys v7** for the WhatsApp connection and LID support.
163
+ - **`node:sqlite`** for a single-file datastore with no database server or
164
+ native SQLite dependency.
165
+ - **Express + EJS** for a panel with no frontend build step or CDN dependency.
166
+ - **Pluggable AI providers** — Google Gemini, OpenAI / any OpenAI-compatible endpoint, or
167
+ Anthropic — each with its own endpoint, API key, and model in the control panel, no env file.
168
+ - **GitHub Actions** validation for tests, npm packaging, Docker persistence,
169
+ and standalone executables on Linux, macOS, and Windows.
170
+
171
+ [AGENTS.md](AGENTS.md) documents the module layout, message flow, storage API,
172
+ and architectural constraints. [PACKAGING.md](PACKAGING.md) covers npm, Docker,
173
+ systemd, installers, and single-executable releases.
174
+
175
+ ## Development
111
176
 
112
177
  ```bash
113
178
  git clone https://github.com/Abdodiab2005/levix
114
179
  cd levix
115
- npm install
116
- npm start # data lands in ./data
180
+ npm ci
181
+ npm test
182
+ npm start
117
183
  ```
118
184
 
119
- `CLAUDE.md` is the architecture document: module layout, the storage API, the
120
- message flow, and the rules a change has to respect. `PACKAGING.md` covers
121
- shipping it — npm, Docker, systemd, and the single-executable build.
185
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) before proposing a substantial
186
+ change. Bug reports and focused pull requests are welcome.
187
+
188
+ For security issues, follow [SECURITY.md](SECURITY.md) and report them
189
+ privately rather than opening a public issue.
122
190
 
123
191
  ## License
124
192
 
125
- MIT.
193
+ Levix is free and open source under the [MIT License](LICENSE).
194
+
195
+ Built by [Abdelrhman Diab](https://github.com/Abdodiab2005) under
196
+ [Leviro](https://leviro.net).
package/SETUP.md CHANGED
@@ -58,9 +58,14 @@ The terminal prints something like:
58
58
  bot before you do.)
59
59
  3. You land on the control panel. Go to **Connection**, press **Start session**,
60
60
  and scan the QR with WhatsApp → Settings → Linked devices → Link a device.
61
- Levix does not connect to WhatsApp until you ask it to.
61
+ A brand-new install does not begin pairing until you press Start.
62
62
  4. Send `!ping` in any chat. The bot answers.
63
63
 
64
+ After that first successful pairing, the saved WhatsApp session resumes
65
+ automatically after a Levix, Docker or systemd restart. You only need the
66
+ Connection screen again if you deliberately stop/unlink the session or WhatsApp
67
+ requires a new link.
68
+
64
69
  Levix only opens a browser when there is one to open: over SSH, under systemd,
65
70
  in Docker or in CI it just prints the address and says why. `--no-open` turns
66
71
  it off anywhere.
@@ -85,7 +90,8 @@ Then open <http://localhost:3001>. The setup code is in the logs:
85
90
  docker compose logs levix | grep -F '[Setup] Setup code:' | tail -1
86
91
  ```
87
92
 
88
- Everything the bot owns lives in the `levix-data` volume.
93
+ Everything the bot owns lives in the `levix-data` volume. Once WhatsApp has
94
+ been paired, restarting the container reuses that saved session automatically.
89
95
 
90
96
  ---
91
97
 
@@ -181,11 +187,11 @@ journalctl -u levix --no-pager | grep -F '[Setup] Setup code:' | tail -1
181
187
 
182
188
  Read the script first if you like — it is short, and it explains each step.
183
189
 
184
- That always installs the newest stable Levix. To stay on one version — the same
185
- script, pinned — use its own URL:
190
+ That always installs the newest stable Levix. To stay on this release — the
191
+ same script, pinned — use its own URL:
186
192
 
187
193
  ```bash
188
- curl -fsSL https://levix.leviro.net/install/v2.0.0.sh | bash
194
+ curl -fsSL https://levix.leviro.net/install/v2.2.1.sh | bash
189
195
  ```
190
196
 
191
197
  **Behind a proxy** — if this server reaches the internet through a proxy, the
package/app.cjs CHANGED
@@ -19,6 +19,7 @@ const { assetPath } = require("./src/config/paths.cjs");
19
19
  const brand = require("./src/config/brand.cjs");
20
20
  const settings = require("./src/config/settings.cjs");
21
21
  const secrets = require("./src/config/secrets.cjs");
22
+ const { PanelSessionStore } = require("./src/panel/session-store.cjs");
22
23
  const { getQrCode } = require("./src/utils/storage.cjs");
23
24
  const {
24
25
  clientAddress,
@@ -151,9 +152,14 @@ app.locals.brand = brand;
151
152
  app.use(express.static(assetPath("public")));
152
153
  app.use(express.urlencoded({ extended: true, limit: "100kb" }));
153
154
 
155
+ // Levix is intentionally one process and one operator, so a small expiring
156
+ // in-memory store is enough. Unlike express-session's development MemoryStore,
157
+ // it is explicitly bounded and prunes expired entries instead of growing for
158
+ // the lifetime of the process. Sessions still disappear on restart by design.
154
159
  const sessionMiddleware = session({
155
160
  name: SESSION_COOKIE_NAME,
156
161
  secret: secrets.getSessionSecret(),
162
+ store: new PanelSessionStore({ ttlMs: SESSION_MAX_AGE_MS }),
157
163
  resave: false,
158
164
  saveUninitialized: false,
159
165
  cookie: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "levix-bot",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "A self-hosted personal WhatsApp bot with an AI agent, group moderation and a web control panel.",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -18,6 +18,9 @@
18
18
  "bot",
19
19
  "baileys",
20
20
  "gemini",
21
+ "openai",
22
+ "anthropic",
23
+ "openai-compatible",
21
24
  "ai-agent",
22
25
  "sqlite",
23
26
  "self-hosted"
@@ -57,7 +60,7 @@
57
60
  "type": "git",
58
61
  "url": "https://github.com/Abdodiab2005/levix.git"
59
62
  },
60
- "homepage": "https://github.com/Abdodiab2005/levix",
63
+ "homepage": "https://levix.leviro.net",
61
64
  "bugs": {
62
65
  "url": "https://github.com/Abdodiab2005/levix/issues"
63
66
  },
@@ -1167,7 +1167,7 @@
1167
1167
  ${setting.choices
1168
1168
  .map(
1169
1169
  (choice) =>
1170
- `<option value="${esc(choice)}"${choice === setting.value ? " selected" : ""}>${esc(choice)}</option>`
1170
+ `<option value="${esc(choice)}"${choice === setting.value ? " selected" : ""}>${esc(setting.choiceLabels?.[choice] || choice)}</option>`
1171
1171
  )
1172
1172
  .join("")}
1173
1173
  </select>
@@ -6,8 +6,12 @@
6
6
  // * the multi-message context buffer (`!gemini add` ... `!gemini send`),
7
7
  // * image generation (`!generate`),
8
8
  // * memory management shortcuts (`!del`, `!delall`),
9
- // * the fallbacks: sanitize-and-retry when an uploaded file URI expired, and
10
- // Groq when Gemini is rate limited.
9
+ // * the fallback: sanitize-and-retry when an uploaded file URI expired.
10
+ //
11
+ // The active provider itself (gemini / openai / anthropic) is a setting, and
12
+ // the loop it dispatches to lives in services/aiAgent.cjs +
13
+ // services/aiProviders.cjs. Media and !generate/!stt stay Gemini-only — the
14
+ // other providers have no Files API to upload to.
11
15
  //
12
16
  // What moved out:
13
17
  // * the system prompt -> config/ai-persona.md (editable, hot-reloaded)
@@ -21,7 +25,6 @@
21
25
  const { GoogleGenAI } = require("@google/genai");
22
26
 
23
27
  const logger = require("../utils/logger.cjs");
24
- const aiIdentity = require("../config/ai-identity.cjs");
25
28
  const settings = require("../config/settings.cjs");
26
29
  const {
27
30
  getChatHistoryAsync,
@@ -41,6 +44,7 @@ const {
41
44
  formatSources,
42
45
  isFileReferenceError,
43
46
  sanitizeHistoryForFiles,
47
+ activeProviderKeySetting,
44
48
  } = require("../services/aiAgent.cjs");
45
49
  const { downloadContentFromMessage } = require("@whiskeysockets/baileys");
46
50
  const fs = require("fs").promises;
@@ -69,79 +73,34 @@ function setBuffer(msg, entries) {
69
73
  // One client does everything now: @google/genai folded the separate
70
74
  // GoogleAIFileManager into `ai.files`, and the model is named per request
71
75
  // instead of being baked into a model object.
72
- let geminiCache = { key: null, genAI: null };
76
+ let geminiCache = { key: null, baseUrl: null, genAI: null };
73
77
 
74
78
  function geminiClients() {
75
79
  const key = settings.get("gemini_api_key");
76
80
  if (!key) return { genAI: null };
77
- if (geminiCache.key !== key) {
78
- geminiCache = { key, genAI: new GoogleGenAI({ apiKey: key }) };
81
+ const baseUrl = String(settings.get("gemini_base_url")).replace(/\/+$/, "");
82
+ if (geminiCache.key !== key || geminiCache.baseUrl !== baseUrl) {
83
+ geminiCache = {
84
+ key,
85
+ baseUrl,
86
+ genAI: new GoogleGenAI({ apiKey: key, httpOptions: { baseUrl } }),
87
+ };
79
88
  }
80
89
  return geminiCache;
81
90
  }
82
91
 
83
- function isGeminiRateLimitError(error) {
84
- const message = `${error?.message || ""} ${error?.details || ""}`.toLowerCase();
85
- // @google/genai's ApiError carries a numeric `status`; the older shapes are
86
- // kept because they cost nothing and a queued error can predate an upgrade.
87
- const status =
88
- error?.status ||
89
- error?.statusCode ||
90
- error?.code ||
91
- error?.response?.status ||
92
- error?.error?.code;
93
-
94
- return (
95
- status === 429 ||
96
- message.includes("429") ||
97
- message.includes("rate limit") ||
98
- message.includes("resource_exhausted") ||
99
- message.includes("quota")
100
- );
101
- }
102
-
103
- async function getGroqFallbackResponse(parts) {
104
- const groqKey = settings.get("groq_api_key");
105
- if (!groqKey) throw new Error("GROQ_API_KEY is not configured");
106
-
107
- const mergedPrompt = parts
108
- .map((part) => {
109
- if (part?.text) return part.text;
110
- if (part?.fileData) return "[تم إرفاق ملف/وسائط في الرسالة]";
111
- return "";
112
- })
113
- .filter(Boolean)
114
- .join("\n");
115
-
116
- const response = await fetch("https://api.groq.com/openai/v1/chat/completions", {
117
- method: "POST",
118
- headers: {
119
- Authorization: `Bearer ${groqKey}`,
120
- "Content-Type": "application/json",
121
- },
122
- body: JSON.stringify({
123
- model: settings.get("groq_model"),
124
- messages: [
125
- {
126
- role: "system",
127
- // Same code-owned identity the Gemini path gets — the fallback must
128
- // not answer "who made you?" differently from the main model.
129
- content: `${aiIdentity.systemBlock}\n\nBe brief, direct and conversational. Answer in the language the user wrote in.`,
130
- },
131
- { role: "user", content: mergedPrompt },
132
- ],
133
- temperature: 0.7,
134
- }),
135
- });
136
-
137
- const payload = await response.json();
138
- if (!response.ok) {
139
- throw new Error(payload?.error?.message || "Groq API request failed");
92
+ async function processIncomingMedia(parts, mediaMessage, mimeOverride = null) {
93
+ // Reading media is a Gemini capability: the Files API it uploads to does not
94
+ // exist on the openai/anthropic paths. There the turn carries a note and the
95
+ // caption/question text still reaches the model.
96
+ if (settings.get("ai_provider") !== "gemini") {
97
+ const mime = mimeOverride || mediaMessage.mimetype || "ملف";
98
+ parts.push({
99
+ text: `[تم إرفاق وسائط (${mime}) — المزود الحالي لا يستطيع قراءة الوسائط]`,
100
+ });
101
+ return null;
140
102
  }
141
- return payload?.choices?.[0]?.message?.content?.trim();
142
- }
143
103
 
144
- async function processIncomingMedia(parts, mediaMessage, mimeOverride = null) {
145
104
  const { genAI } = geminiClients();
146
105
  if (!genAI) throw new Error("Gemini client unavailable");
147
106
  const tempFilePath = path.join(__dirname, `temp_media_${Date.now()}`);
@@ -392,6 +351,14 @@ module.exports = {
392
351
  );
393
352
 
394
353
  try {
354
+ // Image generation needs a model that returns image bytes, which only
355
+ // the Gemini image models do — it stays on Gemini whatever the chat
356
+ // provider is.
357
+ if (!geminiClients().genAI) {
358
+ throw new Error(
359
+ "مفتاح Gemini API غير معرف — توليد الصور يعمل على Gemini فقط"
360
+ );
361
+ }
395
362
  const fullPrompt =
396
363
  imagePrompt && quotedMsg
397
364
  ? `Prompt: ${imagePrompt} ,using quote: ${quotedMsg}`
@@ -636,11 +603,16 @@ module.exports = {
636
603
  }
637
604
  }
638
605
 
639
- if (!geminiClients().genAI) {
606
+ // Provider-aware, on purpose: when the panel picked openai or anthropic,
607
+ // the bot answers with that key and a missing Gemini key is irrelevant.
608
+ if (!settings.get(activeProviderKeySetting())) {
609
+ const provider = settings.get("ai_provider");
640
610
  return sendBotMessage(
641
611
  sock,
642
612
  chatId,
643
- { text: "خطأ في الإعدادات: مفتاح Gemini API غير معرف." },
613
+ {
614
+ text: `خطأ في الإعدادات: مفتاح مزود الذكاء الاصطناعي الحالي (${provider}) غير معرف — عدّله من لوحة التحكم.`,
615
+ },
644
616
  { replyTo: msg }
645
617
  );
646
618
  }
@@ -718,26 +690,6 @@ module.exports = {
718
690
  await status.finish(`${text}${formatSources(result.sources)}`);
719
691
  } catch (error) {
720
692
  logger.error({ err: error }, "Error in !gemini command");
721
-
722
- // Rate-limit fallback to Groq for text-only prompts.
723
- if (isGeminiRateLimitError(error) && settings.get("groq_api_key")) {
724
- if (parts.some((part) => part?.fileData)) {
725
- await status.finish(
726
- "جيميني عليه ضغط حالياً، ومش قادر أعالج الوسائط دلوقتي. حاول بعد شوية 🙏"
727
- );
728
- return;
729
- }
730
- try {
731
- const groqResponse = await getGroqFallbackResponse(parts);
732
- if (groqResponse) {
733
- await status.finish(groqResponse);
734
- return;
735
- }
736
- } catch (groqError) {
737
- logger.error({ err: groqError }, "Groq fallback failed");
738
- }
739
- }
740
-
741
693
  await status.fail(error, "حصلت مشكلة وأنا بكلم الذكاء الاصطناعي");
742
694
  }
743
695
  },
@@ -13,13 +13,18 @@ const settings = require("../config/settings.cjs");
13
13
  //
14
14
  // One @google/genai client covers both halves: `ai.files` replaced the separate
15
15
  // GoogleAIFileManager, and the model is named per request.
16
- let cache = { key: null, genAI: null };
16
+ let cache = { key: null, baseUrl: null, genAI: null };
17
17
 
18
18
  function geminiStt() {
19
19
  const key = settings.get("gemini_api_key");
20
20
  if (!key) return null;
21
- if (cache.key !== key) {
22
- cache = { key, genAI: new GoogleGenAI({ apiKey: key }) };
21
+ const baseUrl = String(settings.get("gemini_base_url")).replace(/\/+$/, "");
22
+ if (cache.key !== key || cache.baseUrl !== baseUrl) {
23
+ cache = {
24
+ key,
25
+ baseUrl,
26
+ genAI: new GoogleGenAI({ apiKey: key, httpOptions: { baseUrl } }),
27
+ };
23
28
  }
24
29
  return cache;
25
30
  }