new-qwen/bot/app.py

201 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
import json
import time
from pathlib import Path
from typing import Any
from urllib import request
from config import BotConfig
from telegram_api import TelegramAPI
STATE_FILE = Path(__file__).resolve().parent.parent / ".new-qwen" / "telegram-state.json"
def load_state() -> dict[str, Any]:
if not STATE_FILE.exists():
return {"offset": None, "sessions": {}, "auth_flows": {}}
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
def save_state(state: dict[str, Any]) -> None:
STATE_FILE.parent.mkdir(parents=True, exist_ok=True)
STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
def post_json(url: str, payload: dict[str, Any]) -> dict[str, Any]:
data = json.dumps(payload).encode("utf-8")
req = request.Request(
url,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
with request.urlopen(req, timeout=300) as response:
return json.loads(response.read().decode("utf-8"))
def get_json(url: str) -> dict[str, Any]:
with request.urlopen(url, timeout=60) as response:
return json.loads(response.read().decode("utf-8"))
def send_text_chunks(api: TelegramAPI, chat_id: int, text: str) -> None:
normalized = text or "Пустой ответ."
chunk_size = 3800
for start in range(0, len(normalized), chunk_size):
api.send_message(chat_id, normalized[start : start + chunk_size])
def ensure_auth(api: TelegramAPI, config: BotConfig, state: dict[str, Any], chat_id: int) -> bool:
status = get_json(f"{config.server_url}/api/v1/auth/status")
if status.get("authenticated"):
return True
started = post_json(f"{config.server_url}/api/v1/auth/device/start", {})
state.setdefault("auth_flows", {})[str(chat_id)] = started["flow_id"]
api.send_message(
chat_id,
"Qwen OAuth не настроен.\n"
f"Откройте ссылку:\n{started['verification_uri_complete']}\n\n"
f"Потом отправьте /auth_check {started['flow_id']}",
)
return False
def handle_message(api: TelegramAPI, config: BotConfig, state: dict[str, Any], message: dict[str, Any]) -> None:
chat_id = message["chat"]["id"]
user_id = str(message.get("from", {}).get("id", chat_id))
text = (message.get("text") or "").strip()
if not text:
api.send_message(chat_id, "Поддерживаются только текстовые сообщения.")
return
session_key = f"{chat_id}:{user_id}"
session_id = state.setdefault("sessions", {}).get(session_key)
state.setdefault("auth_flows", {})
if text == "/start":
api.send_message(
chat_id,
"new-qwen bot готов.\nКоманды: /help, /auth, /status, /session, /clear.",
)
return
if text == "/help":
api.send_message(
chat_id,
"Команды:\n"
"/auth - начать Qwen OAuth\n"
"/auth_check [flow_id] - проверить авторизацию\n"
"/status - статус OAuth и сервера\n"
"/session - показать текущую сессию\n"
"/clear - очистить контекст",
)
return
if text == "/auth":
started = post_json(f"{config.server_url}/api/v1/auth/device/start", {})
state["auth_flows"][str(chat_id)] = started["flow_id"]
api.send_message(
chat_id,
"Откройте ссылку для авторизации Qwen OAuth:\n"
f"{started['verification_uri_complete']}\n\n"
f"После подтверждения отправьте /auth_check {started['flow_id']}",
)
return
if text.startswith("/auth_check"):
parts = text.split(maxsplit=1)
flow_id = parts[1] if len(parts) == 2 else state["auth_flows"].get(str(chat_id))
if not flow_id:
api.send_message(chat_id, "Нет активного flow_id. Сначала вызови /auth.")
return
result = post_json(
f"{config.server_url}/api/v1/auth/device/poll",
{"flow_id": flow_id},
)
if result.get("done"):
state["auth_flows"].pop(str(chat_id), None)
api.send_message(chat_id, "Qwen OAuth успешно настроен.")
else:
api.send_message(chat_id, "Авторизация ещё не завершена. Повторите команду через пару секунд.")
return
if text == "/status":
status = get_json(f"{config.server_url}/api/v1/auth/status")
send_text_chunks(
api,
chat_id,
"Сервер доступен.\n"
f"OAuth: {'configured' if status.get('authenticated') else 'not configured'}\n"
f"resource_url: {status.get('resource_url')}\n"
f"expires_at: {status.get('expires_at')}",
)
return
if text == "/session":
if not session_id:
api.send_message(chat_id, "У этого чата ещё нет активной сессии.")
return
session = post_json(
f"{config.server_url}/api/v1/session/get",
{"session_id": session_id},
)
send_text_chunks(
api,
chat_id,
"session_id: {session_id}\nmessages: {count}\nlast_answer: {last_answer}".format(
session_id=session_id,
count=len(session.get("messages", [])),
last_answer=session.get("last_answer") or "-",
),
)
return
if text == "/clear":
if session_id:
post_json(f"{config.server_url}/api/v1/session/clear", {"session_id": session_id})
state["sessions"].pop(session_key, None)
api.send_message(chat_id, "Контекст сессии очищен.")
return
if not ensure_auth(api, config, state, chat_id):
return
api.send_message(chat_id, "Обрабатываю запрос...")
result = post_json(
f"{config.server_url}/api/v1/chat",
{
"session_id": session_id,
"user_id": user_id,
"message": text,
},
)
state["sessions"][session_key] = result["session_id"]
answer = result.get("answer") or "Пустой ответ от модели."
send_text_chunks(api, chat_id, answer)
def main() -> None:
config = BotConfig.load()
api = TelegramAPI(config.token)
state = load_state()
print("new-qwen bot polling started")
while True:
try:
updates = api.get_updates(state.get("offset"), config.poll_timeout)
for update in updates:
state["offset"] = update["update_id"] + 1
message = update.get("message")
if message:
handle_message(api, config, state, message)
save_state(state)
except Exception as exc:
print(f"bot loop error: {exc}")
time.sleep(3)
if __name__ == "__main__":
main()