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 +156 -85
- package/SETUP.md +11 -5
- package/app.cjs +6 -0
- package/package.json +5 -2
- package/public/dashboard.js +1 -1
- package/src/commands/gemini.cjs +40 -88
- package/src/commands/stt.cjs +8 -3
- package/src/config/settings.cjs +68 -8
- package/src/core/proxy.js +3 -3
- package/src/panel/session-store.cjs +100 -0
- package/src/routes/dashboard.api.esm.js +7 -2
- package/src/services/aiAgent.cjs +35 -5
- package/src/services/aiProviders.cjs +666 -0
- package/src/utils/memory.cjs +1 -1
package/README.md
CHANGED
|
@@ -1,125 +1,196 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
+
### Docker
|
|
15
62
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
54
|
-
per-chat file. Plain Markdown, hand-editable, injected into every prompt.
|
|
101
|
+
### AI that can act
|
|
55
102
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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
|
|
116
|
-
npm
|
|
180
|
+
npm ci
|
|
181
|
+
npm test
|
|
182
|
+
npm start
|
|
117
183
|
```
|
|
118
184
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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://
|
|
63
|
+
"homepage": "https://levix.leviro.net",
|
|
61
64
|
"bugs": {
|
|
62
65
|
"url": "https://github.com/Abdodiab2005/levix/issues"
|
|
63
66
|
},
|
package/public/dashboard.js
CHANGED
|
@@ -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>
|
package/src/commands/gemini.cjs
CHANGED
|
@@ -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
|
|
10
|
-
//
|
|
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
|
-
|
|
78
|
-
|
|
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
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
},
|
package/src/commands/stt.cjs
CHANGED
|
@@ -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
|
-
|
|
22
|
-
|
|
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
|
}
|