docs: полный гайд по настройке Talk бота + исходник скрипта

This commit is contained in:
Максимка
2026-02-27 00:44:39 +03:00
parent f846ed326a
commit 7d07a62d5c
2 changed files with 476 additions and 59 deletions

View 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()