201 lines
7.1 KiB
Python
201 lines
7.1 KiB
Python
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()
|