Files
chat-summaries/references/chat_summary.plugin
T
2026-09-10 12:47:54 +02:00

884 lines
32 KiB
Plaintext
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 typing import Any, List
from base_plugin import BasePlugin, MenuItemData, MenuItemType
from client_utils import (
get_last_fragment, get_messages_controller,
run_on_queue, send_request, PLUGINS_QUEUE,
)
from android_utils import run_on_ui_thread, log, OnClickListener, copy_to_clipboard
from ui.settings import Header, Divider, Selector, Input, Text
from ui.bulletin import BulletinHelper
from org.telegram.messenger import AndroidUtilities
from org.telegram.ui.ActionBar import Theme
__id__ = "chat_summary"
__name__ = "Chat Summary"
__description__ = "Саммари любого чата через AI — OpenAI, Anthropic, Gemini, Ollama или свой endpoint."
__author__ = "@aaxnet"
__version__ = "2.2.0" # Обновили версию в связи с добавлением дебага и ретраев
__icon__ = "KOSHAKIEBANIYE/44"
__app_version__ = ">=12.5.1"
__sdk_version__ = ">=1.4.3.3"
__requirements__ = ["requests"]
PROVIDER_OPENAI = 0
PROVIDER_ANTHROPIC = 1
PROVIDER_GEMINI = 2
PROVIDER_OLLAMA = 3
PROVIDER_CUSTOM = 4
PROVIDERS = [
"OpenAI (GPT-4o)",
"Anthropic (Claude)",
"Gemini",
"Ollama (local)",
"Custom endpoint",
]
DEFAULT_MODELS = [
"gpt-4o",
"claude-sonnet-4-5",
"gemini-2.0-flash",
"llama3",
"",
]
STYLE_BRIEF = 0
STYLE_DETAILED = 1
STYLE_BULLETS = 2
STYLES = ["Краткий", "Подробный", "По пунктам"]
STYLE_HINTS = [
"23 sentences о главном",
"Все темы и детали",
"Список ключевых моментов",
]
LANG_AUTO = 0
LANG_RU = 1
LANG_EN = 2
LANGS = ["Авто", "Русский", "English"]
MSG_COUNTS = [50, 100, 200, 500]
MAX_CONTENT_CHARS = 48000
KEY_PREFIXES = {
PROVIDER_OPENAI: "sk-",
PROVIDER_ANTHROPIC: "sk-ant-",
PROVIDER_GEMINI: "AIza",
PROVIDER_CUSTOM: "sk-",
}
PROVIDER_MODEL_LABELS = {
PROVIDER_OPENAI: "OpenAI",
PROVIDER_ANTHROPIC: "Anthropic",
PROVIDER_GEMINI: "Gemini",
PROVIDER_OLLAMA: "Ollama",
PROVIDER_CUSTOM: "Custom",
}
def check_key_format(provider, api_key):
"""Returns (is_valid, hint_text) for a quick format check, not a real auth check."""
api_key = (api_key or "").strip()
if provider == PROVIDER_OLLAMA:
return True, "Ключ не требуется"
if not api_key:
return False, "Ключ не указан"
prefix = KEY_PREFIXES.get(provider)
if prefix and not api_key.startswith(prefix):
return False, "Похоже на неверный формат (ожидается " + prefix + "...)"
if len(api_key) < 16:
return False, "Слишком короткий ключ"
return True, "Формат похож на правильный"
def get_theme_color(key, fallback):
try:
return Theme.getColor(getattr(Theme, key))
except Exception:
return fallback
def build_system_prompt(style, lang):
lang_instructions = {
LANG_AUTO: "Respond in the same language as the conversation.",
LANG_RU: "Respond in Russian.",
LANG_EN: "Respond in English.",
}
style_instructions = {
STYLE_BRIEF: "Write a concise 2-3 sentence summary of the key points.",
STYLE_DETAILED: "Write a detailed summary covering all important topics discussed.",
STYLE_BULLETS: "Write a bullet-point list of the main topics and decisions.",
}
lang_part = lang_instructions.get(lang, "")
style_part = style_instructions.get(style, "")
return (
"You are a helpful assistant that summarizes Telegram chat conversations. "
+ style_part + " " + lang_part + " "
"Focus only on content. Do not include meta-commentary."
)
def parse_java_list(java_list):
result = {}
if not java_list:
return result
try:
size = java_list.size()
for i in range(size):
item = java_list.get(i)
try:
result[int(item.id)] = item
except Exception:
pass
except Exception:
pass
return result
def get_sender_name(raw_msg, user_map, chat_map):
fid = getattr(raw_msg, "from_id", None)
if fid is None:
fid = getattr(raw_msg, "peer_id", None)
if fid is None:
return "Unknown"
uid = getattr(fid, "user_id", None)
channel_id = getattr(fid, "channel_id", None)
chat_id = getattr(fid, "chat_id", None)
if uid:
uid = int(uid)
user = user_map.get(uid)
if user:
first = getattr(user, "first_name", "") or ""
last = getattr(user, "last_name", "") or ""
name = (first + " " + last).strip()
return name if name else "User" + str(uid)
return "User" + str(uid)
if channel_id:
channel_id = int(channel_id)
chat = chat_map.get(channel_id)
if chat:
return getattr(chat, "title", None) or "Channel" + str(channel_id)
return "Channel" + str(channel_id)
if chat_id:
chat_id = int(chat_id)
chat = chat_map.get(chat_id)
if chat:
return getattr(chat, "title", None) or "Chat" + str(chat_id)
return "Chat" + str(chat_id)
return "Unknown"
def get_message_text(msg, raw_msg):
mt = getattr(msg, "messageText", None)
if mt:
return str(mt)
raw_text = getattr(raw_msg, "message", None)
if raw_text:
return str(raw_text)
return ""
def build_transcript(messages, users=None, chats=None):
user_map = parse_java_list(users)
chat_map = parse_java_list(chats)
size = messages.size() if hasattr(messages, "size") else len(messages)
lines = []
used = 0
for i in range(size - 1, -1, -1):
try:
msg = messages.get(i) if hasattr(messages, "get") else messages[i]
raw = getattr(msg, "messageOwner", msg)
cls = type(raw).__name__
if "Service" in cls or "Empty" in cls:
continue
sender = get_sender_name(raw, user_map, chat_map)
text = get_message_text(msg, raw)
if text.strip():
lines.append(sender + ": " + text)
used += 1
except Exception:
continue
full_text = "\n".join(lines)
if len(full_text) > MAX_CONTENT_CHARS:
full_text = "...[обрезано]\n" + full_text[-MAX_CONTENT_CHARS:]
return full_text, used
PROVIDER_EMOJIS = ["🤖", "🧠", "✨", "💻", "🔗"]
LONG_SUMMARY_THRESHOLD = 600 # chars, above this we add a scroll + "via model" footer is still shown either way
def show_result(act, summary, msg_count, provider, model):
try:
from ui.alert import AlertDialogBuilder
from android.widget import LinearLayout, TextView, ScrollView
from android.view import Gravity
from android.util import TypedValue
dp = AndroidUtilities.dp
provider_label = PROVIDER_MODEL_LABELS.get(provider, "AI")
model_label = model.strip() if model and model.strip() else (
DEFAULT_MODELS[provider] if 0 <= provider < len(DEFAULT_MODELS) else ""
)
footer_text = "via " + provider_label + (" · " + model_label if model_label else "")
def on_copy(b, w):
copy_to_clipboard(summary)
BulletinHelper.show_info("Скопировано ✓")
b.dismiss()
def on_share(b, w):
try:
from android.content import Intent
intent = Intent(Intent.ACTION_SEND)
intent.setType("text/plain")
intent.putExtra(Intent.EXTRA_TEXT, summary)
act.startActivity(Intent.createChooser(intent, "Поделиться саммари"))
except Exception as ex:
log("chat_summary: share error: " + str(ex))
b.dismiss()
root = LinearLayout(act)
root.setOrientation(LinearLayout.VERTICAL)
root.setPadding(dp(24), dp(8), dp(24), dp(4))
scroll = ScrollView(act)
max_height_px = dp(360)
body = TextView(act)
body.setText(summary)
body.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15)
body.setTextColor(get_theme_color("key_dialogTextBlack", 0xFF212121))
body.setLineSpacing(dp(2), 1.0)
scroll.addView(body)
lp_scroll = LinearLayout.LayoutParams(-1, -2)
if len(summary) > LONG_SUMMARY_THRESHOLD:
lp_scroll = LinearLayout.LayoutParams(-1, max_height_px)
root.addView(scroll, lp_scroll)
if len(summary) > LONG_SUMMARY_THRESHOLD:
hint = TextView(act)
hint.setText("↕ прокрути для остального текста")
hint.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 11)
hint.setGravity(Gravity.CENTER)
hint.setTextColor(get_theme_color("key_windowBackgroundWhiteGrayText", 0xFF888888))
lp_hint = LinearLayout.LayoutParams(-1, -2)
lp_hint.topMargin = dp(6)
root.addView(hint, lp_hint)
footer = TextView(act)
footer.setText(footer_text)
footer.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 11)
footer.setGravity(Gravity.CENTER)
footer.setTextColor(get_theme_color("key_windowBackgroundWhiteGrayText", 0xFF999999))
lp_footer = LinearLayout.LayoutParams(-1, -2)
lp_footer.topMargin = dp(14)
lp_footer.bottomMargin = dp(4)
root.addView(footer, lp_footer)
builder = AlertDialogBuilder(act)
builder.set_title("✨ Саммари · " + str(msg_count) + " сообщений")
builder.set_view(root)
builder.set_positive_button("Закрыть", lambda b, w: b.dismiss())
builder.set_negative_button("Копировать", on_copy)
builder.set_neutral_button("Поделиться", on_share)
builder.show()
except Exception as e:
log("chat_summary: show_result error: " + str(e))
try:
from ui.alert import AlertDialogBuilder
builder = AlertDialogBuilder(act)
builder.set_title("✨ Саммари · " + str(msg_count) + " сообщений")
builder.set_message(summary)
builder.set_positive_button("Закрыть", lambda b, w: b.dismiss())
builder.set_negative_button("Копировать", lambda b, w: (copy_to_clipboard(summary), b.dismiss()))
builder.show()
except Exception as e2:
log("chat_summary: fallback dialog error: " + str(e2))
def show_spinner(act, title="Анализирую"):
try:
from ui.alert import AlertDialogBuilder
from android.os import Handler, Looper
d = AlertDialogBuilder(act, AlertDialogBuilder.ALERT_TYPE_SPINNER)
d.set_title(title + "…")
d.set_cancelable(False)
d.show()
dots = ["", ".", "..", "..."]
state = [0, True]
handler = Handler(Looper.getMainLooper())
def tick():
if not state[1]:
return
state[0] = (state[0] + 1) % len(dots)
try:
d.set_title(title + dots[state[0]])
except Exception:
state[1] = False
return
handler.postDelayed(tick, 500)
handler.postDelayed(tick, 500)
original_dismiss = d.dismiss
def dismiss_and_stop():
state[1] = False
try:
original_dismiss()
except Exception:
pass
d.dismiss = dismiss_and_stop
return d
except Exception as e:
log("chat_summary: spinner error: " + str(e))
return None
def dismiss_dialog(dlg):
if dlg is None:
return
try:
dlg.dismiss()
except Exception:
pass
def call_openai(api_key, model, system, content, url=None):
import requests
model = model or "gpt-4o"
url = url or "https://api.openai.com/v1/chat/completions"
resp = requests.post(
url,
headers={
"Authorization": "Bearer " + api_key,
"Content-Type": "application/json",
},
json={
"model": model,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": content},
],
},
timeout=60,
)
resp.raise_for_status()
return resp.json()["choices"][0]["message"]["content"]
def call_anthropic(api_key, model, system, content):
import requests
model = model or "claude-sonnet-4-5"
resp = requests.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"Content-Type": "application/json",
},
json={
"model": model,
"max_tokens": 1024,
"system": system,
"messages": [{"role": "user", "content": content}],
},
timeout=60,
)
resp.raise_for_status()
return resp.json()["content"][0]["text"]
def call_gemini(api_key, model, system, content):
import requests
model = model or "gemini-2.0-flash"
url = (
"https://generativelanguage.googleapis.com/v1beta/models/"
+ model
+ ":generateContent?key="
+ api_key
)
resp = requests.post(
url,
json={"contents": [{"parts": [{"text": system + "\n\n" + content}]}]},
timeout=60,
)
resp.raise_for_status()
return resp.json()["candidates"][0]["content"]["parts"][0]["text"]
def call_ollama(model, system, content):
import requests
model = model or "llama3"
resp = requests.post(
"http://localhost:11434/api/chat",
json={
"model": model,
"stream": False,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": content},
],
},
timeout=120,
)
resp.raise_for_status()
return resp.json()["message"]["content"]
# ОБНОВЛЕНО: Добавлен алгоритм Exponential Backoff для обработки ошибки 429
def call_ai(provider, api_key, model, system, content, custom_url):
import time
import random
max_retries = 3 # Количество попыток автоматического перезапуска
delay = 2 # Начальная задержка в секундах
for attempt in range(max_retries):
try:
if provider == PROVIDER_OPENAI:
return call_openai(api_key, model, system, content)
if provider == PROVIDER_ANTHROPIC:
return call_anthropic(api_key, model, system, content)
if provider == PROVIDER_GEMINI:
return call_gemini(api_key, model, system, content)
if provider == PROVIDER_OLLAMA:
return call_ollama(model, system, content)
if provider == PROVIDER_CUSTOM:
return call_openai(api_key, model, system, content, url=custom_url)
raise ValueError("Unknown provider: " + str(provider))
except Exception as e:
err_msg = str(e)
# Если словили 429 (Превышение лимитов) и лимит попыток не исчерпан
if "429" in err_msg and attempt < max_retries - 1:
sleep_time = delay + random.uniform(0, 1)
log("chat_summary: Hit 429 Rate Limit. Retrying in " + str(round(sleep_time, 2)) + "s...")
time.sleep(sleep_time)
delay *= 2 # Удваиваем время ожидания для следующего шага
continue
# Во всех остальных случаях (или если попытки кончились) выбрасываем ошибку дальше
raise e
# ОБНОВЛЕНО: Добавлено явное и красивое описание ошибки 429 при тесте связи
def test_connection(provider, api_key, model, custom_url):
"""Sends a minimal ping request, returns (ok, message)."""
try:
system = "Reply with exactly one word: OK."
content = "ping"
result = call_ai(provider, api_key, model, system, content, custom_url)
if result and result.strip():
return True, "Подключение работает ✓"
return False, "Пустой ответ от сервера"
except Exception as e:
msg = str(e)
if "429" in msg:
return False, "Превышен лимит запросов (ошибка 429). Подожди немного."
if "401" in msg or "Unauthorized" in msg:
return False, "Неверный API ключ"
if "404" in msg:
return False, "Модель не найдена"
if "timeout" in msg.lower() or "timed out" in msg.lower():
return False, "Таймаут — сервер не отвечает"
return False, "Ошибка: " + msg[:80]
class ChatSummaryPlugin(BasePlugin):
def on_plugin_load(self):
self.last_error = "Ошибок пока не зафиксировано" # Трекер для Debug-кнопки
self.add_menu_item(MenuItemData(
menu_type=MenuItemType.CHAT_ACTION_MENU,
text="Summarize Chat",
subtext="Краткое AI-саммари",
icon="msg_info",
on_click=self.on_menu_click,
))
def on_plugin_unload(self):
pass
def on_menu_click(self, context):
dialog_id = context.get("dialog_id")
if not dialog_id:
return
fragment = context.get("fragment") or get_last_fragment()
if not fragment:
return
act = fragment.getParentActivity()
if not act:
return
provider = self.get_setting("provider", PROVIDER_OPENAI)
api_key = self.get_setting("api_key", "") or ""
if provider != PROVIDER_OLLAMA and not api_key.strip():
BulletinHelper.show_error("Сначала укажи API ключ в настройках плагина")
return
run_on_ui_thread(lambda: self.show_count_picker(act, dialog_id))
def show_count_picker(self, act, dialog_id):
try:
from android.widget import LinearLayout, TextView
from android.view import Gravity
from android.util import TypedValue
from android.graphics.drawable import GradientDrawable
from org.telegram.ui.ActionBar import BottomSheet
dp = AndroidUtilities.dp
sheet_holder = [None]
provider = self.get_setting("provider", PROVIDER_OPENAI)
emoji = PROVIDER_EMOJIS[provider] if 0 <= provider < len(PROVIDER_EMOJIS) else "🤖"
provider_name = PROVIDERS[provider] if 0 <= provider < len(PROVIDERS) else "AI"
def pick(count):
if sheet_holder[0]:
sheet_holder[0].dismiss()
BulletinHelper.show_info("Загружаю " + str(count) + " сообщений…")
run_on_queue(
lambda: self.fetch_and_summarize(act, dialog_id, count),
PLUGINS_QUEUE, 0,
)
root = LinearLayout(act)
root.setOrientation(LinearLayout.VERTICAL)
root.setPadding(dp(20), dp(20), dp(20), dp(28))
try:
root.setBackgroundColor(Theme.getColor(Theme.key_dialogBackground))
except Exception:
pass
icon_tv = TextView(act)
icon_tv.setText(emoji)
icon_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 36)
icon_tv.setGravity(Gravity.CENTER)
lp = LinearLayout.LayoutParams(-1, -2)
lp.bottomMargin = dp(8)
root.addView(icon_tv, lp)
title = TextView(act)
title.setText("Сколько сообщений?")
title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18)
title.setGravity(Gravity.CENTER)
title.setTextColor(get_theme_color("key_dialogTextBlack", 0xFF212121))
try:
title.setTypeface(AndroidUtilities.bold())
except Exception:
pass
lp = LinearLayout.LayoutParams(-1, -2)
lp.bottomMargin = dp(4)
root.addView(title, lp)
sub = TextView(act)
sub.setText("Провайдер: " + provider_name)
sub.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13)
sub.setGravity(Gravity.CENTER)
sub.setTextColor(get_theme_color("key_windowBackgroundWhiteGrayText", 0xFF888888))
lp = LinearLayout.LayoutParams(-1, -2)
lp.bottomMargin = dp(20)
root.addView(sub, lp)
count_hints = ["быстро", "оптимально", "подробно", "полный анализ"]
for i, count in enumerate(MSG_COUNTS):
row = LinearLayout(act)
row.setOrientation(LinearLayout.HORIZONTAL)
row.setGravity(Gravity.CENTER_VERTICAL)
row.setPadding(dp(16), dp(13), dp(16), dp(13))
row.setClickable(True)
row.setFocusable(True)
bg = GradientDrawable()
bg.setCornerRadius(dp(14))
try:
accent = Theme.getColor(Theme.key_windowBackgroundWhiteBlueHeader)
if i == 1:
bg.setColor(accent & 0x22FFFFFF | 0x15000000)
else:
bg.setColor(get_theme_color("key_windowBackgroundWhite", 0xFFF5F5F5))
except Exception:
bg.setColor(0xFFF5F5F5)
row.setBackground(bg)
label = TextView(act)
label.setText(str(count) + " сообщений")
label.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15)
label.setTextColor(get_theme_color("key_windowBackgroundWhiteBlackText", 0xFF212121))
try:
if i == 1:
label.setTypeface(AndroidUtilities.bold())
except Exception:
pass
lp_label = LinearLayout.LayoutParams(0, -2, 1.0)
row.addView(label, lp_label)
hint = TextView(act)
hint_text = count_hints[i] if i < len(count_hints) else ""
if i == 1:
hint_text = "⭐ " + hint_text
hint.setText(hint_text)
hint.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12)
hint.setTextColor(get_theme_color("key_windowBackgroundWhiteGrayText", 0xFF888888))
hint.setGravity(Gravity.END)
row.addView(hint)
n = count
row.setOnClickListener(OnClickListener(lambda v, c=n: pick(c)))
lp = LinearLayout.LayoutParams(-1, -2)
lp.bottomMargin = dp(8)
root.addView(row, lp)
sheet = BottomSheet.Builder(act).setCustomView(root).create()
sheet_holder[0] = sheet
sheet.show()
except Exception as e:
log("chat_summary: count picker error: " + str(e))
def fetch_and_summarize(self, act, dialog_id, count):
try:
from org.telegram.tgnet import TLRPC
req = TLRPC.TL_messages_getHistory()
req.peer = get_messages_controller().getInputPeer(int(dialog_id))
req.offset_id = 0
req.offset_date = 0
req.add_offset = 0
req.limit = count
req.max_id = 0
req.min_id = 0
req.hash = 0
self_ref = [self]
act_ref = [act]
def on_response(response, error):
if error:
err = getattr(error, "text", "unknown")
run_on_ui_thread(lambda: BulletinHelper.show_error("Ошибка загрузки: " + err))
return
try:
msgs = getattr(response, "messages", None)
if not msgs:
run_on_ui_thread(lambda: BulletinHelper.show_error("Нет сообщений."))
return
users = getattr(response, "users", None)
chats = getattr(response, "chats", None)
text, used = build_transcript(msgs, users, chats)
if not text.strip():
run_on_ui_thread(lambda: BulletinHelper.show_error("Текстовых сообщений не найдено."))
return
spinner = [None]
def start_spinner():
spinner[0] = show_spinner(act_ref[0])
run_on_ui_thread(start_spinner)
run_on_queue(
lambda: self_ref[0].run_ai(act_ref[0], text, used, spinner),
PLUGINS_QUEUE, 0,
)
except Exception as ex:
log("chat_summary: parse error: " + str(ex))
run_on_ui_thread(lambda: BulletinHelper.show_error("Ошибка парсинга сообщений."))
send_request(req, on_response)
except Exception as e:
log("chat_summary: fetch error: " + str(e))
run_on_ui_thread(lambda: BulletinHelper.show_error("Не удалось начать загрузку."))
def run_ai(self, act, text, used, spinner):
try:
provider = self.get_setting("provider", PROVIDER_OPENAI)
api_key = self.get_setting("api_key", "") or ""
model = self.get_setting("model", "") or ""
style = self.get_setting("style", STYLE_BRIEF)
lang = self.get_setting("lang", LANG_AUTO)
custom_url = self.get_setting("custom_endpoint", "") or ""
system = build_system_prompt(style, lang)
content = "Chat conversation to summarize:\n\n" + text
result = call_ai(provider, api_key, model, system, content, custom_url)
def on_done():
dismiss_dialog(spinner[0])
show_result(act, result, used, provider, model)
run_on_ui_thread(on_done)
except Exception as e:
err = str(e)
self.last_error = "Ошибка в run_ai: " + err # ОБНОВЛЕНО: запись лога
log("chat_summary: ai error: " + err)
def on_error():
dismiss_dialog(spinner[0])
BulletinHelper.show_error("Ошибка AI: " + err[:120])
run_on_ui_thread(on_error)
def create_settings(self):
provider = self.get_setting("provider", PROVIDER_OPENAI)
style = self.get_setting("style", STYLE_BRIEF)
api_key = self.get_setting("api_key", "") or ""
default_model = DEFAULT_MODELS[provider] if 0 <= provider < len(DEFAULT_MODELS) else ""
style_hint = STYLE_HINTS[style] if 0 <= style < len(STYLE_HINTS) else ""
key_valid, key_hint = check_key_format(provider, api_key)
key_icon = "msg_invite" if key_valid else "msg_secret"
key_subtext = ("✓ " if key_valid else "✗ ") + key_hint
items = [
Header(text="AI провайдер"),
Selector(
key="provider",
text="Провайдер",
default=PROVIDER_OPENAI,
items=PROVIDERS,
icon="msg_robot",
on_change=lambda idx: self.set_setting("provider", idx, reload_settings=True),
),
Input(
key="api_key",
text="API ключ",
subtext=key_subtext,
icon=key_icon,
default="",
on_change=lambda val: self.set_setting("api_key", val, reload_settings=True),
),
Input(
key="model",
text="Модель",
subtext="По умолчанию: " + default_model if default_model else "Укажи название модели",
icon="msg_topic",
default="",
),
]
if provider == PROVIDER_CUSTOM:
items.append(Input(
key="custom_endpoint",
text="URL endpoint",
subtext="OpenAI-совместимый адрес",
icon="msg_link",
default="",
))
items.append(Text(
text="Проверить подключение",
subtext="Отправит тестовый запрос к API",
icon="msg_pin_code",
on_click=lambda v: self.run_connection_test(),
))
# ОБНОВЛЕНО: Добавлена кнопка сохранения отладочного лога
def action_copy_debug():
provider_str = PROVIDERS[provider] if 0 <= provider < len(PROVIDERS) else str(provider)
key_valid, key_hint = check_key_format(provider, api_key)
debug_text = (
"=== CHAT SUMMARY DEBUG LOG ===\n"
"Plugin Version: " + str(__version__) + "\n"
"SDK Version: " + str(__sdk_version__) + "\n"
"Provider: " + str(provider_str) + "\n"
"Model: " + str(self.get_setting("model", "")) + "\n"
"Has Custom Endpoint: " + str(bool(self.get_setting("custom_endpoint", ""))) + "\n"
"API Key Valid Format: " + str(key_valid) + " (" + str(key_hint) + ")\n"
"Style: " + str(self.get_setting("style", 0)) + "\n"
"Lang: " + str(self.get_setting("lang", 0)) + "\n"
"Last Recorded Error: " + str(getattr(self, "last_error", "None")) + "\n"
"=============================="
)
copy_to_clipboard(debug_text)
BulletinHelper.show_success("Дебаг-лог скопирован в буфер!")
items.append(Text(
text="Скопировать дебаг-лог",
subtext="Скопирует статус плагина и последнюю ошибку для отправки разработчику",
icon="msg_edit",
on_click=lambda v: action_copy_debug(),
))
items += [
Divider(),
Header(text="Саммари"),
Selector(
key="style",
text="Стиль",
default=STYLE_BRIEF,
items=STYLES,
icon="msg_list",
on_change=lambda idx: self.set_setting("style", idx, reload_settings=True),
),
Text(
text=style_hint,
icon="msg_info",
),
Selector(
key="lang",
text="Язык ответа",
default=LANG_AUTO,
items=LANGS,
icon="msg_translate",
),
]
return items
def run_connection_test(self):
provider = self.get_setting("provider", PROVIDER_OPENAI)
api_key = self.get_setting("api_key", "") or ""
model = self.get_setting("model", "") or ""
custom_url = self.get_setting("custom_endpoint", "") or ""
if provider != PROVIDER_OLLAMA and not api_key.strip():
BulletinHelper.show_error("Сначала укажи API ключ")
return
BulletinHelper.show_info("Проверяю подключение…")
def worker():
ok, message = test_connection(provider, api_key, model, custom_url)
# ОБНОВЛЕНО: Если тест провалился, записываем сообщение в лог
if not ok:
self.last_error = "Ошибка теста связи: " + message
def show():
if ok:
BulletinHelper.show_success(message)
else:
BulletinHelper.show_error(message)
run_on_ui_thread(show)
run_on_queue(worker, PLINS_QUEUE if 'PLINS_QUEUE' in globals() else PLUGINS_QUEUE, 0)