levix-bot 2.2.1 → 3.0.0-alpha
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 -87
- package/package.json +4 -1
- 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/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,127 +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
|
-
screen starts the first session when you ask. After that first successful link,
|
|
24
|
-
the saved WhatsApp session resumes automatically after a process, Docker or
|
|
25
|
-
systemd restart unless you explicitly stop or unlink it.
|
|
70
|
+
Standalone Linux, macOS ARM64, and Windows binaries are available from the
|
|
71
|
+
[latest GitHub release](https://github.com/Abdodiab2005/levix/releases/latest).
|
|
26
72
|
|
|
27
|
-
|
|
28
|
-
levix start Levix with the web panel
|
|
29
|
-
levix headless start Levix with no web UI at all
|
|
30
|
-
levix where print the data directory
|
|
31
|
-
levix reset-password reset the panel password
|
|
32
|
-
levix domain [name] point a domain at Levix (may need sudo)
|
|
33
|
-
```
|
|
73
|
+
When Levix starts:
|
|
34
74
|
|
|
35
|
-
|
|
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.
|
|
36
80
|
|
|
37
|
-
|
|
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.
|
|
38
84
|
|
|
39
|
-
|
|
40
|
-
`!notes`, `!debt`, `!weather`, `!prayer`, `!tts`, `!stt`, `!shortlink`,
|
|
41
|
-
`!qr`, `!score`… every one of them can be renamed, re-permissioned or turned
|
|
42
|
-
off from the panel.
|
|
85
|
+
## What you get
|
|
43
86
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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 |
|
|
47
97
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
several tool rounds, narrating the whole run inside a single message it keeps
|
|
51
|
-
editing. Gemini's own Google Search is available to it too, so questions about
|
|
52
|
-
current things get grounded answers with their sources listed. Its personality
|
|
53
|
-
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.
|
|
54
100
|
|
|
55
|
-
|
|
56
|
-
per-chat file. Plain Markdown, hand-editable, injected into every prompt.
|
|
101
|
+
### AI that can act
|
|
57
102
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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.
|
|
62
107
|
|
|
63
|
-
|
|
64
|
-
through an HTTP, HTTPS or SOCKS5 proxy, including the media it sends and
|
|
65
|
-
receives. Nothing else changes: the control panel and the AI still connect
|
|
66
|
-
directly.
|
|
108
|
+
AI is optional. The rest of Levix works without an AI key.
|
|
67
109
|
|
|
68
|
-
|
|
69
|
-
WhatsApp session, and shows what it is actually doing: waiting for a scan,
|
|
70
|
-
connected, reconnecting (5s, 10s, 15s, 20s, 25s), or stopped. Reconnects happen
|
|
71
|
-
in the bot whether or not a browser is open, and a WhatsApp connection that will
|
|
72
|
-
not come back never takes the panel down with it.
|
|
110
|
+
### Persistent schedules and memory
|
|
73
111
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
77
116
|
|
|
78
|
-
|
|
79
|
-
the server already runs and works with it: it adds one nginx or Caddy site and
|
|
80
|
-
validates the whole configuration before reloading, and on a panel-managed or
|
|
81
|
-
containerised host it changes nothing and prints the exact reverse-proxy
|
|
82
|
-
settings instead.
|
|
117
|
+
### A connection you control
|
|
83
118
|
|
|
84
|
-
|
|
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.
|
|
85
122
|
|
|
86
|
-
|
|
123
|
+
An optional HTTP, HTTPS, or SOCKS5 proxy can route WhatsApp traffic, including
|
|
124
|
+
media, without changing how the panel or AI connects.
|
|
87
125
|
|
|
88
|
-
|
|
89
|
-
- **[Baileys v7](https://github.com/WhiskeySockets/Baileys)** for WhatsApp,
|
|
90
|
-
including the LID system.
|
|
91
|
-
- **SQLite through `node:sqlite`** — Node's own module, so the whole datastore
|
|
92
|
-
is one file and zero dependencies. No database server, nothing to compile.
|
|
93
|
-
- **Express + EJS** for the panel. No framework, no CDN, no build step.
|
|
94
|
-
- **Google Gemini** for the AI, with an optional Groq fallback.
|
|
126
|
+
## CLI
|
|
95
127
|
|
|
96
|
-
|
|
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
|
|
97
147
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
and every tuning value is a row in `bot_settings`, and every read goes through
|
|
101
|
-
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.
|
|
102
150
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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.
|
|
106
154
|
|
|
107
|
-
|
|
108
|
-
|
|
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.
|
|
109
158
|
|
|
110
|
-
|
|
159
|
+
## Architecture
|
|
111
160
|
|
|
112
|
-
|
|
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
|
|
113
176
|
|
|
114
177
|
```bash
|
|
115
178
|
git clone https://github.com/Abdodiab2005/levix
|
|
116
179
|
cd levix
|
|
117
|
-
npm
|
|
118
|
-
npm
|
|
180
|
+
npm ci
|
|
181
|
+
npm test
|
|
182
|
+
npm start
|
|
119
183
|
```
|
|
120
184
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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.
|
|
124
190
|
|
|
125
191
|
## License
|
|
126
192
|
|
|
127
|
-
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "levix-bot",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-alpha",
|
|
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"
|
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
|
}
|
package/src/config/settings.cjs
CHANGED
|
@@ -48,13 +48,40 @@ const SETTINGS = [
|
|
|
48
48
|
},
|
|
49
49
|
|
|
50
50
|
// --- AI ----------------------------------------------------------------
|
|
51
|
+
//
|
|
52
|
+
// One provider answers chats, chosen here and configured right below: the
|
|
53
|
+
// dashboard renders every one of these fields (the provider as a dropdown,
|
|
54
|
+
// keys as password fields), so a provider switch is a settings change, not a
|
|
55
|
+
// code change or an env file.
|
|
56
|
+
{
|
|
57
|
+
key: "ai_provider",
|
|
58
|
+
type: "string",
|
|
59
|
+
default: "gemini",
|
|
60
|
+
choices: ["gemini", "openai", "anthropic"],
|
|
61
|
+
choiceLabels: {
|
|
62
|
+
gemini: "Google Gemini",
|
|
63
|
+
openai: "OpenAI / compatible",
|
|
64
|
+
anthropic: "Anthropic Claude",
|
|
65
|
+
},
|
|
66
|
+
group: "ai",
|
|
67
|
+
label: "AI provider",
|
|
68
|
+
hint: "Which API answers chats. gemini = Google Gemini (the only one that reads media and has built-in Google Search). openai = any OpenAI-compatible chat-completions server. anthropic = Claude.",
|
|
69
|
+
},
|
|
51
70
|
{
|
|
52
71
|
key: "gemini_api_key",
|
|
53
72
|
type: "secret",
|
|
54
73
|
default: "",
|
|
55
74
|
group: "ai",
|
|
56
75
|
label: "Gemini API key",
|
|
57
|
-
hint: "Free key from aistudio.google.com/apikey
|
|
76
|
+
hint: "Free key from aistudio.google.com/apikey. Needed when the provider is gemini, and by !stt / !generate in every case.",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
key: "gemini_base_url",
|
|
80
|
+
type: "string",
|
|
81
|
+
default: "https://generativelanguage.googleapis.com",
|
|
82
|
+
group: "ai",
|
|
83
|
+
label: "Google Gemini base URL",
|
|
84
|
+
hint: "Google Generative Language API root. Change it for a proxy or compatible gateway; do not append /v1beta. Applies live to chat, media uploads, !stt and !generate.",
|
|
58
85
|
},
|
|
59
86
|
{
|
|
60
87
|
key: "gemini_model",
|
|
@@ -131,22 +158,54 @@ const SETTINGS = [
|
|
|
131
158
|
default: true,
|
|
132
159
|
group: "ai",
|
|
133
160
|
label: "Gemini Google Search",
|
|
134
|
-
hint: "Lets Gemini use Google's own search grounding when a question needs current information. Gemini only — the
|
|
161
|
+
hint: "Lets Gemini use Google's own search grounding when a question needs current information. Gemini only — the openai and anthropic providers never see it.",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
key: "openai_api_key",
|
|
165
|
+
type: "secret",
|
|
166
|
+
default: "",
|
|
167
|
+
group: "ai",
|
|
168
|
+
label: "OpenAI-compatible API key",
|
|
169
|
+
hint: "Used when the provider is openai. Works with OpenAI itself or any server that speaks the same format: OpenRouter, Together, LM Studio, ...",
|
|
135
170
|
},
|
|
136
171
|
{
|
|
137
|
-
key: "
|
|
172
|
+
key: "openai_base_url",
|
|
173
|
+
type: "string",
|
|
174
|
+
default: "https://api.openai.com/v1",
|
|
175
|
+
group: "ai",
|
|
176
|
+
label: "OpenAI-compatible base URL",
|
|
177
|
+
hint: "The /v1 root of a chat-completions server. Examples: https://openrouter.ai/api/v1, https://api.together.xyz/v1, http://127.0.0.1:11434/v1 (Ollama).",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
key: "openai_model",
|
|
181
|
+
type: "string",
|
|
182
|
+
default: "gpt-4o-mini",
|
|
183
|
+
group: "ai",
|
|
184
|
+
label: "OpenAI-compatible model",
|
|
185
|
+
hint: "Must exist on the server the base URL points at (e.g. gpt-4o-mini on OpenAI, a llama model on a self-hosted server).",
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
key: "anthropic_api_key",
|
|
138
189
|
type: "secret",
|
|
139
190
|
default: "",
|
|
140
191
|
group: "ai",
|
|
141
|
-
label: "
|
|
142
|
-
hint: "Used
|
|
192
|
+
label: "Anthropic API key",
|
|
193
|
+
hint: "Used when the provider is anthropic. Create one at console.anthropic.com.",
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
key: "anthropic_base_url",
|
|
197
|
+
type: "string",
|
|
198
|
+
default: "https://api.anthropic.com",
|
|
199
|
+
group: "ai",
|
|
200
|
+
label: "Anthropic base URL",
|
|
201
|
+
hint: "The host root — /v1/messages is appended. Change it only for a proxy or a compatible gateway.",
|
|
143
202
|
},
|
|
144
203
|
{
|
|
145
|
-
key: "
|
|
204
|
+
key: "anthropic_model",
|
|
146
205
|
type: "string",
|
|
147
|
-
default: "
|
|
206
|
+
default: "claude-sonnet-4-5",
|
|
148
207
|
group: "ai",
|
|
149
|
-
label: "
|
|
208
|
+
label: "Anthropic model",
|
|
150
209
|
},
|
|
151
210
|
{
|
|
152
211
|
key: "google_search_api_key",
|
|
@@ -466,6 +525,7 @@ function describe() {
|
|
|
466
525
|
min: definition.min ?? null,
|
|
467
526
|
max: definition.max ?? null,
|
|
468
527
|
choices: definition.choices ?? null,
|
|
528
|
+
choiceLabels: definition.choiceLabels ?? null,
|
|
469
529
|
restart: definition.restart === true,
|
|
470
530
|
source: sourceOf(definition.key),
|
|
471
531
|
};
|
package/src/core/proxy.js
CHANGED
|
@@ -25,9 +25,9 @@
|
|
|
25
25
|
// WHAT IS NOT PROXIED
|
|
26
26
|
// -------------------
|
|
27
27
|
// Nothing global is patched. The agents are handed to one `makeWASocket()`
|
|
28
|
-
// call by the session manager, so the dashboard's own HTTP server,
|
|
29
|
-
//
|
|
30
|
-
// use today.
|
|
28
|
+
// call by the session manager, so the dashboard's own HTTP server, the AI
|
|
29
|
+
// providers and every other outbound request keep using the direct connection
|
|
30
|
+
// they use today.
|
|
31
31
|
//
|
|
32
32
|
// SECRETS
|
|
33
33
|
// -------
|