497 lines
No EOL
26 KiB
Markdown
497 lines
No EOL
26 KiB
Markdown
# Mistral Chat App - Development Context
|
||
|
||
## Project Overview
|
||
|
||
Android-приложение для чата с Mistral AI. Перспективный проект с развитием в сторону AI-агента с памятью, tools и автономной работой.
|
||
|
||
**Основные технологии:**
|
||
- Kotlin + Android (minSdk 26, targetSdk 34)
|
||
- Room + SQLCipher (encrypted database)
|
||
- OkHttp для API
|
||
- Material Design 3
|
||
- Russian language UI
|
||
|
||
**Расположение проекта:**
|
||
```
|
||
/Users/alexabudaev/Documents/Zed/mistral-chat-app/
|
||
```
|
||
|
||
**⚠️ ВАЖНО: Принципы разработки**
|
||
- Приложение должно работать БЕЗ платных подписок и инвестиций
|
||
- Всегда использовать собственные разработки или бесплатные решения
|
||
- Не рассчитывать на имеющиеся платные API при планировании функций
|
||
- Сначала находим бесплатное решение, потом реализуем
|
||
- **Если что-то невозможно сделать без платных API или ты не можешь понять задачу - говори честно!**
|
||
- При планировании любого следующего этапа следует тщательно анализировать возможность реализовать ту или иную функцию не только исходя из программной совместимости, но и с учётом наличия бесплатных версий необходимых сервисов, API и других продуктов (или возможности написать собственное решение)
|
||
|
||
---
|
||
|
||
## Completed Work
|
||
|
||
### ✅ Core Features
|
||
- Чат с Mistral API (Chat Completion)
|
||
- Управление профилями (до 10 профилей)
|
||
- Управление сессиями (множественные чаты)
|
||
- **Генерация названия сессии** - после 2-го сообщения AI генерирует краткое название (3-5 слов)
|
||
- Шифрованное хранилище (SQLCipher + EncryptedSharedPreferences)
|
||
- Валидация API ключа (32+ символов, A-Z, a-z, 0-9)
|
||
- Левое drawer-меню с диалогами
|
||
- Тёмная/светлая тема
|
||
|
||
### ✅ UI/UX
|
||
- Material Design 3
|
||
- Русский язык интерфейса
|
||
- Отступы в поле ввода (12dp)
|
||
- Прокрутка к новым сообщениям
|
||
- **Долгий тап на сообщение** - меню Копировать/Редактировать/Удалить
|
||
|
||
### ✅ Security
|
||
- API ключ: EncryptedSharedPreferences (AES-256-GCM)
|
||
- Ключ БД: EncryptedSharedPreferences (AES-256-SIV + AES-256-GCM)
|
||
- Профили, сессии, сообщения: SQLCipher
|
||
|
||
---
|
||
|
||
## Current Issues & Architecture
|
||
|
||
### ⚠️ Важное: Назначение web_search
|
||
|
||
web_search НЕ является интерфейсом поисковика или Wikipedia. Это инструмент для AI-агента:
|
||
|
||
**Правильная логика работы (Kai-style):**
|
||
```
|
||
1. AI получает вопрос пользователя
|
||
2. AI решает что нужен поиск → вызывает web_search
|
||
3. Выполняются ВСЕ tool_calls параллельно
|
||
4. Результаты НЕ показываются пользователю - только отправляются AI
|
||
5. AI интерпретирует результаты → выдаёт ОДИН финальный ответ
|
||
```
|
||
|
||
**Проблемы с текущей реализацией:**
|
||
- ❌ Показываем промежуточные ответы пользователю (каждый tool result = сообщение)
|
||
- ❌ AI получает результаты и отвечает после КАЖДОГО tool_calls
|
||
- ❌ AI выводит куски данных вместо интерпретации
|
||
|
||
**Требуется исправление:**
|
||
- ✅ Выполнить ВСЕ tool_calls за один проход (уже делаем)
|
||
- ✅ Результаты НЕ показывать пользователю (только AI видит)
|
||
- ✅ AI интерпретирует и выдаёт ОДИН ответ
|
||
|
||
### 🔍 Web Search (Текущая реализация - БЕСПЛАТНОЕ решение)
|
||
|
||
**Используется:** Russian Wikipedia API (бесплатно, без API ключа)
|
||
- **API:** `https://ru.wikipedia.org/w/api.php`
|
||
- **Метод:** `query/list/search` - поиск статей по заголовкам
|
||
- **Ограничение результатов:** до 10 статей (параметр `num_results`)
|
||
- **Ограничение символов:** 4000 символов на ответ
|
||
- **ПРИМЕЧАНИЕ:** Это временное решение! Позже можно добавить платный API для полноценного поиска (новости, погода, актуальная информация)
|
||
|
||
**Логика работы:**
|
||
1. AI вызывает `web_search` с текстовым запросом
|
||
2. Выполняется поиск по Wikipedia API
|
||
3. Результаты (заголовки + сниппеты) обрезаются до 4000 символов
|
||
4. Результаты отправляются AI для интерпретации
|
||
5. AI выдаёт ОДИН финальный ответ пользователю
|
||
|
||
**Tool Loop (MainActivity):**
|
||
- Максимум итераций: 15
|
||
- Timeout на итерацию: 60 секунд
|
||
- Retry (до 2 попыток) при ошибке "stream was reset: CANCEL"
|
||
- AI может сделать несколько последовательных поисков если нужно
|
||
|
||
### 🌤️ Weather Tool (БЕСПЛАТНОЕ решение)
|
||
|
||
**Используется:** Open-Meteo API (полностью бесплатно, без API ключа)
|
||
- **Geocoding API:** `https://geocoding-api.open-meteo.com/v1/search` - определение координат города
|
||
- **Weather API:** `https://api.open-meteo.com/v1/forecast` - текущая погода + прогноз на 7 дней
|
||
- **Параметры:** температура, ветер, погодные коды, осадки
|
||
|
||
**Логика работы:**
|
||
1. AI вызывает `get_weather` с названием города
|
||
2. Определяются координаты через Geocoding API
|
||
3. Запрашивается погода по координатам
|
||
4. Возвращается текущая погода + прогноз на 7 дней
|
||
|
||
### 🔗 OpenUrlTool (Часть Phase 3)
|
||
**Статус:** ✅ Реализовано | **Оценка:** 1 день
|
||
|
||
**Назначение:** Позволяет AI парсить любую веб-страницу по URL.
|
||
|
||
**Гибридная схема (AI сам решает откуда взять URL):**
|
||
1. **RSS-ленты новостей** (рекомендуется):
|
||
- lenta.ru → https://lenta.ru/rss/
|
||
- kommersant.ru → https://www.kommersant.ru/rss/news.xml
|
||
2. **Из памяти** - AI помнит рабочие URL
|
||
3. **Через web_search** - находит URL в интернете
|
||
4. **От пользователя** - пользователь может передать URL
|
||
|
||
**Логика работы с новостями:**
|
||
1. Получив запрос о новостях → сначала проверь память пользователя (предпочтения по темам)
|
||
2. Открой RSS-ленту через open_url (самый эффективный способ)
|
||
3. Составь сводку с учётом интересов пользователя
|
||
4. Если источники недоступны → используй web_search
|
||
5. Проверь память приложения
|
||
6. Выдай ответ на основе всех доступных источников
|
||
|
||
**Реализация:**
|
||
- HTTP GET запрос к любому URL
|
||
- Возврат ТОЛЬКО текста (удаляются HTML теги)
|
||
- Ограничение: 4000 символов
|
||
- Таймаут: 10 секунд
|
||
- Блокировка опасных URL (javascript:, file:, data:)
|
||
|
||
---
|
||
|
||
### 📚 Изучено из Kai (open-source AI assistant)
|
||
|
||
Kai имеет отличную документацию по tools: https://kai9000.com/docs/features/tools/
|
||
|
||
**Ключевые решения из Kai:**
|
||
|
||
1. **Execution Flow:**
|
||
- Все tool calls выполняются параллельно (coroutine async/await)
|
||
- TOOL_EXECUTING показывается в UI как "пульсирующий индикатор"
|
||
- Результаты НЕ показываются пользователю - только отправляются AI
|
||
- AI может вызвать еще tool calls → цикл повторяется
|
||
- Когда AI отвечает без tool_calls → финальный текст показан пользователю
|
||
|
||
2. **Safety Guards (важно!):**
|
||
- Iteration limit: максимум 15 итераций
|
||
- Repeated call detection: если одинаковый tool с одинаковыми аргументами вызывается 3 раза подряд → остановка
|
||
- Timeout: 30 секунд по умолчанию
|
||
- Result truncation: результаты > 8000 символов обрезаются
|
||
- Context trimming: между итерациями обрезается история сообщений
|
||
|
||
3. **Web Search в Kai:**
|
||
- Есть встроенный web_search tool
|
||
- Работает (вероятно использует платный API или свой парсинг)
|
||
|
||
---
|
||
|
||
## Active Plan (Phases 1-3)
|
||
|
||
### Phase 1: Расширенные профили (Extended Profiles)
|
||
**Статус:** ✅ Завершена | **Оценка:** 1-2 дня
|
||
|
||
Добавлено поле `systemPrompt` в профиль для отправки как role: "system".
|
||
|
||
| Задача | Статус |
|
||
|--------|--------|
|
||
| Profile entity | ✅ Добавлено поле systemPrompt |
|
||
| Profile dialog UI | ✅ Добавлен EditText с maxLength=4000 |
|
||
| ProfileDao | ✅ CRUD работает |
|
||
| MainActivity | ✅ Инжектирует systemPrompt как role: "system" |
|
||
| MistralClient | ✅ Использует msg.role |
|
||
|
||
---
|
||
|
||
### Phase 2: Система памяти (Memory System)
|
||
**Статус:** ✅ Завершена | **Оценка:** 2-3 дня
|
||
|
||
Система запоминания информации с категориями и hitCount.
|
||
|
||
| Задача | Статус |
|
||
|--------|--------|
|
||
| Memory entity | ✅ key, value, category, hitCount, timestamps |
|
||
| MemoryDao | ✅ CRUD + getByCategory, incrementHitCount, getPromotionCandidates |
|
||
| ChatDatabase | ✅ Добавлен MemoryDao, version=2 |
|
||
| MemoryRepository | ✅ buildMemoryContext() для инжекции в prompt |
|
||
|
||
**Memory categories:**
|
||
- GENERAL — общие факты
|
||
- LEARNING — выводы и паттерны
|
||
- ERROR — известные ошибки
|
||
- PREFERENCE — предпочтения пользователя
|
||
- REMINDER_CAL — напоминания календаря (local mode)
|
||
- CALENDAR_ERROR — ошибки подключения CalDAV
|
||
|
||
**Prompt injection:**
|
||
```
|
||
=== Важная информация ===
|
||
[Факты]
|
||
- ключ: значение
|
||
|
||
[Выводы]
|
||
- ключ: значение (N использований)
|
||
|
||
[Предпочтения пользователя]
|
||
- ключ: значение
|
||
```
|
||
|
||
---
|
||
|
||
### Phase 3: Tools / Tool Execution
|
||
**Статус:** ✅ Завершена (тестирование) | **Оценка:** 3-4 дня
|
||
|
||
Инструменты для AI (function calling) для выполнения действий.
|
||
|
||
| Задача | Статус |
|
||
|--------|--------|
|
||
| Tool abstract class | ✅ name, description, inputSchema, executor |
|
||
| GetTimeTool | ✅ get_local_time с timezone |
|
||
| GetDateTool | ✅ get_date с timezone |
|
||
| WebSearchTool | ✅ Протестировано (только Wikipedia) |
|
||
| GetWeatherTool | ✅ Протестировано (Open-Meteo API) |
|
||
| NotificationTool | ✅ send_notification |
|
||
| MemoryStoreTool | ✅ Протестировано |
|
||
| MemoryLearnTool | ✅ Протестировано |
|
||
| MemoryForgetTool | ✅ Протестировано |
|
||
| MemoryReinforceTool | ✅ Протестировано |
|
||
| MemoryPreferenceTool | ✅ Протестировано |
|
||
| ToolExecutor | ✅ управление всеми tools, updateSettings() |
|
||
| MistralClient | ✅ tools в chat completion, обработка tool_calls |
|
||
| Safety | ✅ Max iterations (15), timeout (30s), result truncation (2000 chars) |
|
||
| **OpenUrlTool (RSS)** | ✅ Автоматическое определение и парсинг RSS/Atom |
|
||
|
||
**CalDAV Calendar + Local Reminders (Phase 3 extension):**
|
||
| Задача | Статус |
|
||
|--------|--------|
|
||
| iCalDAV зависимость (Apache 2.0) | ⏳ |
|
||
| CalDavRepository (CRUD) | ⏳ |
|
||
| UI: drawer_menu → диалог настроек CalDAV | ⏳ |
|
||
| Настройка синхронизации (15м-сутки) | ⏳ |
|
||
| caldav_get_events, create, update, delete | ⏳ |
|
||
| Memory category REMINDER_CAL | ⏳ |
|
||
| calendar_add_reminder tool | ⏳ |
|
||
| calendar_get_reminders tool | ⏳ |
|
||
| Напоминания (любой период: 5мин, 13мин, 2ч, 24ч) | ⏳ |
|
||
| Счётчик ошибок CalDAV → memory | ⏳ |
|
||
| UnifiedPush (опционально, на потом) | ⏳ |
|
||
|
||
**Memory REMINDER_CAL fields:**
|
||
- key: название напоминания
|
||
- value: описание
|
||
- triggerTime: unix timestamp когда напомнить
|
||
- status: pending / triggered / expired
|
||
|
||
**Trigger logic:** AI проверяет pending напоминания при каждом запросе
|
||
|
||
**RSS-ленты (протестировано):**
|
||
- lenta.ru/rss/ ✅
|
||
- kommersant.ru/rss/news.xml ✅
|
||
|
||
**Тестирование Phase 3:**
|
||
- ✅ web_search (Wikipedia) - работает
|
||
- ✅ get_weather (Open-Meteo) - работает
|
||
- ✅ Memory tools - работает, изолирована по профилям (протестировано)
|
||
|
||
**Location Settings (в рамках Phase 3):**
|
||
| Задача | Статус |
|
||
|--------|--------|
|
||
| Preferences keys | ✅ KEY_DEFAULT_TIMEZONE, KEY_DEFAULT_CITY |
|
||
| dialog_location.xml | ✅ UI для ввода timezone/city |
|
||
| showLocationDialog() | ✅ Реализована в MainActivity |
|
||
| drawer_menu.xml | ✅ Добавлен item action_location |
|
||
| ic_location.xml | ✅ Создан vector drawable |
|
||
| ToolExecutor.updateSettings() | ✅ Принимает timezone/city при сохранении |
|
||
|
||
**Defaults:**
|
||
- Timezone: Asia/Irkutsk
|
||
- City: Иркутск
|
||
|
||
---
|
||
|
||
## Active Plan (Phases 1-5)
|
||
|
||
### Phase 3 (Active): CalDAV Calendar + Local Reminders
|
||
**Статус:** 🔄 В разработке | **Оценка:** 4-5 дней
|
||
|
||
**Два режима:**
|
||
1. CalDAV — синхронизация с Baikal сервером
|
||
2. Local — автономная напоминалка в памяти AI (работает БЕЗ интернета)
|
||
|
||
**Подробнее:** см. таблицу в разделе Phase 3 выше
|
||
|
||
---
|
||
|
||
### Phase 4: Heartbeat (Scheduled)
|
||
**Оценка:** 2-3 дня
|
||
|
||
Автономная периодическая самопроверка:
|
||
- WorkManager задача (каждые 30 минут)
|
||
- Active hours (8:00-22:00)
|
||
- Обработка ответа (молча vs уведомление)
|
||
|
||
### Phase 5: Email (IMAP/SMTP)
|
||
**Оценка:** 4-5 дней
|
||
|
||
Интеграция с email без OAuth:
|
||
- IMAP клиент (чтение писем)
|
||
- SMTP клиент (отправка)
|
||
- UI настройки ящика (сервер, порт, логин, пароль)
|
||
- Email tools для AI
|
||
|
||
---
|
||
|
||
## Technical Context
|
||
|
||
### ⚠️ ВАЖНО: Сборка APK после каждого изменения
|
||
**После каждого исправления или добавления функций НЕОБХОДИМО собирать APK!**
|
||
|
||
Пользователь должен иметь возможность сразу протестировать изменения.
|
||
|
||
```bash
|
||
# Сборка APK
|
||
JAVA_HOME=/opt/homebrew/opt/openjdk@17 ./gradlew assembleDebug
|
||
|
||
# Путь к APK
|
||
app/build/outputs/apk/debug/app-debug.apk
|
||
```
|
||
|
||
### Key Files
|
||
- `app/src/main/java/com/mistral/chat/ui/MainActivity.kt` — главная активность
|
||
- `app/src/main/java/com/mistral/chat/api/MistralClient.kt` — API клиент
|
||
- `app/src/main/java/com/mistral/chat/api/ToolExecutor.kt` — менеджер tools
|
||
- `app/src/main/java/com/mistral/chat/data/ChatDatabase.kt` — база данных
|
||
- `app/src/main/java/com/mistral/chat/data/Profile.kt` — профиль
|
||
- `app/src/main/java/com/mistral/chat/data/Memory.kt` — память
|
||
- `app/src/main/res/layout/dialog_location.xml` — настройки местоположения
|
||
|
||
### Current Issues
|
||
- Кнопка STOP не работает (требует streaming mode)
|
||
|
||
### Model Selection
|
||
- **Default:** mistral-medium-latest (быстрее, меньше ошибок)
|
||
- **Доступные модели:** Large, Medium, Codestral, Pixtral
|
||
- **OkHttp timeouts:** connect 60s, read 120s, write 60s
|
||
|
||
### Error Handling (Исправлено)
|
||
- Ошибки tool execution (таймауты, network errors) НЕ сохраняются в БД
|
||
- Показываются пользователю через Toast
|
||
- Предотвращает "отравление" контекста сообщениями об ошибках
|
||
|
||
---
|
||
|
||
## ⚠️ ВАЖНЫЕ ПРАВИЛА РАЗРАБОТКИ
|
||
|
||
### Запрет на удаление реализованных функций
|
||
**НИКОГДА не удаляй уже реализованные функции!** Даже если они кажутся неидеальными:
|
||
- Если нужно изменить поведение - исправь, а не удаляй
|
||
- Если что-то сломалось - почини, а не упрощай удалением
|
||
- При удалении функций (даже "неиспользуемых") всегда согласовывай с пользователем
|
||
|
||
### Запрет на хардкодинг переменных
|
||
**НИКОГДА не хардкодь значения, которые должны быть динамическими!**
|
||
- Даты, года, время,地名, названия - всё должно подставляться из системы/контекста
|
||
- Если что-то не получается реализовать без хардкода - ОБСУДИ с пользователем перед реализацией
|
||
- Пример правильного подхода: `{CURRENT_YEAR}` → подставляется через `SimpleDateFormat`
|
||
|
||
### Сборка APK после каждого изменения
|
||
**После каждого исправления или добавления функций ОБЯЗАТЕЛЬНО собирай APK!**
|
||
- Пользователь должен иметь возможность сразу протестировать изменения
|
||
- Команда: `JAVA_HOME=/opt/homebrew/opt/openjdk@17 ./gradlew assembleDebug`
|
||
- Расположение: `app/build/outputs/apk/debug/app-debug.apk`
|
||
|
||
### Дублирование сообщений при переключении сессий (BUG FIX)
|
||
**Проблема:** При переходе из второй сессии в первую (или любую другую) сообщения дублировались.
|
||
|
||
**Причина:** Асинхронная загрузка сообщений без проверки актуальности sessionId.
|
||
|
||
**Решение в MainActivity.kt:**
|
||
1. Очищаем список СРАЗУ при переключении (до асинхронной загрузки)
|
||
2. Используем `loadMessagesJob` для отмены предыдущей загрузки сообщений
|
||
3. Проверяем sessionId внутри async загрузки (несколько раз)
|
||
4. Передаём `expectedSessionId` в `addMessage` для правильного сохранения в БД
|
||
5. Прокрутка к последнему сообщению после загрузки
|
||
|
||
### ⚠️ ВАЖНО: Логика прокрутки чата
|
||
**Правильная реализация:**
|
||
1. **К концу сообщения пользователя** - прокрутка к концу (scrollToPosition) через 100мс после добавления
|
||
2. **К началу ответа ИИ** - прокрутка к НАЧАЛУ (scrollToPositionWithOffset) через 150мс после добавления сообщения ИИ
|
||
|
||
**Техническая реализация:**
|
||
- В `addMessage()`: для сообщений ИИ (`!message.isUser`) - прокрутка к началу через 150мс
|
||
- Используй `layoutManager.scrollToPositionWithOffset(position, 0)` для прокрутки к началу элемента
|
||
- Используй `scrollToPosition(position)` для прокрутки к концу элемента
|
||
- Проверяй `!userScrolledAfterSend` перед прокруткой к ответу ИИ
|
||
|
||
### Удаление debug логирования
|
||
После отладки и подтверждения что баг исправлен - удали все `android.util.Log.d("DEBUG", ...)` из кода.
|
||
|
||
### Порядок действий при работе с багом
|
||
1. Проанализируй код и найди причину
|
||
2. Исправь проблему, а не симптомы
|
||
3. Не удаляй существующий функционал
|
||
4. Проверь что исправление не ломает другие сценарии
|
||
5. Документируй исправление в agents.md
|
||
|
||
---
|
||
|
||
## Выводы и предмет для обсуждения
|
||
|
||
### WebSearchTool
|
||
- **Wikipedia API** - работает, но содержит только энциклопедические статьи (нет погоды, новостей)
|
||
- **DuckDuckGo Instant Answer API** - возвращает 0 результатов для большинства запросов (ограничение бесплатного API)
|
||
- **Вывод:** Текущая реализация web_search не может полноценно заменить поисковик
|
||
|
||
### OpenUrlTool (предложено, отложено)
|
||
- AI не знает все URL наизусть - нужен либо справочник в system prompt, либо web_search для нахождения URL
|
||
- При гибридном подходе: web_search находит URL → open_url парсит страницу
|
||
- Проблема: в system prompt не влезет список URL для всех типичных запросов (погода, новости, курсы валют и т.д.)
|
||
- **Вывод:** Реализация отложена до починки web_search
|
||
|
||
### Tool Execution Loop
|
||
- Предыдущая реализация: 1 итерация → результаты → финальный запрос без tools
|
||
- **Проблема:** Не даёт AI сделать несколько последовательных поисков (web_search → получить URL → open_url)
|
||
- Новая реализация: до 15 итераций, как в Kai - AI сам решает сколько поисков нужно
|
||
- Лимит iteration: 15
|
||
- Timeout на итерацию: 30 сек
|
||
- Если API Mistral не выдержит - снизим до 10 или 5
|
||
|
||
---
|
||
|
||
## 📋 Контекст сессии и оптимизация (В ОБСУЖДЕНИИ)
|
||
|
||
### Текущая реализация (без оптимизации)
|
||
|
||
При каждом запросе отправляется полный контекст:
|
||
1. System prompt (профиль)
|
||
2. Текущая дата и время
|
||
3. Часовой пояс + город
|
||
4. Контекст профиля (имя, о себе)
|
||
5. Контекст памяти (факты, выводы, предпочтения)
|
||
6. **ВСЕ сообщения сессии**
|
||
7. Результаты tool calls (полностью, до 2000 символов каждый)
|
||
|
||
**Проблемы:**
|
||
- При 2-3 tool calls (RSS + статья) добавляется 4000-6000 символов в контекст
|
||
- При росте сессии (100+ сообщений) запрос станет слишком большим
|
||
- 503 ошибки чаще происходят при больших запросах
|
||
- Превышение лимита токенов контекста
|
||
|
||
### Варианты решения
|
||
|
||
**1. Trimming (простое)**
|
||
- Оставлять только последние N сообщений + память + system prompt
|
||
- Просто реализовать, но теряется история
|
||
|
||
**2. Свёртывание tool results**
|
||
- Не добавлять полный результат open_url в историю
|
||
- Добавлять краткую выжимку: "Найдено 5 новостей о [тема]"
|
||
- Сложнее реализовать, сохраняет суть
|
||
|
||
**3. Контекстное окно (гибкое)**
|
||
- Оставлять последние N сообщений + summary предыдущих
|
||
- ИИ сам решает что важно
|
||
- Сложная реализация
|
||
|
||
**Статус:** Не решено, требует обсуждения с пользователем
|
||
|
||
---
|
||
|
||
## Conversation Context (for AI Agent)
|
||
|
||
**При начале новой сессии:**
|
||
Прочитай файл AGENTS.md для понимания текущего контекста разработки.
|
||
|
||
**При запросе "продолжаем":**
|
||
Мы работаем над Phase 3 (Tools). Последняя завершённая задача — добавление настроек location (timezone/city) в drawer menu.
|
||
|
||
**Важно:**
|
||
- Пушить в GitHub только после тестирования и подтверждения пользователя
|
||
- Не делать push автоматически после каждого изменения
|
||
|
||
---
|
||
|
||
*Last updated: 2026-04-10*
|
||
*Version: 1.10* |