.
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
dist/
|
||||||
|
references/
|
||||||
|
__pycache__/
|
||||||
Generated
+10
@@ -0,0 +1,10 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Ignored default folder with query files
|
||||||
|
/queries/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
@@ -1,2 +1,66 @@
|
|||||||
# chat-summaries
|
# AI Chat Summaries Plugin for exteraGram
|
||||||
|
|
||||||
|
Advanced AI-powered Telegram chat summarization plugin for [exteraGram](https://exteragram.app).
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **ChatGPT OAuth Integration**: Secure browser authorization with automatic tier detection (Free: `luna`, `gpt-4o-mini`; Plus/Pro/Business: `terra`, `sol`, `gpt-4o`, `o1`, `o3-mini`).
|
||||||
|
- **Custom AI Provider**: OpenAI-compatible custom endpoints (OpenRouter, DeepSeek, Together AI, local vLLM/Ollama) with **Multi-Stage Verification**:
|
||||||
|
- Stage 1: URL reachability test.
|
||||||
|
- Stage 2: Lightweight probe test (`max_tokens: 10`, thinking disabled).
|
||||||
|
- Stage 3: Error logs with one-click full sanitized request log copier.
|
||||||
|
- Stage 4: Context window token budgeting (8k to 1M) preserving up to 8,000 characters per message.
|
||||||
|
- **Built-in Connectors**: Direct API key connectors for OpenAI, Anthropic Claude (with Claude 3.7 reasoning tokens), Google Gemini, and Ollama.
|
||||||
|
- **Dual Triggers**:
|
||||||
|
1. **Pinned Header AI Button**: Top-right AI action button next to the pinned message bar.
|
||||||
|
2. **Unread Badge Long-Press**: Long-press on floating unread badge or unread divider in chat with auto-filled message count.
|
||||||
|
3. **Chat Action Bar Menu**: Fallback action item for universal accessibility.
|
||||||
|
- **Animated Progress Widget**: Dynamic in-header pulsing progress indicator that shrinks `PinnedMessageView` to prevent text overlap.
|
||||||
|
- **Live Thinking Stream Preview**: Real-time streaming inspector for reasoning and thought process tokens (`reasoning_content`, `<think>...</think>`, `delta.thought`). Can be toggled off in settings to save mobile bandwidth.
|
||||||
|
- **Per-Chat Customization**: Pre-request bottom sheet with message count slider, style chips (Brief, Detailed, Key Highlights, Custom), and per-chat prompt override.
|
||||||
|
- **Rich Result Dialog**: Formatted Markdown rendering with Share, Copy Summary, Copy Full Prompt & Data, Regenerate, and Insert into Chat Composer.
|
||||||
|
- **Localization**: Full English (`en`) and Russian (`ru`) translations.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
exteragram-chat-summaries/
|
||||||
|
├── ai_chat_summary.plugin # Main entrypoint & metadata header
|
||||||
|
├── build.py # Standalone bundler & syntax validator
|
||||||
|
├── src/
|
||||||
|
│ ├── config.py # Configuration constants & model tiers
|
||||||
|
│ ├── localization.py # LocalizationManager (EN/RU dictionaries)
|
||||||
|
│ ├── providers/
|
||||||
|
│ │ ├── base.py # Base provider interface, debug logger, prompt builder
|
||||||
|
│ │ ├── oauth.py # ChatGPT OAuth handler & tier mapper
|
||||||
|
│ │ ├── custom.py # Custom provider with multi-stage verification
|
||||||
|
│ │ ├── builtin.py # OpenAI, Anthropic, Gemini, Ollama direct handlers
|
||||||
|
│ │ └── dispatcher.py # Unified dispatcher & live streaming state manager
|
||||||
|
│ ├── services/
|
||||||
|
│ │ └── message_fetcher.py # TLRPC history pagination & media formatting
|
||||||
|
│ ├── ui/
|
||||||
|
│ │ ├── settings.py # Dynamic settings layout & verification dialogs
|
||||||
|
│ │ ├── pre_request.py # Pre-request bottom sheet & prompt customizer
|
||||||
|
│ │ ├── progress_widget.py # In-header progress widget & pinned view shrinker
|
||||||
|
│ │ ├── thinking_sheet.py # Live streaming reasoning viewer
|
||||||
|
│ │ └── summary_dialog.py # Markdown result presentation & error log viewer
|
||||||
|
│ └── hooks/
|
||||||
|
│ ├── pinned_hook.py # PinnedMessageView top bar hook
|
||||||
|
│ └── unread_hook.py # ChatUnreadCell & floating badge long-press hook
|
||||||
|
├── tests/
|
||||||
|
│ └── test_plugin.py # Unit & simulation test suite
|
||||||
|
└── dist/
|
||||||
|
└── ai_chat_summary.plugin # Bundled single-file distributable artifact
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bundling & Testing
|
||||||
|
|
||||||
|
To bundle into a single-file distributable artifact:
|
||||||
|
```bash
|
||||||
|
python3 build.py
|
||||||
|
```
|
||||||
|
|
||||||
|
To run the unit and simulation test suite:
|
||||||
|
```bash
|
||||||
|
python3 -m unittest discover -s tests -v
|
||||||
|
```
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
@@ -0,0 +1,884 @@
|
|||||||
|
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 = [
|
||||||
|
"2–3 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)
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user