Improve Telegram formatting and web synthesis
This commit is contained in:
parent
605c135b08
commit
0675f9a441
8 changed files with 425 additions and 508 deletions
|
|
@ -1,10 +1,10 @@
|
||||||
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here
|
||||||
GEMINI_API_KEY=your_gemini_api_key_here
|
|
||||||
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
OPENROUTER_API_KEY=your_openrouter_api_key_here
|
||||||
|
SEARXNG_URL=http://192.168.0.225:8080
|
||||||
|
|
||||||
# Optional:
|
# Optional:
|
||||||
# GEMINI_MODEL=gemini-2.5-flash-lite
|
# OPENROUTER_MODEL=openai/gpt-5.4-mini
|
||||||
# OPENROUTER_MODEL=openrouter/free
|
# Use a vision-capable OpenRouter model if you want image analysis.
|
||||||
# PERSONALITY_FILE=personality.md
|
# PERSONALITY_FILE=personality.md
|
||||||
# MEMORY_DB=marvin.sqlite
|
# MEMORY_DB=marvin.sqlite
|
||||||
# MAX_HISTORY_MESSAGES=20
|
# MAX_HISTORY_MESSAGES=20
|
||||||
|
|
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -4,5 +4,6 @@ node_modules/
|
||||||
*.sqlite-shm
|
*.sqlite-shm
|
||||||
*.sqlite-wal
|
*.sqlite-wal
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
*.log
|
||||||
.DS_Store
|
.DS_Store
|
||||||
personality.md
|
personality.md
|
||||||
|
|
|
||||||
47
README.md
47
README.md
|
|
@ -1,6 +1,6 @@
|
||||||
# Marvin Telegram Bot
|
# Marvin Telegram Bot
|
||||||
|
|
||||||
Simple Telegram bot server that sends your Telegram messages to Gemini and replies back. OpenRouter can be configured as a fallback. Recent conversation memory is stored persistently in SQLite.
|
Simple Telegram bot server that sends your Telegram messages and photos to OpenRouter and replies back. Recent conversation memory is stored persistently in SQLite.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
|
|
@ -14,16 +14,13 @@ cp .env.example .env
|
||||||
|
|
||||||
```env
|
```env
|
||||||
TELEGRAM_BOT_TOKEN=...
|
TELEGRAM_BOT_TOKEN=...
|
||||||
GEMINI_API_KEY=...
|
|
||||||
OPENROUTER_API_KEY=...
|
OPENROUTER_API_KEY=...
|
||||||
|
SEARXNG_URL=http://192.168.0.225:8080
|
||||||
```
|
```
|
||||||
|
|
||||||
Gemini is the primary provider. OpenRouter is optional, but useful as fallback.
|
Get your key from:
|
||||||
|
|
||||||
Get keys from:
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
https://aistudio.google.com/app/apikey
|
|
||||||
https://openrouter.ai/keys
|
https://openrouter.ai/keys
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -38,8 +35,7 @@ You are Marvin, a funny, slightly sarcastic assistant. Keep replies short and pl
|
||||||
Optional settings in `.env`:
|
Optional settings in `.env`:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
GEMINI_MODEL=gemini-2.5-flash-lite
|
OPENROUTER_MODEL=openai/gpt-5.4-mini
|
||||||
OPENROUTER_MODEL=openrouter/free
|
|
||||||
PERSONALITY_FILE=personality.md
|
PERSONALITY_FILE=personality.md
|
||||||
MEMORY_DB=marvin.sqlite
|
MEMORY_DB=marvin.sqlite
|
||||||
MAX_HISTORY_MESSAGES=20
|
MAX_HISTORY_MESSAGES=20
|
||||||
|
|
@ -69,18 +65,51 @@ Long-term memory commands:
|
||||||
/forget_memories
|
/forget_memories
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Web browsing
|
||||||
|
|
||||||
|
Marvin will automatically search the web when a message looks like it needs current or live information, and it will return a short sourced summary directly. It uses your self-hosted SearXNG instance.
|
||||||
|
|
||||||
|
It will also automatically generate an image when a message looks like an image-generation request.
|
||||||
|
|
||||||
|
Use `/web` if you want to force a web search.
|
||||||
|
Use `/image` if you want to force image generation.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/web latest OpenRouter gpt-5.4-mini model ID
|
||||||
|
```
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
|
|
||||||
|
Send the bot a photo with an optional caption/question and it will analyze it using the configured OpenRouter model.
|
||||||
|
|
||||||
Generate and send an image using Pollinations:
|
Generate and send an image using Pollinations:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
/image a sad robot drinking coffee in Zagreb, noir style
|
/image a sad robot drinking coffee in Zagreb, noir style
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Start the bot:
|
4. Start the bot locally:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
Then open Telegram and message your bot.
|
Then open Telegram and message your bot.
|
||||||
|
|
||||||
|
## Production setup
|
||||||
|
|
||||||
|
Recommended production setup:
|
||||||
|
|
||||||
|
1. Run Marvin as a **systemd** service
|
||||||
|
2. Keep using **polling**
|
||||||
|
3. Store config in `.env`
|
||||||
|
4. Run it as a dedicated unprivileged user
|
||||||
|
5. View logs with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u marvin -f
|
||||||
|
```
|
||||||
|
|
||||||
|
See `deploy/README.md` and `deploy/marvin.service` for an example service configuration.
|
||||||
|
|
|
||||||
31
deploy/README.md
Normal file
31
deploy/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Marvin deployment (systemd)
|
||||||
|
|
||||||
|
This project is intended to run as a systemd service using polling, a `.env` file for configuration, and a dedicated unprivileged user.
|
||||||
|
|
||||||
|
## Recommended setup
|
||||||
|
|
||||||
|
- Create a dedicated user: `marvin`
|
||||||
|
- Place the project at: `/home/marvin/marvin`
|
||||||
|
- Store secrets in: `/home/marvin/marvin/.env`
|
||||||
|
- Run the bot with systemd
|
||||||
|
- View logs with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -u marvin -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Service file
|
||||||
|
|
||||||
|
Install the provided service file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp deploy/marvin.service /etc/systemd/system/marvin.service
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable --now marvin
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- The bot uses Telegram polling (`getUpdates`), so only one instance should run at a time.
|
||||||
|
- If another copy is running, you may see a Telegram 409 conflict.
|
||||||
|
- Use `journalctl -u marvin -f` to follow logs live.
|
||||||
18
deploy/marvin.service
Normal file
18
deploy/marvin.service
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Marvin Telegram Bot
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
User=marvin
|
||||||
|
Group=marvin
|
||||||
|
WorkingDirectory=/home/marvin/marvin
|
||||||
|
EnvironmentFile=/home/marvin/marvin/.env
|
||||||
|
ExecStart=/usr/bin/node --no-warnings src/bot.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
NoNewPrivileges=true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
449
package-lock.json
generated
449
package-lock.json
generated
|
|
@ -9,119 +9,16 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^2.6.0",
|
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"telegraf": "^4.16.3"
|
"telegraf": "^4.16.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@google/genai": {
|
|
||||||
"version": "2.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.6.0.tgz",
|
|
||||||
"integrity": "sha512-HjoW3mPuEn7pnuKABJl9VbDoWDSF4nbwYKYvYYor7YjPeDxrrBxHzu2d1Prcd+BAuC4w+85UP6y7ZdcrQAoO7g==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"google-auth-library": "^10.3.0",
|
|
||||||
"p-retry": "^4.6.2",
|
|
||||||
"protobufjs": "^7.5.4",
|
|
||||||
"ws": "^8.18.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@modelcontextprotocol/sdk": "^1.25.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@modelcontextprotocol/sdk": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/aspromise": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/base64": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/codegen": {
|
|
||||||
"version": "2.0.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
|
|
||||||
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/eventemitter": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/fetch": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@protobufjs/aspromise": "^1.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/float": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/inquire": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/path": {
|
|
||||||
"version": "1.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
|
||||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/pool": {
|
|
||||||
"version": "1.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
|
||||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@protobufjs/utf8": {
|
|
||||||
"version": "1.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz",
|
|
||||||
"integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/@telegraf/types": {
|
"node_modules/@telegraf/types": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@telegraf/types/-/types-7.1.0.tgz",
|
||||||
"integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==",
|
"integrity": "sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
|
||||||
"version": "25.9.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
|
|
||||||
"integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"undici-types": ">=7.24.0 <7.24.7"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@types/retry": {
|
|
||||||
"version": "0.12.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
|
|
||||||
"integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/abort-controller": {
|
"node_modules/abort-controller": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
|
@ -134,44 +31,6 @@
|
||||||
"node": ">=6.5"
|
"node": ">=6.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/agent-base": {
|
|
||||||
"version": "7.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
|
||||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/base64-js": {
|
|
||||||
"version": "1.5.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
|
||||||
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/bignumber.js": {
|
|
||||||
"version": "9.3.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz",
|
|
||||||
"integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": "*"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/buffer-alloc": {
|
"node_modules/buffer-alloc": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz",
|
||||||
|
|
@ -188,27 +47,12 @@
|
||||||
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
|
"integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/buffer-equal-constant-time": {
|
|
||||||
"version": "1.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
|
||||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
|
||||||
"license": "BSD-3-Clause"
|
|
||||||
},
|
|
||||||
"node_modules/buffer-fill": {
|
"node_modules/buffer-fill": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz",
|
||||||
"integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==",
|
"integrity": "sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/data-uri-to-buffer": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||||
|
|
@ -238,15 +82,6 @@
|
||||||
"url": "https://dotenvx.com"
|
"url": "https://dotenvx.com"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ecdsa-sig-formatter": {
|
|
||||||
"version": "1.0.11",
|
|
||||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
|
||||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"safe-buffer": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/event-target-shim": {
|
"node_modules/event-target-shim": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
|
@ -256,150 +91,6 @@
|
||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/extend": {
|
|
||||||
"version": "3.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
|
|
||||||
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/fetch-blob": {
|
|
||||||
"version": "3.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
|
|
||||||
"integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/jimmywarting"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "paypal",
|
|
||||||
"url": "https://paypal.me/jimmywarting"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"node-domexception": "^1.0.0",
|
|
||||||
"web-streams-polyfill": "^3.0.3"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20 || >= 14.13"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/formdata-polyfill": {
|
|
||||||
"version": "4.0.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
|
||||||
"integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fetch-blob": "^3.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.20.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gaxios": {
|
|
||||||
"version": "7.1.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz",
|
|
||||||
"integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"extend": "^3.0.2",
|
|
||||||
"https-proxy-agent": "^7.0.1",
|
|
||||||
"node-fetch": "^3.3.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/gcp-metadata": {
|
|
||||||
"version": "8.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz",
|
|
||||||
"integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"gaxios": "^7.0.0",
|
|
||||||
"google-logging-utils": "^1.0.0",
|
|
||||||
"json-bigint": "^1.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/google-auth-library": {
|
|
||||||
"version": "10.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz",
|
|
||||||
"integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"base64-js": "^1.3.0",
|
|
||||||
"ecdsa-sig-formatter": "^1.0.11",
|
|
||||||
"gaxios": "^7.1.4",
|
|
||||||
"gcp-metadata": "8.1.2",
|
|
||||||
"google-logging-utils": "1.1.3",
|
|
||||||
"jws": "^4.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/google-logging-utils": {
|
|
||||||
"version": "1.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz",
|
|
||||||
"integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/https-proxy-agent": {
|
|
||||||
"version": "7.0.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
|
||||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"agent-base": "^7.1.2",
|
|
||||||
"debug": "4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/json-bigint": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"bignumber.js": "^9.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jwa": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"buffer-equal-constant-time": "^1.0.1",
|
|
||||||
"ecdsa-sig-formatter": "1.0.11",
|
|
||||||
"safe-buffer": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/jws": {
|
|
||||||
"version": "4.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
|
|
||||||
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"jwa": "^2.0.1",
|
|
||||||
"safe-buffer": "^5.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/long": {
|
|
||||||
"version": "5.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
|
||||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
|
||||||
"license": "Apache-2.0"
|
|
||||||
},
|
|
||||||
"node_modules/mri": {
|
"node_modules/mri": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||||
|
|
@ -415,57 +106,6 @@
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/node-domexception": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
|
||||||
"deprecated": "Use your platform's native DOMException instead",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/jimmywarting"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://paypal.me/jimmywarting"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.5.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/node-fetch": {
|
|
||||||
"version": "3.3.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
|
|
||||||
"integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"data-uri-to-buffer": "^4.0.0",
|
|
||||||
"fetch-blob": "^3.1.4",
|
|
||||||
"formdata-polyfill": "^4.0.10"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/node-fetch"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/p-retry": {
|
|
||||||
"version": "4.6.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz",
|
|
||||||
"integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/retry": "0.12.0",
|
|
||||||
"retry": "^0.13.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/p-timeout": {
|
"node_modules/p-timeout": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-4.1.0.tgz",
|
||||||
|
|
@ -475,59 +115,6 @@
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/protobufjs": {
|
|
||||||
"version": "7.6.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.1.tgz",
|
|
||||||
"integrity": "sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==",
|
|
||||||
"hasInstallScript": true,
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"dependencies": {
|
|
||||||
"@protobufjs/aspromise": "^1.1.2",
|
|
||||||
"@protobufjs/base64": "^1.1.2",
|
|
||||||
"@protobufjs/codegen": "^2.0.5",
|
|
||||||
"@protobufjs/eventemitter": "^1.1.1",
|
|
||||||
"@protobufjs/fetch": "^1.1.1",
|
|
||||||
"@protobufjs/float": "^1.0.2",
|
|
||||||
"@protobufjs/inquire": "^1.1.2",
|
|
||||||
"@protobufjs/path": "^1.1.2",
|
|
||||||
"@protobufjs/pool": "^1.1.0",
|
|
||||||
"@protobufjs/utf8": "^1.1.1",
|
|
||||||
"@types/node": ">=13.7.0",
|
|
||||||
"long": "^5.3.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/retry": {
|
|
||||||
"version": "0.13.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz",
|
|
||||||
"integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safe-buffer": {
|
|
||||||
"version": "5.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
|
||||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "patreon",
|
|
||||||
"url": "https://www.patreon.com/feross"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "consulting",
|
|
||||||
"url": "https://feross.org/support"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/safe-compare": {
|
"node_modules/safe-compare": {
|
||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/safe-compare/-/safe-compare-1.1.4.tgz",
|
||||||
|
|
@ -594,21 +181,6 @@
|
||||||
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
|
||||||
"version": "7.24.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
|
||||||
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/web-streams-polyfill": {
|
|
||||||
"version": "3.3.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
|
||||||
"integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
|
|
@ -624,27 +196,6 @@
|
||||||
"tr46": "~0.0.3",
|
"tr46": "~0.0.3",
|
||||||
"webidl-conversions": "^3.0.0"
|
"webidl-conversions": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"node_modules/ws": {
|
|
||||||
"version": "8.20.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
|
||||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10.0.0"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bufferutil": "^4.0.1",
|
|
||||||
"utf-8-validate": ">=5.0.2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"bufferutil": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"utf-8-validate": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"name": "marvin",
|
"name": "marvin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Telegram AI bot powered by Gemini",
|
"description": "Telegram AI bot powered by OpenRouter",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/bot.js",
|
"main": "src/bot.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
@ -11,7 +11,6 @@
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/genai": "^2.6.0",
|
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
"telegraf": "^4.16.3"
|
"telegraf": "^4.16.3"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
378
src/bot.js
378
src/bot.js
|
|
@ -2,14 +2,12 @@ import 'dotenv/config';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { DatabaseSync } from 'node:sqlite';
|
import { DatabaseSync } from 'node:sqlite';
|
||||||
import { Telegraf } from 'telegraf';
|
import { Telegraf } from 'telegraf';
|
||||||
import { GoogleGenAI } from '@google/genai';
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
TELEGRAM_BOT_TOKEN,
|
TELEGRAM_BOT_TOKEN,
|
||||||
GEMINI_API_KEY,
|
|
||||||
GEMINI_MODEL = 'gemini-2.5-flash-lite',
|
|
||||||
OPENROUTER_API_KEY,
|
OPENROUTER_API_KEY,
|
||||||
OPENROUTER_MODEL = 'openrouter/free',
|
OPENROUTER_MODEL = 'openai/gpt-5.4-mini',
|
||||||
|
SEARXNG_URL = 'http://192.168.0.225:8080',
|
||||||
PERSONALITY_FILE = 'personality.md',
|
PERSONALITY_FILE = 'personality.md',
|
||||||
MEMORY_DB = 'marvin.sqlite',
|
MEMORY_DB = 'marvin.sqlite',
|
||||||
MAX_HISTORY_MESSAGES = '20',
|
MAX_HISTORY_MESSAGES = '20',
|
||||||
|
|
@ -19,12 +17,14 @@ if (!TELEGRAM_BOT_TOKEN) {
|
||||||
throw new Error('Missing TELEGRAM_BOT_TOKEN in environment');
|
throw new Error('Missing TELEGRAM_BOT_TOKEN in environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GEMINI_API_KEY && !OPENROUTER_API_KEY) {
|
if (!OPENROUTER_API_KEY) {
|
||||||
throw new Error('Missing AI provider key: set GEMINI_API_KEY or OPENROUTER_API_KEY');
|
throw new Error('Missing OPENROUTER_API_KEY in environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
const bot = new Telegraf(TELEGRAM_BOT_TOKEN);
|
const bot = new Telegraf(TELEGRAM_BOT_TOKEN);
|
||||||
const ai = GEMINI_API_KEY ? new GoogleGenAI({ apiKey: GEMINI_API_KEY }) : null;
|
const webUserAgent = 'Mozilla/5.0 (compatible; MarvinBot/1.0; +https://github.com/hbrain/marvin)';
|
||||||
|
const webSearchResultLimit = 3;
|
||||||
|
const searxngBaseUrl = new URL(SEARXNG_URL);
|
||||||
|
|
||||||
const systemInstruction = readFileSync(PERSONALITY_FILE, 'utf8').trim();
|
const systemInstruction = readFileSync(PERSONALITY_FILE, 'utf8').trim();
|
||||||
const maxHistoryMessages = Number.parseInt(MAX_HISTORY_MESSAGES, 10);
|
const maxHistoryMessages = Number.parseInt(MAX_HISTORY_MESSAGES, 10);
|
||||||
|
|
@ -109,30 +109,212 @@ function buildSystemInstruction(chatId) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function cleanReply(reply) {
|
function cleanReply(reply) {
|
||||||
return reply.replace(/^\s*Marvin:\s*/i, '').trim();
|
return reply
|
||||||
|
.replace(/^\s*Marvin:\s*/i, '')
|
||||||
|
// Remove label variants anywhere in the reply, e.g.:
|
||||||
|
// **Personal comment:**, **Personal comment**:, Personal comment:, (Personal comment: ...)
|
||||||
|
.replace(/\(?[ \t]*\*{0,3}[ \t]*personal[ \t]+comment[ \t]*\*{0,3}[ \t]*(?::|[-–—])?[ \t]*\*{0,3}[ \t]*/giu, '')
|
||||||
|
.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatHistoryForGemini(history) {
|
function escapeHtml(text) {
|
||||||
return history
|
return text
|
||||||
.map((message) => `${message.role === 'user' ? 'User' : 'Assistant'}: ${message.content}`)
|
.replace(/&/g, '&')
|
||||||
.join('\n\n');
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askGemini(chatId, history) {
|
function formatTelegramReply(reply) {
|
||||||
if (!ai) throw new Error('Gemini is not configured');
|
const cleaned = cleanReply(reply).replace(/\r\n/g, '\n').trim();
|
||||||
|
|
||||||
const response = await ai.models.generateContent({
|
const codeBlocks = [];
|
||||||
model: GEMINI_MODEL,
|
const withPlaceholders = cleaned.replace(/```([\s\S]*?)```/g, (_, code) => {
|
||||||
contents: formatHistoryForGemini(history),
|
const token = `__CODEBLOCK_${codeBlocks.length}__`;
|
||||||
config: {
|
codeBlocks.push(`<pre><code>${escapeHtml(code.trim())}</code></pre>`);
|
||||||
systemInstruction: buildSystemInstruction(chatId),
|
return token;
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = escapeHtml(withPlaceholders)
|
||||||
|
.replace(/^###\s+(.+)$/gm, '<b>$1</b>')
|
||||||
|
.replace(/^##\s+(.+)$/gm, '<b>$1</b>')
|
||||||
|
.replace(/^#\s+(.+)$/gm, '<b>$1</b>')
|
||||||
|
.replace(/^\*\*(.+?)\*\*/gm, '<b>$1</b>')
|
||||||
|
.replace(/^__(.+?)__$/gm, '<i>$1</i>')
|
||||||
|
.replace(/`([^`\n]+)`/g, '<code>$1</code>')
|
||||||
|
.replace(/^\s*[-*]\s+/gm, '• ')
|
||||||
|
.replace(/\n{3,}/g, '\n\n');
|
||||||
|
|
||||||
|
codeBlocks.forEach((block, index) => {
|
||||||
|
html = html.replace(`__CODEBLOCK_${index}__`, block);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (html.length > 180 && !html.includes('\n')) {
|
||||||
|
html = html.replace(/(?<=[.!?])\s+(?=[A-Z0-9(])/g, '\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
return html.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendAssistantReply(ctx, reply, extra = {}) {
|
||||||
|
const html = formatTelegramReply(reply);
|
||||||
|
return ctx.reply(html, {
|
||||||
|
parse_mode: 'HTML',
|
||||||
|
disable_web_page_preview: true,
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeHtmlEntities(text) {
|
||||||
|
return text
|
||||||
|
.replace(/ /gi, ' ')
|
||||||
|
.replace(/&/gi, '&')
|
||||||
|
.replace(/"/gi, '"')
|
||||||
|
.replace(/'/gi, "'")
|
||||||
|
.replace(/</gi, '<')
|
||||||
|
.replace(/>/gi, '>');
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripHtml(html) {
|
||||||
|
return decodeHtmlEntities(
|
||||||
|
html
|
||||||
|
.replace(/<script[\s\S]*?<\/script>/gi, ' ')
|
||||||
|
.replace(/<style[\s\S]*?<\/style>/gi, ' ')
|
||||||
|
.replace(/<noscript[\s\S]*?<\/noscript>/gi, ' ')
|
||||||
|
.replace(/<[^>]+>/g, ' ')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveSearxngUrl(rawUrl) {
|
||||||
|
try {
|
||||||
|
return new URL(rawUrl, searxngBaseUrl).toString();
|
||||||
|
} catch {
|
||||||
|
return rawUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchWeb(query) {
|
||||||
|
const searchUrl = new URL('/search', searxngBaseUrl);
|
||||||
|
searchUrl.searchParams.set('q', query);
|
||||||
|
searchUrl.searchParams.set('language', 'auto');
|
||||||
|
searchUrl.searchParams.set('time_range', '');
|
||||||
|
searchUrl.searchParams.set('safesearch', '0');
|
||||||
|
searchUrl.searchParams.set('categories', 'general');
|
||||||
|
|
||||||
|
const response = await fetch(searchUrl, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': webUserAgent,
|
||||||
|
Accept: 'text/html,application/xhtml+xml',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return cleanReply(response.text?.trim() || 'I got an empty response. Try again?');
|
if (!response.ok) {
|
||||||
|
throw new Error(`Web search failed: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
const results = [];
|
||||||
|
const articleRegex = /<article\b[\s\S]*?<\/article>/gi;
|
||||||
|
let articleMatch;
|
||||||
|
|
||||||
|
while ((articleMatch = articleRegex.exec(html)) && results.length < webSearchResultLimit) {
|
||||||
|
const article = articleMatch[0];
|
||||||
|
const titleMatch = article.match(/<h3><a href="([^"]+)"[^>]*>([\s\S]*?)<\/a><\/h3>/i);
|
||||||
|
const snippetMatch = article.match(/<p class="content">([\s\S]*?)<\/p>/i);
|
||||||
|
|
||||||
|
if (!titleMatch) continue;
|
||||||
|
|
||||||
|
const url = resolveSearxngUrl(titleMatch[1]);
|
||||||
|
const title = stripHtml(titleMatch[2]);
|
||||||
|
const snippet = snippetMatch ? stripHtml(snippetMatch[1]) : '';
|
||||||
|
|
||||||
|
if (!title || !url) continue;
|
||||||
|
results.push({ title, url, snippet });
|
||||||
|
}
|
||||||
|
|
||||||
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askOpenRouter(chatId, history) {
|
function formatWebEvidence(results) {
|
||||||
|
if (results.length === 0) return 'No web search results were found.';
|
||||||
|
|
||||||
|
return results
|
||||||
|
.map((result, index) => `${index + 1}. ${result.title}\n Snippet: ${result.snippet || 'No snippet available.'}`)
|
||||||
|
.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchWebPageEvidence(url) {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
'User-Agent': webUserAgent,
|
||||||
|
Accept: 'text/html,application/xhtml+xml',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch page: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = await response.text();
|
||||||
|
const text = stripHtml(html)
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return text.slice(0, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFetchedPageEvidence(results) {
|
||||||
|
if (results.length === 0) return 'No page evidence was collected.';
|
||||||
|
|
||||||
|
return results
|
||||||
|
.map((result, index) => {
|
||||||
|
const parts = [`${index + 1}. ${result.title}`];
|
||||||
|
if (result.snippet) parts.push(` Search snippet: ${result.snippet}`);
|
||||||
|
if (result.pageText) parts.push(` Page text: ${result.pageText}`);
|
||||||
|
if (result.fetchError) parts.push(` Fetch error: ${result.fetchError}`);
|
||||||
|
return parts.join('\n');
|
||||||
|
})
|
||||||
|
.join('\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatMessagesForOpenRouter(history, image) {
|
||||||
|
if (!image) return history;
|
||||||
|
|
||||||
|
const messages = history.slice(0, -1);
|
||||||
|
const lastMessage = history.at(-1) || { role: 'user', content: 'Describe this image.' };
|
||||||
|
|
||||||
|
messages.push({
|
||||||
|
role: lastMessage.role,
|
||||||
|
content: [
|
||||||
|
{ type: 'text', text: lastMessage.content },
|
||||||
|
{ type: 'image_url', image_url: { url: `data:${image.mimeType};base64,${image.data}` } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchTelegramImage(ctx, fileId) {
|
||||||
|
const fileUrl = await ctx.telegram.getFileLink(fileId);
|
||||||
|
const response = await fetch(fileUrl);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to download Telegram image: ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mimeType = response.headers.get('content-type') || 'image/jpeg';
|
||||||
|
const buffer = Buffer.from(await response.arrayBuffer());
|
||||||
|
|
||||||
|
return {
|
||||||
|
mimeType,
|
||||||
|
data: buffer.toString('base64'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function askOpenRouter(chatId, history, image = null) {
|
||||||
if (!OPENROUTER_API_KEY) throw new Error('OpenRouter is not configured');
|
if (!OPENROUTER_API_KEY) throw new Error('OpenRouter is not configured');
|
||||||
|
|
||||||
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
const response = await fetch('https://openrouter.ai/api/v1/chat/completions', {
|
||||||
|
|
@ -147,7 +329,7 @@ async function askOpenRouter(chatId, history) {
|
||||||
model: OPENROUTER_MODEL,
|
model: OPENROUTER_MODEL,
|
||||||
messages: [
|
messages: [
|
||||||
{ role: 'system', content: buildSystemInstruction(chatId) },
|
{ role: 'system', content: buildSystemInstruction(chatId) },
|
||||||
...history,
|
...formatMessagesForOpenRouter(history, image),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
@ -163,17 +345,47 @@ async function askOpenRouter(chatId, history) {
|
||||||
return cleanReply(data.choices?.[0]?.message?.content?.trim() || 'I got an empty response. Try again?');
|
return cleanReply(data.choices?.[0]?.message?.content?.trim() || 'I got an empty response. Try again?');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function askAi(chatId, history) {
|
async function askAi(chatId, history, image = null) {
|
||||||
if (!GEMINI_API_KEY) return askOpenRouter(chatId, history);
|
return askOpenRouter(chatId, history, image);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
async function askWebAi(chatId, query) {
|
||||||
return await askGemini(chatId, history);
|
const searchResults = await searchWeb(query);
|
||||||
} catch (error) {
|
|
||||||
if (!OPENROUTER_API_KEY) throw error;
|
|
||||||
|
|
||||||
console.error('Gemini failed, falling back to OpenRouter:', error);
|
if (searchResults.length === 0) {
|
||||||
return askOpenRouter(chatId, history);
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const resultsWithPages = [];
|
||||||
|
for (const result of searchResults) {
|
||||||
|
try {
|
||||||
|
result.pageText = await fetchWebPageEvidence(result.url);
|
||||||
|
} catch (error) {
|
||||||
|
result.fetchError = error.message;
|
||||||
|
result.pageText = '';
|
||||||
|
}
|
||||||
|
resultsWithPages.push(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const webContext = [
|
||||||
|
'Answer the user directly and naturally using the evidence below.',
|
||||||
|
'Synthesize the page content into one concise answer.',
|
||||||
|
'Do not mention browsing, searching, source lists, or limitations.',
|
||||||
|
'Do not output raw URLs unless the user explicitly asks for sources.',
|
||||||
|
'If evidence is incomplete, give the best approximate answer and only mention uncertainty if absolutely necessary.',
|
||||||
|
'Prefer current facts in the evidence over memory.',
|
||||||
|
'Keep the answer concise and useful.',
|
||||||
|
'',
|
||||||
|
`User question: ${query}`,
|
||||||
|
'',
|
||||||
|
formatFetchedPageEvidence(resultsWithPages),
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const reply = await askOpenRouter(chatId, [
|
||||||
|
{ role: 'user', content: webContext },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return cleanReply(reply);
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.start((ctx) => {
|
bot.start((ctx) => {
|
||||||
|
|
@ -181,9 +393,10 @@ bot.start((ctx) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
bot.command('help', (ctx) => {
|
bot.command('help', (ctx) => {
|
||||||
ctx.reply(`Just send me a text message. I remember recent conversation using SQLite.
|
ctx.reply(`Just send me a text message, a photo with an optional caption, or use /web for browsing. I remember recent conversation using SQLite.
|
||||||
|
|
||||||
Commands:
|
Commands:
|
||||||
|
/web query - search the web and summarize current info
|
||||||
/image prompt - generate and send an image
|
/image prompt - generate and send an image
|
||||||
/remember key = value - save a permanent fact
|
/remember key = value - save a permanent fact
|
||||||
/memories - show permanent facts
|
/memories - show permanent facts
|
||||||
|
|
@ -233,6 +446,38 @@ bot.command('forget', (ctx) => {
|
||||||
ctx.reply('Recent chat history cleared for this chat. Long-term memories were kept.');
|
ctx.reply('Recent chat history cleared for this chat. Long-term memories were kept.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bot.command('web', async (ctx) => {
|
||||||
|
const query = ctx.message.text.replace(/^\/web(@\w+)?\s*/i, '').trim();
|
||||||
|
|
||||||
|
if (!query) {
|
||||||
|
await ctx.reply('Use: /web query\nExample: /web latest OpenRouter gpt-5.4-mini model ID');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await ctx.sendChatAction('typing');
|
||||||
|
rememberMessage(ctx.chat.id, 'user', `[web] ${query}`);
|
||||||
|
|
||||||
|
const reply = await askWebAi(ctx.chat.id, query);
|
||||||
|
|
||||||
|
rememberMessage(ctx.chat.id, 'assistant', reply);
|
||||||
|
await ctx.reply(reply);
|
||||||
|
} catch (error) {
|
||||||
|
await handleAiError(ctx, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function shouldGenerateImage(text) {
|
||||||
|
const normalized = text.toLowerCase();
|
||||||
|
|
||||||
|
const cues = [
|
||||||
|
/\b(image|picture|photo|illustration|art|drawing|draw|paint|painting|sketch|generate an image|create an image|make an image|generate a picture|create a picture|make a picture)\b/i,
|
||||||
|
/^\/image(@\w+)?\b/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
return cues.some((pattern) => pattern.test(normalized));
|
||||||
|
}
|
||||||
|
|
||||||
bot.command('image', async (ctx) => {
|
bot.command('image', async (ctx) => {
|
||||||
const prompt = ctx.message.text.replace(/^\/image(@\w+)?\s*/i, '').trim();
|
const prompt = ctx.message.text.replace(/^\/image(@\w+)?\s*/i, '').trim();
|
||||||
|
|
||||||
|
|
@ -251,29 +496,72 @@ bot.command('image', async (ctx) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function handleAiError(ctx, error) {
|
||||||
|
console.error('Failed to handle message:', error);
|
||||||
|
|
||||||
|
if (error?.status === 429) {
|
||||||
|
await ctx.reply('AI quota/rate limit was hit. Try again later, or change the model/API provider.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.reply('Sorry, something went wrong. Check the server logs.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleImageMessage(ctx, fileId, caption) {
|
||||||
|
try {
|
||||||
|
await ctx.sendChatAction('typing');
|
||||||
|
const image = await fetchTelegramImage(ctx, fileId);
|
||||||
|
|
||||||
|
rememberMessage(ctx.chat.id, 'user', `[image] ${caption}`);
|
||||||
|
|
||||||
|
const history = getHistory(ctx.chat.id);
|
||||||
|
const reply = await askAi(ctx.chat.id, history, image);
|
||||||
|
|
||||||
|
rememberMessage(ctx.chat.id, 'assistant', reply);
|
||||||
|
await sendAssistantReply(ctx, reply);
|
||||||
|
} catch (error) {
|
||||||
|
await handleAiError(ctx, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bot.on('photo', async (ctx) => {
|
||||||
|
const largestPhoto = ctx.message.photo.at(-1);
|
||||||
|
const caption = ctx.message.caption?.trim() || 'Describe this image.';
|
||||||
|
|
||||||
|
await handleImageMessage(ctx, largestPhoto.file_id, caption);
|
||||||
|
});
|
||||||
|
|
||||||
|
bot.on('document', async (ctx) => {
|
||||||
|
const document = ctx.message.document;
|
||||||
|
|
||||||
|
if (!document.mime_type?.startsWith('image/')) return;
|
||||||
|
|
||||||
|
const caption = ctx.message.caption?.trim() || 'Describe this image.';
|
||||||
|
await handleImageMessage(ctx, document.file_id, caption);
|
||||||
|
});
|
||||||
|
|
||||||
bot.on('text', async (ctx) => {
|
bot.on('text', async (ctx) => {
|
||||||
const text = ctx.message.text;
|
const text = ctx.message.text;
|
||||||
|
|
||||||
if (!text || text.startsWith('/')) return;
|
if (!text || text.startsWith('/')) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await ctx.sendChatAction('typing');
|
if (shouldGenerateImage(text)) {
|
||||||
rememberMessage(ctx.chat.id, 'user', text);
|
await ctx.sendChatAction('upload_photo');
|
||||||
|
const imageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(text)}?width=1024&height=1024&nologo=true&safe=true`;
|
||||||
const history = getHistory(ctx.chat.id);
|
await ctx.replyWithPhoto({ url: imageUrl }, { caption: text.slice(0, 1024) });
|
||||||
const reply = await askAi(ctx.chat.id, history);
|
|
||||||
|
|
||||||
rememberMessage(ctx.chat.id, 'assistant', reply);
|
|
||||||
await ctx.reply(reply);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to handle message:', error);
|
|
||||||
|
|
||||||
if (error?.status === 429) {
|
|
||||||
await ctx.reply('AI quota/rate limit was hit. Try again later, or change the model/API provider.');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ctx.reply('Sorry, something went wrong. Check the server logs.');
|
await ctx.sendChatAction('typing');
|
||||||
|
rememberMessage(ctx.chat.id, 'user', text);
|
||||||
|
|
||||||
|
const reply = (await askWebAi(ctx.chat.id, text)) ?? await askAi(ctx.chat.id, getHistory(ctx.chat.id));
|
||||||
|
|
||||||
|
rememberMessage(ctx.chat.id, 'assistant', reply);
|
||||||
|
await sendAssistantReply(ctx, reply);
|
||||||
|
} catch (error) {
|
||||||
|
await handleAiError(ctx, error);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue