docs: полный гайд по настройке Talk бота + исходник скрипта
This commit is contained in:
@@ -1,94 +1,246 @@
|
|||||||
# Nextcloud Talk Bot — Максимка
|
# Nextcloud Talk Bot — Максимка 🤖
|
||||||
|
|
||||||
## Обзор
|
## Обзор
|
||||||
AI-бот для Nextcloud Talk, который отвечает на сообщения в чате используя Claude через cliproxy (Clawdbot).
|
AI-бот в Nextcloud Talk с доступом к базе знаний. Отвечает на вопросы об инфраструктуре, серверах, настройках и проектах. Использует Claude через cliproxy (Clawdbot).
|
||||||
|
|
||||||
## Архитектура
|
## Архитектура
|
||||||
|
|
||||||
```
|
```
|
||||||
Пользователь → Nextcloud Talk (чат "Максимка")
|
Пользователь → Nextcloud Talk (чат "Максимка")
|
||||||
↓ (polling API)
|
↓ (long polling API каждые 30с)
|
||||||
Talk Bot (Python) ← порт 3401 не используется, polling
|
Talk Bot (Python скрипт на LXC 129)
|
||||||
↓
|
↓
|
||||||
cliproxy (localhost:8317) → Claude Sonnet
|
cliproxy (localhost:8317) → Claude Sonnet
|
||||||
|
↑
|
||||||
|
База знаний из Gitea (knowledge-base repo)
|
||||||
↓
|
↓
|
||||||
Ответ → Talk API → Чат
|
Ответ → Talk API → Чат (от пользователя "maximka")
|
||||||
```
|
```
|
||||||
|
|
||||||
## Компоненты
|
## Быстрый старт
|
||||||
|
|
||||||
### 1. Nextcloud Talk Bot (зарегистрированный)
|
|
||||||
- **Bot ID:** 2
|
|
||||||
- **Name:** Максимка
|
|
||||||
- **Secret:** `maximka-talk-bot-secret-key-2026-very-long-40chars`
|
|
||||||
- **Webhook URL:** `http://10.0.0.206:3401/nextcloud-talk/webhook`
|
|
||||||
- **Features:** webhook, response, event
|
|
||||||
- **Статус:** Webhook не работает (Docker networking), используем polling
|
|
||||||
|
|
||||||
### 2. Python Polling Bot
|
|
||||||
- **Скрипт:** `/root/clawd/scripts/nextcloud-talk-bot.py`
|
|
||||||
- **Метод:** Long polling через Talk API (`lookIntoFuture=1`)
|
|
||||||
- **AI:** Claude Sonnet через cliproxy на localhost:8317
|
|
||||||
- **Лог:** `/tmp/talk-bot.log`
|
|
||||||
|
|
||||||
### 3. Nextcloud пользователи
|
|
||||||
- **admin** (1qaz!QAZ) — для чтения сообщений (polling)
|
|
||||||
- **maximka** (MaximkaBot2026) — для отправки ответов
|
|
||||||
|
|
||||||
## Чат
|
|
||||||
- **Имя:** Максимка
|
|
||||||
- **Token:** `aecax6yg`
|
|
||||||
- **Участники:** admin, maximka
|
|
||||||
|
|
||||||
## Nextcloud
|
|
||||||
- **URL:** http://10.0.0.230:11001
|
|
||||||
- **Тип:** Nextcloud AIO (Docker)
|
|
||||||
- **VM:** 10.0.0.230 (cloud@10.0.0.230)
|
|
||||||
|
|
||||||
## Запуск
|
|
||||||
|
|
||||||
|
### Запуск бота
|
||||||
```bash
|
```bash
|
||||||
# Запуск бота
|
# На LXC 129 (clawdbot, 10.0.0.206)
|
||||||
nohup python3 -u /root/clawd/scripts/nextcloud-talk-bot.py > /tmp/talk-bot.log 2>&1 &
|
nohup python3 -u /root/clawd/scripts/nextcloud-talk-bot.py > /tmp/talk-bot.log 2>&1 &
|
||||||
|
```
|
||||||
|
|
||||||
# Проверка
|
### Проверка
|
||||||
|
```bash
|
||||||
cat /tmp/talk-bot.log | tail -20
|
cat /tmp/talk-bot.log | tail -20
|
||||||
|
pgrep -f nextcloud-talk-bot
|
||||||
|
```
|
||||||
|
|
||||||
# Остановка
|
### Остановка
|
||||||
|
```bash
|
||||||
pkill -f nextcloud-talk-bot
|
pkill -f nextcloud-talk-bot
|
||||||
```
|
```
|
||||||
|
|
||||||
## Управление через OCC (внутри контейнера)
|
### Обновить базу знаний (из чата Talk)
|
||||||
|
Написать `/reload` в чат — бот сделает git pull и перезагрузит KB.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Установка с нуля
|
||||||
|
|
||||||
|
### Предварительные требования
|
||||||
|
- Nextcloud с приложением Talk (протестировано на Nextcloud AIO)
|
||||||
|
- Python 3.x на машине где будет бот
|
||||||
|
- Clawdbot с cliproxy (для Claude API)
|
||||||
|
- Git репозиторий с базой знаний
|
||||||
|
|
||||||
|
### Шаг 1: Создать пользователя для бота в Nextcloud
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# На VM с Nextcloud (10.0.0.230)
|
||||||
|
# Через OCC в Docker контейнере
|
||||||
|
sudo docker exec -i -e OC_PASS='MaximkaBot2026' -u www-data \
|
||||||
|
nextcloud-aio-nextcloud php occ user:add \
|
||||||
|
--password-from-env \
|
||||||
|
--display-name="Максимка 🤖" \
|
||||||
|
maximka
|
||||||
|
|
||||||
|
# Если пользователь уже есть — сбросить пароль:
|
||||||
|
sudo docker exec -i -e OC_PASS='MaximkaBot2026' -u www-data \
|
||||||
|
nextcloud-aio-nextcloud php occ user:resetpassword --password-from-env maximka
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 2: Создать чат в Nextcloud Talk
|
||||||
|
|
||||||
|
Через веб-интерфейс Nextcloud Talk:
|
||||||
|
1. Нажать "+" → Создать групповую беседу
|
||||||
|
2. Назвать "Максимка" (или любое имя)
|
||||||
|
3. Добавить участников: admin, maximka, и других пользователей
|
||||||
|
|
||||||
|
Через API:
|
||||||
|
```bash
|
||||||
|
# Создать групповой чат
|
||||||
|
curl -s -u "admin:1qaz!QAZ" \
|
||||||
|
-X POST "http://10.0.0.230:11001/ocs/v2.php/apps/spreed/api/v4/room" \
|
||||||
|
-H "OCS-APIRequest: true" \
|
||||||
|
-d "roomType=2&roomName=Максимка"
|
||||||
|
|
||||||
|
# Добавить бота в чат
|
||||||
|
curl -s -u "admin:1qaz!QAZ" \
|
||||||
|
-X POST "http://10.0.0.230:11001/ocs/v2.php/apps/spreed/api/v4/room/{TOKEN}/participants" \
|
||||||
|
-H "OCS-APIRequest: true" \
|
||||||
|
-d "newParticipant=maximka&source=users"
|
||||||
|
```
|
||||||
|
|
||||||
|
Где `{TOKEN}` — токен чата из предыдущего ответа (у нас: `aecax6yg`).
|
||||||
|
|
||||||
|
### Шаг 3: Получить токен чата
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Список всех чатов
|
||||||
|
curl -s -u "admin:1qaz!QAZ" \
|
||||||
|
"http://10.0.0.230:11001/ocs/v2.php/apps/spreed/api/v4/room" \
|
||||||
|
-H "OCS-APIRequest: true" \
|
||||||
|
-H "Accept: application/json" | python3 -c "
|
||||||
|
import sys, json
|
||||||
|
data = json.load(sys.stdin)
|
||||||
|
for r in data['ocs']['data']:
|
||||||
|
print(f\"Token: {r['token']} | Name: {r.get('displayName','')} | Type: {r['type']}\")
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 4: Скопировать скрипт бота
|
||||||
|
|
||||||
|
Скрипт: `/root/clawd/scripts/nextcloud-talk-bot.py`
|
||||||
|
|
||||||
|
Отредактировать конфигурацию в начале файла:
|
||||||
|
```python
|
||||||
|
NC_URL = "http://10.0.0.230:11001" # URL Nextcloud
|
||||||
|
NC_USER = "admin" # Пользователь для чтения (polling)
|
||||||
|
NC_PASS = "1qaz!QAZ" # Пароль
|
||||||
|
BOT_USER = "maximka" # Пользователь для отправки ответов
|
||||||
|
BOT_PASS = "MaximkaBot2026" # Пароль бота
|
||||||
|
CONVERSATION_TOKEN = "aecax6yg" # Токен чата
|
||||||
|
|
||||||
|
CLIPROXY_URL = "http://localhost:8317/v1/messages" # Claude API
|
||||||
|
CLIPROXY_KEY = "sk-clawdbot-proxy" # API ключ
|
||||||
|
|
||||||
|
KB_PATH = "/root/clawd/knowledge-base" # Путь к базе знаний
|
||||||
|
MAX_KB_SIZE = 80000 # Макс размер KB в промпте
|
||||||
|
```
|
||||||
|
|
||||||
|
### Шаг 5: Запустить бота
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nohup python3 -u /root/clawd/scripts/nextcloud-talk-bot.py > /tmp/talk-bot.log 2>&1 &
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
### Учётные данные
|
||||||
|
| Что | Логин | Пароль | Назначение |
|
||||||
|
|-----|-------|--------|-----------|
|
||||||
|
| NC Polling | admin | 1qaz!QAZ | Чтение сообщений |
|
||||||
|
| NC Bot | maximka | MaximkaBot2026 | Отправка ответов |
|
||||||
|
| cliproxy | - | sk-clawdbot-proxy | Claude API |
|
||||||
|
|
||||||
|
### Nextcloud
|
||||||
|
- **URL:** http://10.0.0.230:11001
|
||||||
|
- **Тип:** Nextcloud AIO (Docker)
|
||||||
|
- **VM:** 250 на Proxmox (10.0.0.230)
|
||||||
|
- **SSH:** cloud@10.0.0.230
|
||||||
|
|
||||||
|
### Чат
|
||||||
|
- **Имя:** Максимка
|
||||||
|
- **Token:** aecax6yg
|
||||||
|
- **Участники:** admin, maximka
|
||||||
|
|
||||||
|
### Файлы
|
||||||
|
- **Скрипт:** `/root/clawd/scripts/nextcloud-talk-bot.py`
|
||||||
|
- **Лог:** `/tmp/talk-bot.log`
|
||||||
|
- **База знаний:** `/root/clawd/knowledge-base/`
|
||||||
|
- **Gitea repo:** http://10.0.0.189:3000/oleg/knowledge-base
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Как работает
|
||||||
|
|
||||||
|
### Polling
|
||||||
|
Бот использует Talk API с long polling (`lookIntoFuture=1&timeout=30`). Nextcloud держит HTTP соединение открытым до 30 секунд и отвечает сразу когда появляется новое сообщение.
|
||||||
|
|
||||||
|
### Join Room
|
||||||
|
Перед каждой отправкой бот делает `POST .../room/{token}/participants/active` — это "join" в разговор. Без этого Nextcloud возвращает 401.
|
||||||
|
|
||||||
|
### База знаний
|
||||||
|
При старте бот:
|
||||||
|
1. Делает `git pull` в `/root/clawd/knowledge-base`
|
||||||
|
2. Загружает все `.md` файлы (до 80KB)
|
||||||
|
3. Передаёт как первое user/assistant сообщение в контекст Claude
|
||||||
|
|
||||||
|
**Важно:** KB передаётся через user message, а не system prompt — cliproxy не передаёт system field.
|
||||||
|
|
||||||
|
### Фильтрация
|
||||||
|
Бот пропускает:
|
||||||
|
- Свои сообщения (`actorId == "maximka"`)
|
||||||
|
- Сообщения от ботов (`actorType == "bots"`)
|
||||||
|
- Системные (`messageType == "system"`)
|
||||||
|
|
||||||
|
### Команды
|
||||||
|
- `/reload` — обновить базу знаний (git pull + перезагрузка)
|
||||||
|
|
||||||
|
### История
|
||||||
|
Бот помнит последние 10 пар сообщений (user/assistant) в текущей сессии. При перезапуске история сбрасывается.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Проблемы и решения
|
||||||
|
|
||||||
|
### Webhook не работает
|
||||||
|
Nextcloud Talk Bot API поддерживает webhooks, но в AIO (Docker) setup они не триггерятся надёжно. Решение: polling.
|
||||||
|
|
||||||
|
### 401 при отправке
|
||||||
|
Причина: бот не "вошёл" в чат. Решение: `join_room()` перед каждой отправкой.
|
||||||
|
|
||||||
|
### Бот зацикливается
|
||||||
|
Причина: бот отвечает на свои же сообщения. Решение: фильтр по `actorId`.
|
||||||
|
|
||||||
|
### Бот не использует базу знаний
|
||||||
|
Причина: cliproxy не передаёт system prompt. Решение: KB как первое user/assistant сообщение.
|
||||||
|
|
||||||
|
### Бот не запускается после перезагрузки
|
||||||
|
Нужен systemd сервис (TODO).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## OCC команды (управление Talk ботами)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# SSH на VM
|
# SSH на VM
|
||||||
ssh cloud@10.0.0.230
|
ssh cloud@10.0.0.230
|
||||||
|
|
||||||
# Список ботов
|
# Все команды через docker
|
||||||
echo '1qaz!QAZ' | sudo -S docker exec -u www-data nextcloud-aio-nextcloud php occ talk:bot:list aecax6yg
|
EXEC="sudo docker exec -u www-data nextcloud-aio-nextcloud"
|
||||||
|
|
||||||
# Список чатов
|
# Установить бота (webhook-based, необязательно для polling)
|
||||||
curl -s -u "admin:1qaz!QAZ" "http://10.0.0.230:11001/ocs/v2.php/apps/spreed/api/v4/room" -H "OCS-APIRequest: true" -H "Accept: application/json"
|
$EXEC php occ talk:bot:install \
|
||||||
|
-f webhook -f response -f event \
|
||||||
|
'Максимка' 'secret-key' 'http://webhook-url/endpoint' 'Описание'
|
||||||
|
|
||||||
# Сброс пароля пользователя
|
# Список ботов в чате
|
||||||
echo '1qaz!QAZ' | sudo -S docker exec -i -e OC_PASS='NewPassword' -u www-data nextcloud-aio-nextcloud php occ user:resetpassword --password-from-env maximka
|
$EXEC php occ talk:bot:list {TOKEN}
|
||||||
|
|
||||||
|
# Добавить бота в чат
|
||||||
|
$EXEC php occ talk:bot:setup {BOT_ID} {CHAT_TOKEN}
|
||||||
|
|
||||||
|
# Удалить бота из чата
|
||||||
|
$EXEC php occ talk:bot:remove {BOT_ID} {CHAT_TOKEN}
|
||||||
|
|
||||||
|
# Удалить бота полностью
|
||||||
|
$EXEC php occ talk:bot:uninstall {BOT_ID}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Важные заметки
|
---
|
||||||
|
|
||||||
1. **Webhook не работает** — Nextcloud Talk webhook для ботов требует background jobs и работает ненадёжно в AIO setup. Поэтому используем polling.
|
|
||||||
|
|
||||||
2. **Join room обязательно** — перед отправкой сообщения от maximka нужно вызвать `POST .../room/{token}/participants/active` (join), иначе 401.
|
|
||||||
|
|
||||||
3. **Фильтрация** — бот пропускает:
|
|
||||||
- Свои сообщения (actorId == "maximka")
|
|
||||||
- Сообщения от других ботов (actorType == "bots")
|
|
||||||
- Системные сообщения (messageType == "system")
|
|
||||||
|
|
||||||
4. **Long polling** — Talk API поддерживает `lookIntoFuture=1&timeout=30` для эффективного ожидания новых сообщений.
|
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
- [ ] Systemd сервис для автозапуска
|
- [ ] Systemd сервис для автозапуска
|
||||||
- [ ] История контекста (несколько последних сообщений)
|
|
||||||
- [ ] Поддержка нескольких чатов
|
- [ ] Поддержка нескольких чатов
|
||||||
- [ ] Аватар для пользователя maximka
|
- [ ] Аватар для пользователя maximka
|
||||||
|
- [ ] Интеграция с основным Clawdbot (через hooks или sessions)
|
||||||
|
- [ ] Возможность выполнять команды (не только отвечать)
|
||||||
|
|||||||
265
projects/dttb/nextcloud-talk-bot/nextcloud-talk-bot.py
Normal file
265
projects/dttb/nextcloud-talk-bot/nextcloud-talk-bot.py
Normal file
@@ -0,0 +1,265 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Nextcloud Talk Bot — Polling-based bridge to Claude
|
||||||
|
Polls Talk API for new messages, gets AI reply with knowledge-base context, posts back.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
# === CONFIG ===
|
||||||
|
NC_URL = "http://10.0.0.230:11001"
|
||||||
|
NC_USER = "admin"
|
||||||
|
NC_PASS = "1qaz!QAZ"
|
||||||
|
BOT_USER = "maximka"
|
||||||
|
BOT_PASS = "MaximkaBot2026"
|
||||||
|
CONVERSATION_TOKEN = "aecax6yg"
|
||||||
|
|
||||||
|
CLIPROXY_URL = "http://localhost:8317/v1/messages"
|
||||||
|
CLIPROXY_KEY = "sk-clawdbot-proxy"
|
||||||
|
|
||||||
|
KB_PATH = "/root/clawd/knowledge-base"
|
||||||
|
MAX_KB_SIZE = 80000 # ~80KB max for knowledge base in prompt
|
||||||
|
|
||||||
|
# Chat history for context
|
||||||
|
chat_history = []
|
||||||
|
MAX_HISTORY = 10 # last N message pairs
|
||||||
|
|
||||||
|
|
||||||
|
def load_knowledge_base():
|
||||||
|
"""Load all .md files from knowledge-base repo into context string"""
|
||||||
|
kb_parts = []
|
||||||
|
total_size = 0
|
||||||
|
|
||||||
|
# Priority order: projects first, then snippets, decisions, notes
|
||||||
|
patterns = [
|
||||||
|
f"{KB_PATH}/README.md",
|
||||||
|
f"{KB_PATH}/CLAUDE.md",
|
||||||
|
f"{KB_PATH}/projects/**/*.md",
|
||||||
|
f"{KB_PATH}/snippets/*.md",
|
||||||
|
f"{KB_PATH}/decisions/*.md",
|
||||||
|
f"{KB_PATH}/notes/README.md",
|
||||||
|
]
|
||||||
|
|
||||||
|
seen = set()
|
||||||
|
for pattern in patterns:
|
||||||
|
for fpath in sorted(glob.glob(pattern, recursive=True)):
|
||||||
|
if fpath in seen:
|
||||||
|
continue
|
||||||
|
seen.add(fpath)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(fpath, "r") as f:
|
||||||
|
content = f.read()
|
||||||
|
if total_size + len(content) > MAX_KB_SIZE:
|
||||||
|
break
|
||||||
|
rel = os.path.relpath(fpath, KB_PATH)
|
||||||
|
kb_parts.append(f"### {rel}\n{content}")
|
||||||
|
total_size += len(content)
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return "\n\n---\n\n".join(kb_parts)
|
||||||
|
|
||||||
|
|
||||||
|
def build_system_prompt():
|
||||||
|
"""Build system prompt with knowledge base"""
|
||||||
|
kb = load_knowledge_base()
|
||||||
|
kb_size = len(kb)
|
||||||
|
|
||||||
|
prompt = (
|
||||||
|
"Ты Максимка — AI-ассистент DevOps-инженера Олега Батлаева. "
|
||||||
|
"Отвечай кратко и по делу. Используй русский язык. "
|
||||||
|
"Ты работаешь в Nextcloud Talk.\n\n"
|
||||||
|
"ВАЖНО: Ниже приведена полная база знаний инфраструктуры Олега. "
|
||||||
|
"Ты УЖЕ ЗНАЕШЬ всю его инфраструктуру — серверы, IP-адреса, пароли, проекты, решения. "
|
||||||
|
"Когда тебя спрашивают про серверы, сеть, настройки — ВСЕГДА отвечай на основе этих данных. "
|
||||||
|
"НЕ говори что у тебя нет доступа — информация уже есть ниже. "
|
||||||
|
"Отвечай конкретно, с IP-адресами, именами и деталями из базы знаний.\n\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
if kb:
|
||||||
|
prompt += f"## База знаний ({kb_size} символов)\n\n{kb}"
|
||||||
|
|
||||||
|
return prompt
|
||||||
|
|
||||||
|
|
||||||
|
def nc_request(path, method="GET", data=None, user=None, password=None):
|
||||||
|
"""Nextcloud OCS API request"""
|
||||||
|
url = f"{NC_URL}/ocs/v2.php/apps/spreed/api/{path}"
|
||||||
|
payload = json.dumps(data).encode() if data else None
|
||||||
|
req = urllib.request.Request(url, data=payload, method=method)
|
||||||
|
req.add_header("OCS-APIRequest", "true")
|
||||||
|
req.add_header("Accept", "application/json")
|
||||||
|
req.add_header("Content-Type", "application/json")
|
||||||
|
creds = base64.b64encode(f"{user or NC_USER}:{password or NC_PASS}".encode()).decode()
|
||||||
|
req.add_header("Authorization", f"Basic {creds}")
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
|
return json.loads(resp.read())
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_message_id():
|
||||||
|
"""Get the highest message ID in the conversation"""
|
||||||
|
result = nc_request(f"v1/chat/{CONVERSATION_TOKEN}?lookIntoFuture=0&limit=1")
|
||||||
|
messages = result.get("ocs", {}).get("data", [])
|
||||||
|
return max((m.get("id", 0) for m in messages), default=0)
|
||||||
|
|
||||||
|
|
||||||
|
def poll_new_messages(last_id):
|
||||||
|
"""Long-poll for new messages after last_id"""
|
||||||
|
try:
|
||||||
|
result = nc_request(
|
||||||
|
f"v1/chat/{CONVERSATION_TOKEN}?lookIntoFuture=1&limit=20"
|
||||||
|
f"&lastKnownMessageId={last_id}&timeout=30"
|
||||||
|
)
|
||||||
|
return result.get("ocs", {}).get("data", [])
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
if e.code == 304:
|
||||||
|
return []
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def join_room():
|
||||||
|
"""Join conversation as bot user"""
|
||||||
|
try:
|
||||||
|
nc_request(
|
||||||
|
f"v4/room/{CONVERSATION_TOKEN}/participants/active",
|
||||||
|
method="POST", user=BOT_USER, password=BOT_PASS
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def send_message(text, reply_to=None):
|
||||||
|
"""Send message as bot user"""
|
||||||
|
join_room()
|
||||||
|
data = {"message": text[:32000]}
|
||||||
|
if reply_to:
|
||||||
|
data["replyTo"] = reply_to
|
||||||
|
nc_request(
|
||||||
|
f"v1/chat/{CONVERSATION_TOKEN}",
|
||||||
|
method="POST", data=data, user=BOT_USER, password=BOT_PASS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ai_reply(message, author, system_prompt):
|
||||||
|
"""Get reply from Claude via cliproxy"""
|
||||||
|
global chat_history
|
||||||
|
|
||||||
|
# Build messages: KB as first user/assistant pair + history + new message
|
||||||
|
messages = [
|
||||||
|
{"role": "user", "content": f"[СИСТЕМНЫЙ КОНТЕКСТ]\n{system_prompt}\n[/СИСТЕМНЫЙ КОНТЕКСТ]\n\nПодтверди что ты Максимка и готов отвечать на вопросы."},
|
||||||
|
{"role": "assistant", "content": "Я Максимка — AI-ассистент Олега. База знаний загружена. Готов отвечать на вопросы об инфраструктуре, серверах и проектах."},
|
||||||
|
]
|
||||||
|
messages.extend(chat_history)
|
||||||
|
messages.append({"role": "user", "content": f"{author}: {message}"})
|
||||||
|
|
||||||
|
payload = json.dumps({
|
||||||
|
"model": "claude-sonnet-4-6",
|
||||||
|
"max_tokens": 2048,
|
||||||
|
"system": system_prompt,
|
||||||
|
"messages": messages
|
||||||
|
}).encode()
|
||||||
|
|
||||||
|
req = urllib.request.Request(CLIPROXY_URL, data=payload, method="POST")
|
||||||
|
req.add_header("Content-Type", "application/json")
|
||||||
|
req.add_header("x-api-key", CLIPROXY_KEY)
|
||||||
|
req.add_header("anthropic-version", "2023-06-01")
|
||||||
|
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as resp:
|
||||||
|
result = json.loads(resp.read())
|
||||||
|
content = result.get("content", [])
|
||||||
|
if content and isinstance(content, list):
|
||||||
|
reply = content[0].get("text", "")
|
||||||
|
else:
|
||||||
|
reply = str(content)
|
||||||
|
|
||||||
|
# Update history
|
||||||
|
chat_history.append({"role": "user", "content": f"{author}: {message}"})
|
||||||
|
chat_history.append({"role": "assistant", "content": reply})
|
||||||
|
|
||||||
|
# Trim history
|
||||||
|
while len(chat_history) > MAX_HISTORY * 2:
|
||||||
|
chat_history.pop(0)
|
||||||
|
chat_history.pop(0)
|
||||||
|
|
||||||
|
return reply
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("🤖 Максимка Talk Bot v5 (with Knowledge Base)")
|
||||||
|
print(f" NC: {NC_URL} | Chat: {CONVERSATION_TOKEN}")
|
||||||
|
|
||||||
|
# Git pull knowledge base
|
||||||
|
os.system(f"cd {KB_PATH} && git pull --quiet 2>/dev/null")
|
||||||
|
|
||||||
|
# Load knowledge base
|
||||||
|
system_prompt = build_system_prompt()
|
||||||
|
print(f" Knowledge base: {len(system_prompt)} chars in system prompt")
|
||||||
|
|
||||||
|
# Start from latest message
|
||||||
|
last_id = get_last_message_id()
|
||||||
|
print(f" Starting from message ID: {last_id}")
|
||||||
|
print(f" Listening...\n")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
messages = poll_new_messages(last_id)
|
||||||
|
|
||||||
|
for msg in messages:
|
||||||
|
msg_id = msg.get("id", 0)
|
||||||
|
if msg_id <= last_id:
|
||||||
|
continue
|
||||||
|
last_id = msg_id
|
||||||
|
|
||||||
|
actor_id = msg.get("actorId", "")
|
||||||
|
actor_type = msg.get("actorType", "")
|
||||||
|
actor_name = msg.get("actorDisplayName", "")
|
||||||
|
text = msg.get("message", "").strip()
|
||||||
|
msg_type = msg.get("messageType", "")
|
||||||
|
|
||||||
|
# Skip: own, system, bots, empty
|
||||||
|
if actor_id == BOT_USER:
|
||||||
|
continue
|
||||||
|
if actor_type == "bots":
|
||||||
|
continue
|
||||||
|
if msg_type in ("system", "command"):
|
||||||
|
continue
|
||||||
|
if not text:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Special commands
|
||||||
|
if text.lower() == "/reload":
|
||||||
|
os.system(f"cd {KB_PATH} && git pull --quiet 2>/dev/null")
|
||||||
|
system_prompt = build_system_prompt()
|
||||||
|
send_message(f"🔄 База знаний обновлена ({len(system_prompt)} символов)", reply_to=msg_id)
|
||||||
|
print(f"[RELOAD] KB reloaded: {len(system_prompt)} chars")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"[IN] {actor_name} (id:{msg_id}): {text[:80]}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
reply = get_ai_reply(text, actor_name, system_prompt)
|
||||||
|
if reply:
|
||||||
|
send_message(reply, reply_to=msg_id)
|
||||||
|
print(f"[OUT] → {reply[:80]}...")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERR] Reply failed: {e}")
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nBye!")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ERR] Poll: {e}")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user