From 53c572ef46cf1605eb9021ca1d6625569ffb8580 Mon Sep 17 00:00:00 2001 From: ars Date: Thu, 19 Feb 2026 18:12:09 +0000 Subject: [PATCH] Add project and deployment instruction (docs/DEPLOYMENT.md) Co-authored-by: Cursor --- .env.example | 36 +++ .gitignore | 65 ++++ APPLY_AUTHENTIK_FIX.sh | 54 ++++ APPLY_OAUTH_FIX.sh | 65 ++++ AUTHENTIK_FIX.md | 94 ++++++ AUTHENTIK_SETUP.md | 139 ++++++++ DIAGNOSE_VISION_ISSUE.md | 111 +++++++ FIX_DOCUMENTATION_LINKS.md | 70 ++++ FIX_IMAGE_TRANSFER.md | 124 ++++++++ FIX_LOGIN_TITLE.md | 82 +++++ FIX_OAUTH_REDIRECT.md | 42 +++ FIX_OAUTH_SLUG.md | 41 +++ FIX_OLLAMA_URL.md | 86 +++++ FIX_TRACE_ERROR.md | 61 ++++ LOGO_FIX.md | 61 ++++ LOGO_SETUP.md | 109 +++++++ QUICK_FIX.md | 70 ++++ QUICK_START.md | 58 ++++ README.md | 294 +++++++++++++++++ REBRANDING.md | 94 ++++++ REBRAND_FIX.md | 57 ++++ REBRAND_OAUTH_FIX.md | 89 ++++++ REBRAND_SOLUTION.md | 258 +++++++++++++++ RESTORE_AFTER_500.md | 56 ++++ RESTORE_CONTAINER.md | 55 ++++ SEARXNG_SETUP.md | 306 ++++++++++++++++++ TEST_VISION.md | 93 ++++++ TROUBLESHOOTING.md | 102 ++++++ VISION_MODELS.md | 100 ++++++ check_authentik.sh | 47 +++ docker-compose.yml | 150 +++++++++ docs/DEPLOYMENT.md | 288 +++++++++++++++++ docs/TTS_CMU_ARCTIC_SPEAKERS.md | 51 +++ instr | 24 ++ media/favicon.png | Bin 0 -> 49855 bytes media/favicon.svg | 6 + media/init-logos.sh | 35 ++ media/logo-dark.svg | 6 + media/logo-light.svg | 6 + media/logo.png | Bin 0 -> 49855 bytes media/Лого светлый справа слоган.md | 41 +++ media/Логотип светлый svg.md | 32 ++ media/Логотип светлый справа svg.md | 32 ++ media/Логотип тёмный svg.md | 32 ++ media/Логотип тёмный справа avg.md | 32 ++ media/лого темный справа слоган.md | 42 +++ media/логотип анимация .md | 75 +++++ media/логотип анимация2.md | 78 +++++ nginx-proxy-manager-config.md | 143 +++++++++ scripts/apply_logos_persistent.sh | 79 +++++ scripts/check_gpu.sh | 100 ++++++ scripts/check_image_transfer.sh | 85 +++++ scripts/check_searxng_json.sh | 43 +++ scripts/check_vision_models.sh | 42 +++ scripts/diagnose_search.sh | 90 ++++++ scripts/find_model_image_api.sh | 32 ++ scripts/find_openwebui_text.sh | 64 ++++ scripts/find_settings_elements.sh | 42 +++ scripts/fix_openwebui.sh | 49 +++ scripts/fix_search_complete.sh | 56 ++++ scripts/fix_searxng_config.sh | 55 ++++ scripts/fix_searxng_json.sh | 31 ++ scripts/fix_trace_error.sh | 57 ++++ scripts/fix_user_agent.sh | 132 ++++++++ scripts/fix_user_agent_aggressive.sh | 59 ++++ scripts/fix_user_agent_final.sh | 42 +++ scripts/init-logos.sh | 35 ++ scripts/init.sh | 45 +++ scripts/install_vision_model.sh | 58 ++++ scripts/rebrand.sh | 248 +++++++++++++++ scripts/rebrand_careful.sh | 126 ++++++++ scripts/rebrand_complete.sh | 274 ++++++++++++++++ scripts/rebrand_fast.sh | 134 ++++++++ scripts/rebrand_final.sh | 212 ++++++++++++ scripts/rebrand_full.sh | 209 ++++++++++++ scripts/rebrand_precise.sh | 173 ++++++++++ scripts/rebrand_safe.sh | 152 +++++++++ scripts/rebrand_safe_final.sh | 460 +++++++++++++++++++++++++++ scripts/rebrand_ultra_safe.sh | 185 +++++++++++ scripts/remove_footer_links.sh | 133 ++++++++ scripts/test_direct_vision.sh | 68 ++++ scripts/test_vision.sh | 142 +++++++++ scripts/update.sh | 50 +++ searxng/settings.yml | 22 ++ ssl-setup-guide.md | 121 +++++++ test.jpg | 1 + test_images/test_image.jpg | Bin 0 -> 23550 bytes worker/.env.example | 34 ++ worker/config.py | 132 ++++++++ worker/document_processor.py | 243 ++++++++++++++ worker/nextcloud_client.py | 249 +++++++++++++++ worker/nextcloud_sync.py | 448 ++++++++++++++++++++++++++ worker/openwebui_client.py | 179 +++++++++++ worker/requirements.txt | 17 + 94 files changed, 9200 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100755 APPLY_AUTHENTIK_FIX.sh create mode 100755 APPLY_OAUTH_FIX.sh create mode 100644 AUTHENTIK_FIX.md create mode 100644 AUTHENTIK_SETUP.md create mode 100644 DIAGNOSE_VISION_ISSUE.md create mode 100644 FIX_DOCUMENTATION_LINKS.md create mode 100644 FIX_IMAGE_TRANSFER.md create mode 100644 FIX_LOGIN_TITLE.md create mode 100644 FIX_OAUTH_REDIRECT.md create mode 100644 FIX_OAUTH_SLUG.md create mode 100644 FIX_OLLAMA_URL.md create mode 100644 FIX_TRACE_ERROR.md create mode 100644 LOGO_FIX.md create mode 100644 LOGO_SETUP.md create mode 100644 QUICK_FIX.md create mode 100644 QUICK_START.md create mode 100644 README.md create mode 100644 REBRANDING.md create mode 100644 REBRAND_FIX.md create mode 100644 REBRAND_OAUTH_FIX.md create mode 100644 REBRAND_SOLUTION.md create mode 100644 RESTORE_AFTER_500.md create mode 100644 RESTORE_CONTAINER.md create mode 100644 SEARXNG_SETUP.md create mode 100644 TEST_VISION.md create mode 100644 TROUBLESHOOTING.md create mode 100644 VISION_MODELS.md create mode 100755 check_authentik.sh create mode 100644 docker-compose.yml create mode 100644 docs/DEPLOYMENT.md create mode 100644 docs/TTS_CMU_ARCTIC_SPEAKERS.md create mode 100644 instr create mode 100644 media/favicon.png create mode 100644 media/favicon.svg create mode 100755 media/init-logos.sh create mode 100644 media/logo-dark.svg create mode 100644 media/logo-light.svg create mode 100644 media/logo.png create mode 100644 media/Лого светлый справа слоган.md create mode 100644 media/Логотип светлый svg.md create mode 100644 media/Логотип светлый справа svg.md create mode 100644 media/Логотип тёмный svg.md create mode 100644 media/Логотип тёмный справа avg.md create mode 100644 media/лого темный справа слоган.md create mode 100644 media/логотип анимация .md create mode 100644 media/логотип анимация2.md create mode 100644 nginx-proxy-manager-config.md create mode 100755 scripts/apply_logos_persistent.sh create mode 100755 scripts/check_gpu.sh create mode 100755 scripts/check_image_transfer.sh create mode 100755 scripts/check_searxng_json.sh create mode 100755 scripts/check_vision_models.sh create mode 100755 scripts/diagnose_search.sh create mode 100755 scripts/find_model_image_api.sh create mode 100755 scripts/find_openwebui_text.sh create mode 100755 scripts/find_settings_elements.sh create mode 100755 scripts/fix_openwebui.sh create mode 100755 scripts/fix_search_complete.sh create mode 100755 scripts/fix_searxng_config.sh create mode 100755 scripts/fix_searxng_json.sh create mode 100755 scripts/fix_trace_error.sh create mode 100755 scripts/fix_user_agent.sh create mode 100755 scripts/fix_user_agent_aggressive.sh create mode 100755 scripts/fix_user_agent_final.sh create mode 100644 scripts/init-logos.sh create mode 100755 scripts/init.sh create mode 100755 scripts/install_vision_model.sh create mode 100755 scripts/rebrand.sh create mode 100755 scripts/rebrand_careful.sh create mode 100755 scripts/rebrand_complete.sh create mode 100755 scripts/rebrand_fast.sh create mode 100755 scripts/rebrand_final.sh create mode 100755 scripts/rebrand_full.sh create mode 100755 scripts/rebrand_precise.sh create mode 100755 scripts/rebrand_safe.sh create mode 100755 scripts/rebrand_safe_final.sh create mode 100755 scripts/rebrand_ultra_safe.sh create mode 100755 scripts/remove_footer_links.sh create mode 100755 scripts/test_direct_vision.sh create mode 100755 scripts/test_vision.sh create mode 100755 scripts/update.sh create mode 100644 searxng/settings.yml create mode 100644 ssl-setup-guide.md create mode 100644 test.jpg create mode 100644 test_images/test_image.jpg create mode 100644 worker/.env.example create mode 100644 worker/config.py create mode 100644 worker/document_processor.py create mode 100644 worker/nextcloud_client.py create mode 100755 worker/nextcloud_sync.py create mode 100644 worker/openwebui_client.py create mode 100644 worker/requirements.txt diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..07ee8c4 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# === ДОМЕНЫ И СЕТЬ === +DOMAIN_OPENWEBUI=https://odo.iieasy.ru +DOMAIN_NEXTCLOUD=https://next.iieasy.ru +DOMAIN_AUTHENTIK=https://auth.iieasy.ru +DOMAIN_VAULTWARDEN=http://192.168.88.165:8082 + +# === AUTHENTIK (OIDC SSO) === +# Где брать: Authentik -> Providers -> твой OIDC Provider +OAUTH_CLIENT_ID=your_oauth_client_id +OAUTH_CLIENT_SECRET=your_oauth_client_secret +OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/ + +# === NEXTCLOUD (Для Python-воркера) === +# Где брать: Nextcloud -> Настройки -> Безопасность -> Устройства и сессии (Создать пароль приложения) +NC_USER=your_nextcloud_username +NC_APP_PASSWORD=your_app_password + +# === OPEN WEBUI API (Для пуша файлов в Qdrant) === +# Где брать: Open WebUI -> Settings -> Account -> API Keys (создашь после первого запуска) +OPENWEBUI_API_KEY=your_api_key_here + +# === VAULTWARDEN (Для интеграции Bitwarden CLI) === +# Где брать: Vaultwarden -> Настройки аккаунта -> Безопасность -> Ключи -> API-ключ +BW_CLIENTID=your_vaultwarden_client_id +BW_CLIENTSECRET=your_vaultwarden_client_secret + +# === QDRANT === +# Сгенерировать случайный ключ: openssl rand -hex 32 +QDRANT_API_KEY=your_qdrant_api_key_here + +# === SEARXNG === +SEARXNG_HOSTNAME=searxng:8080 + +# === OLLAMA === +OLLAMA_MODEL=gemma3n:e4b-it-fp16 +NVIDIA_VISIBLE_DEVICES=all diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b97e50a --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Переменные окружения +.env +.env.local +.env.*.local + +# Docker volumes и данные +volumes/ +*.db +*.sqlite +*.sqlite3 + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Виртуальные окружения +venv/ +env/ +ENV/ +.venv + +# Логи +*.log +logs/ +sync.log + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Временные файлы +*.tmp +*.bak +*.cache + +# Медиа файлы (если не нужны в репозитории) +# Раскомментируйте если не хотите хранить медиа в git +# media/*.png +# media/*.jpg +# media/*.ico diff --git a/APPLY_AUTHENTIK_FIX.sh b/APPLY_AUTHENTIK_FIX.sh new file mode 100755 index 0000000..da0a7a4 --- /dev/null +++ b/APPLY_AUTHENTIK_FIX.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Скрипт для применения исправления Authentik + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$SCRIPT_DIR" + +echo "=== Применение исправления Authentik ===" +echo "Рабочая директория: $PROJECT_DIR" + +cd "$PROJECT_DIR" + +# Проверка наличия .env файла +if [ ! -f ".env" ]; then + echo "✗ Ошибка: .env файл не найден в $PROJECT_DIR" + echo "Создайте .env из .env.example или проверьте путь" + exit 1 +fi + +# Проверка правильности .env +if grep -q "ii-easy-web" .env; then + echo "✓ .env содержит правильный slug: ii-easy-web" +else + echo "⚠ Предупреждение: .env не содержит slug 'ii-easy-web'" + echo "Проверьте значение OPENID_CONNECT_ISSUER в .env" +fi + +# Перезапуск контейнера для применения изменений +echo "Перезапуск контейнера open-webui..." +docker compose restart open-webui + +echo "Ожидание запуска (15 секунд)..." +sleep 15 + +# Проверка доступности +if curl -f http://localhost:3001/health >/dev/null 2>&1; then + echo "✓ Open WebUI запущен и отвечает" +else + echo "⚠ Open WebUI может еще запускаться, проверьте логи:" + echo " docker compose logs open-webui --tail 30" +fi + +echo "" +echo "=== Готово! ===" +echo "" +echo "Проверьте вход через Authentik:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. Попробуйте войти через 'iiEasy ID' (Authentik)" +echo "" +echo "Если все еще Internal Server Error:" +echo " 1. Проверьте redirect URI в Authentik: https://odo.iieasy.ru/oauth/oidc/callback" +echo " 2. Проверьте логи: docker compose logs open-webui | grep -i oauth" +echo " 3. Временно используйте форму входа (уже включена)" diff --git a/APPLY_OAUTH_FIX.sh b/APPLY_OAUTH_FIX.sh new file mode 100755 index 0000000..44162cf --- /dev/null +++ b/APPLY_OAUTH_FIX.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# Скрипт для применения исправления OAuth endpoint + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$SCRIPT_DIR" + +cd "$PROJECT_DIR" + +echo "=== Применение исправления OAuth endpoint ===" +echo "" + +# Проверка .env файла +if [ ! -f ".env" ]; then + echo "✗ Ошибка: .env файл не найден" + exit 1 +fi + +# Проверка правильного slug +if grep -q "OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/" .env; then + echo "✓ .env содержит правильный slug: open-webui" +else + echo "✗ Ошибка: .env не содержит правильный slug" + echo " Должно быть: OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/" + exit 1 +fi + +echo "" +echo "1. Перезапуск контейнера open-webui..." +sudo docker compose restart open-webui + +echo "" +echo "2. Ожидание запуска контейнера (20 секунд)..." +sleep 20 + +echo "" +echo "3. Проверка статуса контейнера..." +if sudo docker compose ps open-webui | grep -q "Up"; then + echo "✓ Контейнер запущен" +else + echo "✗ Контейнер не запустился. Проверьте логи:" + echo " sudo docker compose logs open-webui" + exit 1 +fi + +echo "" +echo "4. Проверка логов на ошибки OAuth..." +OAUTH_ERRORS=$(sudo docker compose logs open-webui --tail 50 2>&1 | grep -i "oauth\|oidc\|404\|error" | tail -5 || true) +if [ -z "$OAUTH_ERRORS" ]; then + echo "✓ Ошибок OAuth не найдено" +else + echo "⚠ Найдены ошибки в логах:" + echo "$OAUTH_ERRORS" +fi + +echo "" +echo "=== Готово! ===" +echo "" +echo "Проверьте OAuth:" +echo "1. Откройте https://odo.iieasy.ru" +echo "2. Нажмите кнопку 'iiEasy ID' для входа через OAuth" +echo "" +echo "Если есть проблемы, проверьте логи:" +echo " sudo docker compose logs open-webui --tail 100 | grep -i oauth" diff --git a/AUTHENTIK_FIX.md b/AUTHENTIK_FIX.md new file mode 100644 index 0000000..1f1c88e --- /dev/null +++ b/AUTHENTIK_FIX.md @@ -0,0 +1,94 @@ +# Быстрое решение проблемы с Authentik + +## Проблема: Internal Server Error при входе через Authentik + +Endpoint `https://auth.iieasy.ru/application/o/open-webui/.well-known/openid-configuration` возвращает 404 Not Found. + +## Решение 1: Временное включение формы входа (работает сейчас) + +Форма входа уже включена в `docker-compose.yml`. Перезапустите контейнер: + +```bash +cd /home/its/iiEasyWeb +sudo docker compose restart open-webui +``` + +Теперь вы сможете войти через форму входа на странице `https://odo.iieasy.ru`. + +## Решение 2: Правильная настройка Authentik + +### Проверка в Authentik + +1. Войдите в Authentik: `https://auth.iieasy.ru` +2. Перейдите в **Applications** +3. Найдите Application для Open WebUI +4. Проверьте **Slug** - он должен быть `open-webui` + +### Если Application не существует + +1. **Providers** → **Add Provider** + - Тип: **OpenID Connect / OAuth2 / OAuth2 with OpenID Connect** + - Название: `Open WebUI` + - Сохраните и запомните **Client ID** и **Client Secret** + +2. **Applications** → **Add Application** + - **Name**: `Open WebUI` + - **Slug**: `open-webui` (ВАЖНО! Должен совпадать с URL) + - **Provider**: Выберите созданный Provider + - **Redirect URIs**: `https://odo.iieasy.ru/oauth/oidc/callback` + - Сохраните + +3. **Обновите .env:** + ```bash + OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/ + ``` + Где `open-webui` - это slug из Application. + +4. **Проверьте endpoint:** + ```bash + curl https://auth.iieasy.ru/application/o/open-webui/.well-known/openid-configuration + ``` + Должен вернуться JSON, а не HTML. + +5. **Перезапустите контейнер:** + ```bash + sudo docker compose restart open-webui + ``` + +### Если Application существует, но slug другой + +Если slug в Authentik не `open-webui`, а например `openwebui` или `webui`: + +1. Обновите `.env`: + ```bash + OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/ВАШ_SLUG/ + ``` + +2. Или измените slug в Authentik Application на `open-webui` + +## Решение 3: Отключение Authentik (если не нужен) + +Если Authentik не нужен, отключите его: + +В `docker-compose.yml` измените: +```yaml +- ENABLE_OAUTH_SIGNUP=false +- ENABLE_LOGIN_FORM=true +``` + +И удалите или закомментируйте переменные OAuth. + +## Проверка после настройки + +1. Проверьте endpoint: + ```bash + curl https://auth.iieasy.ru/application/o/open-webui/.well-known/openid-configuration + ``` + Должен вернуться JSON с `issuer`, `authorization_endpoint` и т.д. + +2. Проверьте логи: + ```bash + sudo docker compose logs open-webui | grep -i "oidc\|oauth" + ``` + +3. Попробуйте войти через `https://odo.iieasy.ru` diff --git a/AUTHENTIK_SETUP.md b/AUTHENTIK_SETUP.md new file mode 100644 index 0000000..89f8ecc --- /dev/null +++ b/AUTHENTIK_SETUP.md @@ -0,0 +1,139 @@ +# Настройка Authentik для Open WebUI + +## Проблема: Internal Server Error при входе через Authentik + +### Диагностика + +1. **Проверьте доступность Authentik:** + ```bash + curl -I https://auth.iieasy.ru + ``` + +2. **Проверьте OpenID endpoint:** + ```bash + # Правильный путь для Authentik: + curl https://auth.iieasy.ru/application/o/open-webui/.well-known/openid-configuration + ``` + +3. **Проверьте логи Open WebUI:** + ```bash + sudo docker compose logs open-webui | grep -i "auth\|oidc\|oauth\|error" + ``` + +## Правильная настройка Authentik + +### Шаг 1: Создание OIDC Provider в Authentik + +1. Войдите в Authentik: `https://auth.iieasy.ru` +2. Перейдите в **Providers** → **Add Provider** +3. Выберите **OpenID Connect / OAuth2 / OAuth2 with OpenID Connect** +4. Заполните: + - **Name**: `Open WebUI` (или любое имя) + - **Authorization flow**: Выберите существующий flow + - **Redirect URIs**: `https://odo.iieasy.ru/oauth/oidc/callback` + - **Client type**: `Confidential` + - **Client ID**: Запомните этот ID + - **Client Secret**: Запомните этот секрет + +5. Сохраните Provider + +### Шаг 2: Создание Application в Authentik + +1. Перейдите в **Applications** → **Add Application** +2. Заполните: + - **Name**: `Open WebUI` + - **Slug**: `open-webui` (это важно для URL!) + - **Provider**: Выберите созданный Provider +3. Сохраните Application + +### Шаг 3: Проверка правильного URL + +После создания Application, правильный URL будет: +``` +https://auth.iieasy.ru/application/o/open-webui/.well-known/openid-configuration +``` + +Где `open-webui` - это slug из Application. + +### Шаг 4: Обновление .env + +В файле `.env` убедитесь, что: + +```bash +OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/ +``` + +**ВАЖНО:** +- URL должен заканчиваться на `/` +- Slug (`open-webui`) должен совпадать с slug в Authentik Application +- После `/application/o/` идет slug, затем `/` + +### Шаг 5: Проверка переменных в docker-compose.yml + +В `docker-compose.yml` используется: +```yaml +OPENID_PROVIDER_URL=${OPENID_CONNECT_ISSUER}.well-known/openid-configuration +``` + +Это создаст полный URL: +``` +https://auth.iieasy.ru/application/o/open-webui/.well-known/openid-configuration +``` + +### Шаг 6: Перезапуск контейнера + +```bash +cd /home/its/iiEasyWeb +sudo docker compose restart open-webui +``` + +## Временное решение: Включить форму входа + +Если Authentik не работает, временно включите форму входа: + +В `docker-compose.yml` измените: +```yaml +- ENABLE_LOGIN_FORM=true # Временно включено +- ENABLE_OAUTH_SIGNUP=false # Временно отключено +``` + +Затем перезапустите: +```bash +sudo docker compose restart open-webui +``` + +## Проверка конфигурации + +1. **Проверьте, что endpoint доступен:** + ```bash + curl https://auth.iieasy.ru/application/o/open-webui/.well-known/openid-configuration + ``` + Должен вернуться JSON с конфигурацией OpenID Connect. + +2. **Проверьте redirect URI в Authentik:** + - Должен быть: `https://odo.iieasy.ru/oauth/oidc/callback` + - Без завершающего слеша + +3. **Проверьте логи:** + ```bash + sudo docker compose logs open-webui --tail 100 | grep -i "oidc\|oauth" + ``` + +## Частые ошибки + +### 404 Not Found на .well-known/openid-configuration + +- Проверьте slug в Application (должен быть `open-webui`) +- Проверьте URL в .env (должен заканчиваться на `/`) +- Убедитесь, что Application привязан к Provider + +### Invalid redirect URI + +- Проверьте redirect URI в Authentik: `https://odo.iieasy.ru/oauth/oidc/callback` +- Убедитесь, что домен правильный (без порта, если используете HTTPS) + +### Internal Server Error + +- Проверьте логи Open WebUI +- Убедитесь, что Client ID и Client Secret правильные +- Проверьте, что SSL сертификат валидный для auth.iieasy.ru diff --git a/DIAGNOSE_VISION_ISSUE.md b/DIAGNOSE_VISION_ISSUE.md new file mode 100644 index 0000000..779c4f6 --- /dev/null +++ b/DIAGNOSE_VISION_ISSUE.md @@ -0,0 +1,111 @@ +# Диагностика проблемы с передачей изображений в Ollama + +## Проблема +Open WebUI не передает изображения в Ollama для модели gemma3n:e4b-it-fp16, хотя: +- ✅ Адрес Ollama правильный: `http://ollama:11434` +- ✅ Модель поддерживает vision +- ✅ Изображения загружаются в Open WebUI +- ❌ Но не доходят до Ollama (нет запросов с изображениями в логах) + +## Возможные причины + +### 1. Open WebUI v0.8.3 не распознает gemma3n как vision-модель + +Open WebUI может не знать, что gemma3n:e4b-it-fp16 поддерживает vision. Нужно проверить список vision-моделей в коде. + +### 2. Модель не помечена как vision в настройках + +Возможно, нужно явно указать в настройках Open WebUI, что модель поддерживает vision. + +### 3. Проблема с форматом передачи изображений + +Open WebUI может передавать изображения в неправильном формате для Ollama API. + +## Решения + +### Решение 1: Проверка настроек модели в Open WebUI + +1. Откройте https://odo.iieasy.ru +2. Перейдите в **Settings → Models** +3. Найдите модель `gemma3n:e4b-it-fp16` +4. Проверьте, есть ли опция "Vision" или "Multimodal" +5. Включите её, если есть + +### Решение 2: Обновление Open WebUI + +Версия v0.8.3 может иметь проблемы с vision. Попробуйте обновить до последней версии: + +```yaml +# В docker-compose.yml измените: +image: ghcr.io/open-webui/open-webui:latest +# или +image: ghcr.io/open-webui/open-webui:v1.x.x +``` + +Затем: +```bash +docker compose pull open-webui +docker compose up -d open-webui +``` + +### Решение 3: Проверка через прямой API запрос + +Проверьте, работает ли vision напрямую через Ollama API: + +```bash +cd /home/its/iiEasyWeb/test_images +IMAGE_B64=$(base64 -w 0 test_image.jpg) + +sudo docker exec ollama curl -s -X POST http://localhost:11434/api/generate \ + -H 'Content-Type: application/json' \ + -d "{ + \"model\": \"gemma3n:e4b-it-fp16\", + \"prompt\": \"Опиши это изображение на русском языке\", + \"images\": [\"$IMAGE_B64\"], + \"stream\": false + }" | jq -r '.response' +``` + +Если это работает, значит проблема в Open WebUI, а не в Ollama. + +### Решение 4: Проверка логов при отправке изображения + +1. Откройте два терминала + +2. В первом терминале: +```bash +sudo docker logs open-webui -f | grep -i "image\|ollama\|generate" +``` + +3. Во втором терминале: +```bash +sudo docker logs ollama -f | grep -i "generate\|image" +``` + +4. Отправьте изображение через веб-интерфейс + +5. Проверьте, что появляется в логах: + - В Open WebUI должен быть запрос с изображением + - В Ollama должен быть запрос к `/api/generate` с полем `images` + +### Решение 5: Проверка версии Open WebUI + +```bash +sudo docker exec open-webui cat /app/backend/version.txt +``` + +Если версия старая, обновите до последней. + +## Альтернативное решение: Использование API напрямую + +Если Open WebUI не поддерживает vision для gemma3n, можно использовать API напрямую или создать свой клиент. + +## Проверка работы vision модели + +Убедитесь, что модель действительно поддерживает vision: + +```bash +sudo docker exec ollama ollama show gemma3n:e4b-it-fp16 +``` + +В выводе должна быть информация о поддержке vision/multimodal. diff --git a/FIX_DOCUMENTATION_LINKS.md b/FIX_DOCUMENTATION_LINKS.md new file mode 100644 index 0000000..8b9c68e --- /dev/null +++ b/FIX_DOCUMENTATION_LINKS.md @@ -0,0 +1,70 @@ +# Исправление ссылок на документацию и удаление "(Open WebUI)" + +## Проблемы + +1. Текст "(Open WebUI)" все еще виден в "Войти в iiEasyWeb (Open WebUI)" +2. Ссылки на документацию ведут на оригинальный сайт вместо note.iieasy.ru + +## Решение + +**1. Запустите финальный скрипт ребрендинга:** + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/rebrand_final.sh +``` + +Этот скрипт: +- ✅ Агрессивно ищет и удаляет "(Open WebUI)" из ВСЕХ файлов +- ✅ Заменяет ссылки на документацию на note.iieasy.ru +- ✅ Проверяет и исправляет базу данных +- ✅ Обрабатывает все варианты написания + +**2. Очистите кеш браузера:** + +После запуска скрипта обязательно очистите кеш: +- **Chrome/Edge**: Ctrl+Shift+Delete +- Выберите "Изображения и файлы в кеше" +- Очистите кеш + +**3. Проверьте через Admin Panel:** + +Если текст все еще виден: + +1. Откройте `https://odo.iieasy.ru` +2. Войдите как администратор +3. Перейдите в **Settings → Appearance** +4. Проверьте поле **Site Title** - должно быть "iiEasyWeb" без "(Open WebUI)" +5. Если там есть "(Open WebUI)", удалите его вручную и сохраните + +**4. Проверьте переменные окружения:** + +Убедитесь, что в `docker-compose.yml` правильно настроено: + +```yaml +- WEBUI_NAME=iiEasyWeb +``` + +## Что заменяется + +Скрипт заменяет следующие ссылки на документацию: +- `https://docs.openwebui.com` → `https://note.iieasy.ru` +- `https://open-webui.com/docs` → `https://note.iieasy.ru` +- `https://github.com/open-webui/docs` → `https://note.iieasy.ru` +- `docs.openwebui.com` → `note.iieasy.ru` + +## Если проблема осталась + +Если после всех действий текст "(Open WebUI)" все еще виден: + +1. Выполните поиск вручную: + ```bash + sudo docker exec open-webui find /app -type f -exec grep -l "(Open WebUI)" {} \; 2>/dev/null + ``` + +2. Проверьте базу данных напрямую: + ```bash + sudo docker exec open-webui sqlite3 /app/backend/data/webui.db "SELECT * FROM settings WHERE value LIKE '%Open WebUI%';" + ``` + +3. Если найдете в базе, удалите вручную через Admin Panel или SQLite diff --git a/FIX_IMAGE_TRANSFER.md b/FIX_IMAGE_TRANSFER.md new file mode 100644 index 0000000..94b2d13 --- /dev/null +++ b/FIX_IMAGE_TRANSFER.md @@ -0,0 +1,124 @@ +# Исправление проблемы с передачей изображений из Open WebUI в Ollama + +## Проблема +Open WebUI не передает изображения в Ollama правильно. В логах Ollama нет запросов с изображениями. + +## Диагностика + +### 1. Проверка переменной окружения +```bash +sudo docker exec open-webui env | grep -i OLLAMA +``` + +Должно быть: +``` +OLLAMA_BASE_URL=http://ollama:11434 +``` + +### 2. Проверка доступности Ollama +```bash +sudo docker exec open-webui curl -s http://ollama:11434/api/tags | head -5 +``` + +### 3. Проверка логов +```bash +# Логи Open WebUI при отправке изображения +sudo docker logs open-webui --tail 100 | grep -i "image\|ollama\|error" + +# Логи Ollama - должны быть запросы с изображениями +sudo docker logs ollama --tail 100 | grep -i "image\|vision" +``` + +## Решение + +### Шаг 1: Убедитесь, что переменная добавлена в docker-compose.yml + +В файле `/home/its/iiEasyWeb/docker-compose.yml` должна быть строка: +```yaml + # Ollama API для работы с изображениями + - OLLAMA_BASE_URL=http://ollama:11434 +``` + +### Шаг 2: Перезапустите контейнер Open WebUI +```bash +cd /home/its/iiEasyWeb +docker compose restart open-webui +``` + +Или полностью пересоздайте: +```bash +docker compose up -d --force-recreate open-webui +``` + +### Шаг 3: Проверьте настройки в веб-интерфейсе + +1. Откройте https://odo.iieasy.ru +2. Перейдите в **Settings → Connections → Ollama API** +3. Убедитесь, что адрес: **`http://ollama:11434`** + - НЕ используйте `host.docker.internal:11434` + - НЕ используйте `localhost:11434` + - Должно быть именно `http://ollama:11434` + +### Шаг 4: Проверьте формат изображения + +Open WebUI должен передавать изображения в формате base64 в поле `images` массива JSON запроса к Ollama API. + +Формат запроса должен быть: +```json +{ + "model": "gemma3n:e4b-it-fp16", + "prompt": "Опиши это изображение", + "images": ["base64_encoded_image_data"], + "stream": false +} +``` + +## Дополнительная диагностика + +### Запустите скрипт проверки: +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/check_image_transfer.sh +``` + +### Проверка в реальном времени: + +1. Откройте два терминала + +2. В первом терминале следите за логами Open WebUI: +```bash +sudo docker logs open-webui -f | grep -i "image\|ollama" +``` + +3. Во втором терминале следите за логами Ollama: +```bash +sudo docker logs ollama -f | grep -i "image\|vision\|generate" +``` + +4. Отправьте изображение через веб-интерфейс + +5. Проверьте, появляются ли запросы в логах Ollama + +## Возможные проблемы + +1. **Переменная не применена** - контейнер не перезапущен после изменения docker-compose.yml +2. **Неправильный адрес в настройках** - в веб-интерфейсе указан неправильный адрес Ollama +3. **Проблема с сетью Docker** - контейнеры не могут общаться друг с другом +4. **Формат изображения** - Open WebUI передает изображение в неправильном формате +5. **Версия Open WebUI** - старая версия может не поддерживать vision правильно + +## Проверка версии Open WebUI + +```bash +sudo docker exec open-webui cat /app/backend/version.txt +``` + +Текущая версия в docker-compose.yml: `v0.8.3` + +## Альтернативное решение + +Если проблема не решается, можно попробовать: + +1. Обновить Open WebUI до последней версии +2. Использовать прямой API запрос к Ollama для тестирования +3. Проверить документацию Open WebUI по работе с vision моделями diff --git a/FIX_LOGIN_TITLE.md b/FIX_LOGIN_TITLE.md new file mode 100644 index 0000000..c3a16c3 --- /dev/null +++ b/FIX_LOGIN_TITLE.md @@ -0,0 +1,82 @@ +# Исправление текста "Войти в iiEasyWeb (Open WebUI)" + +## Проблема + +В заголовке страницы входа все еще видно "(Open WebUI)": "Войти в iiEasyWeb (Open WebUI)" + +## Решение + +**1. Сначала найдите, где находится этот текст:** + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/find_openwebui_text.sh +``` + +Этот скрипт покажет все файлы, где есть "(Open WebUI)". + +**2. Запустите точный скрипт ребрендинга:** + +```bash +sudo ./scripts/rebrand_precise.sh +``` + +**3. Если текст все еще виден, проверьте:** + +### Вариант A: Текст в базе данных + +Текст может храниться в базе данных Open WebUI. Проверьте через Admin Panel: + +1. Откройте `https://odo.iieasy.ru` +2. Войдите как администратор +3. Перейдите в **Settings → Appearance** +4. Проверьте поле **Site Title** - должно быть "iiEasyWeb" без "(Open WebUI)" +5. Сохраните изменения + +### Вариант B: Текст в переменных окружения + +Проверьте `docker-compose.yml`: + +```bash +grep -i "webui_name\|site_title" docker-compose.yml +``` + +Должно быть: +```yaml +- WEBUI_NAME=iiEasyWeb +``` + +### Вариант C: Очистка кеша браузера + +После изменений очистите кеш браузера: +- Chrome/Edge: Ctrl+Shift+Delete (Cmd+Shift+Delete на Mac) +- Выберите "Изображения и файлы в кеше" +- Очистите кеш + +### Вариант D: Пересборка фронтенда + +Если текст в скомпилированных файлах, может потребоваться пересборка: + +```bash +# Пересоздайте контейнер +sudo docker compose stop open-webui +sudo docker compose rm -f open-webui +sudo docker compose up -d open-webui + +# Подождите 30 секунд +sleep 30 + +# Запустите ребрендинг снова +sudo ./scripts/rebrand_precise.sh +``` + +## Если ничего не помогает + +Выполните поиск вручную: + +```bash +# Найдите все файлы с этим текстом +sudo docker exec open-webui find /app -type f -exec grep -l "(Open WebUI)" {} \; 2>/dev/null + +# Затем замените вручную в найденных файлах +``` diff --git a/FIX_OAUTH_REDIRECT.md b/FIX_OAUTH_REDIRECT.md new file mode 100644 index 0000000..4c39a42 --- /dev/null +++ b/FIX_OAUTH_REDIRECT.md @@ -0,0 +1,42 @@ +# Исправление редиректа на /auth + +## Проблема: Редирект на /auth вместо главной страницы + +Когда Open WebUI настроен на OAuth и `ENABLE_LOGIN_FORM=false`, но OAuth не работает правильно, происходит редирект на `/auth`. + +## Решение + +**1. Убедитесь, что форма входа включена (уже сделано):** + +В `docker-compose.yml`: +```yaml +- ENABLE_LOGIN_FORM=true # Включено +- ENABLE_OAUTH_SIGNUP=true +``` + +**2. Перезапустите контейнер:** + +```bash +cd /home/its/iiEasyWeb +sudo docker compose restart open-webui +``` + +**3. Проверьте конфигурацию OAuth:** + +- Endpoint должен быть доступен: `https://auth.iieasy.ru/application/o/ii-easy-web/.well-known/openid-configuration` +- Redirect URI в Authentik: `https://odo.iieasy.ru/oauth/oidc/callback` +- Client ID и Client Secret должны совпадать + +**4. Если OAuth все еще не работает:** + +Временно можно оставить форму входа включенной (`ENABLE_LOGIN_FORM=true`), чтобы пользователи могли войти. OAuth будет работать параллельно как альтернативный способ входа. + +## Проверка + +После перезапуска: +1. Откройте `https://odo.iieasy.ru` +2. Должна появиться страница входа с кнопкой "iiEasy ID" (OAuth) и формой логина +3. Попробуйте войти через форму входа +4. Попробуйте войти через "iiEasy ID" (OAuth) + +Если OAuth работает, форма входа можно отключить позже (`ENABLE_LOGIN_FORM=false`). diff --git a/FIX_OAUTH_SLUG.md b/FIX_OAUTH_SLUG.md new file mode 100644 index 0000000..68c4887 --- /dev/null +++ b/FIX_OAUTH_SLUG.md @@ -0,0 +1,41 @@ +# Исправление slug для Authentik OAuth + +## Проблема + +Endpoint `https://auth.iieasy.ru/application/o/ii-easy-web/.well-known/openid-configuration` возвращает HTML "Not Found". + +## Решение + +Правильный slug в Authentik - это `open-webui`, а не `ii-easy-web`. + +**1. Исправлен `.env` файл:** + +```bash +OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/ +``` + +**2. Перезапустите контейнер:** + +```bash +cd /home/its/iiEasyWeb +sudo docker compose restart open-webui +``` + +**3. Проверьте endpoint:** + +```bash +curl https://auth.iieasy.ru/application/o/open-webui/.well-known/openid-configuration +``` + +Должен вернуться JSON с конфигурацией OpenID Connect. + +**4. Проверьте OAuth:** + +После перезапуска откройте `https://odo.iieasy.ru` и попробуйте войти через кнопку "iiEasy ID" (OAuth). + +## Проверка в Authentik + +В Authentik для Application должен быть настроен: +- **Slug**: `open-webui` +- **Redirect URI**: `https://odo.iieasy.ru/oauth/oidc/callback` +- **Client ID** и **Client Secret** должны совпадать с `.env` diff --git a/FIX_OLLAMA_URL.md b/FIX_OLLAMA_URL.md new file mode 100644 index 0000000..dca3504 --- /dev/null +++ b/FIX_OLLAMA_URL.md @@ -0,0 +1,86 @@ +# Исправление проблемы с OLLAMA_BASE_URL + +## Проблема +В контейнере Open WebUI переменная `OLLAMA_BASE_URL=/ollama` вместо правильного значения `http://ollama:11434`. + +Это приводит к тому, что изображения не передаются в Ollama, так как используется неправильный URL. + +## Решение + +### Шаг 1: Убедитесь, что в docker-compose.yml правильное значение + +В файле `/home/its/iiEasyWeb/docker-compose.yml` на строке 102 должно быть: +```yaml + - OLLAMA_BASE_URL=http://ollama:11434 +``` + +### Шаг 2: Перезапустите контейнер Open WebUI + +```bash +cd /home/its/iiEasyWeb +docker compose restart open-webui +``` + +Или полностью пересоздайте: +```bash +docker compose up -d --force-recreate open-webui +``` + +### Шаг 3: Проверьте переменную в контейнере + +```bash +sudo docker exec open-webui env | grep OLLAMA_BASE_URL +``` + +Должно быть: +``` +OLLAMA_BASE_URL=http://ollama:11434 +``` + +### Шаг 4: Проверьте настройки в веб-интерфейсе + +1. Откройте https://odo.iieasy.ru +2. Перейдите в **Settings → Connections → Ollama API** +3. Убедитесь, что адрес: **`http://ollama:11434`** + - НЕ `/ollama` + - НЕ `host.docker.internal:11434` + - НЕ `localhost:11434` + - Должно быть именно `http://ollama:11434` + +### Шаг 5: Сохраните настройки + +После изменения адреса в веб-интерфейсе нажмите **"Сохранить"**. + +### Шаг 6: Проверьте работу + +1. Откройте чат с моделью `gemma3n:e4b-it-fp16` +2. Загрузите изображение +3. Задайте вопрос о изображении +4. Проверьте логи Ollama - должны появиться запросы с изображениями: + +```bash +sudo docker logs ollama -f | grep -i "image\|vision\|generate" +``` + +## Почему это важно + +Open WebUI использует `OLLAMA_BASE_URL` для формирования полного URL к Ollama API. Если значение неправильное (`/ollama` вместо `http://ollama:11434`), запросы не будут доходить до Ollama, и изображения не будут обрабатываться. + +## Дополнительная диагностика + +Если после перезапуска проблема сохраняется: + +1. Проверьте логи Open WebUI при отправке изображения: +```bash +sudo docker logs open-webui -f | grep -i "ollama\|image\|error" +``` + +2. Проверьте сеть Docker: +```bash +sudo docker network inspect iieasy-ai | grep -A 5 ollama +``` + +3. Проверьте доступность Ollama из Open WebUI: +```bash +sudo docker exec open-webui curl -s http://ollama:11434/api/tags | head -5 +``` diff --git a/FIX_TRACE_ERROR.md b/FIX_TRACE_ERROR.md new file mode 100644 index 0000000..ebc6b58 --- /dev/null +++ b/FIX_TRACE_ERROR.md @@ -0,0 +1,61 @@ +# Исправление ошибки NameError: name 'trace' is not defined + +## Проблема + +В логах Open WebUI появляется ошибка: +``` +NameError: name 'trace' is not defined +``` + +Эта ошибка может возникать, если скрипт `rebrand.sh` случайно изменил код Python в контейнере. + +## Решение + +**1. Пересоздайте контейнер Open WebUI:** + +```bash +cd /home/its/iiEasyWeb +sudo docker compose stop open-webui +sudo docker compose rm -f open-webui +sudo docker compose up -d open-webui +``` + +Это создаст чистый контейнер без изменений в коде. + +**2. Подождите 30-40 секунд** и проверьте статус: + +```bash +sudo docker compose ps open-webui +``` + +**3. Проверьте логи на наличие ошибок:** + +```bash +sudo docker compose logs open-webui --tail 50 +``` + +**4. Если нужно применить ребрендинг:** + +После пересоздания контейнера, если нужно применить логотипы, используйте: +- **Рекомендуется**: Admin Panel Open WebUI (Settings → Appearance → Logo) - это сохраняется в базе данных +- **Альтернатива**: Запустите обновленный скрипт `rebrand.sh` (он был исправлен и больше не должен ломать код) + +## Проверка OAuth + +После пересоздания контейнера проверьте OAuth: + +1. Убедитесь, что в `.env` правильный slug: + ```bash + grep OPENID_CONNECT_ISSUER .env + ``` + Должно быть: `OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/ii-easy-web/` + +2. Проверьте endpoint Authentik: + ```bash + curl https://auth.iieasy.ru/application/o/ii-easy-web/.well-known/openid-configuration + ``` + Должен вернуться JSON, а не HTML "Not Found" + +3. Если endpoint возвращает "Not Found", проверьте в Authentik: + - Application с slug `ii-easy-web` существует + - Redirect URI настроен: `https://odo.iieasy.ru/oauth/oidc/callback` diff --git a/LOGO_FIX.md b/LOGO_FIX.md new file mode 100644 index 0000000..b5dcfea --- /dev/null +++ b/LOGO_FIX.md @@ -0,0 +1,61 @@ +# Решение проблемы с возвратом логотипов после перезапуска + +## Проблема + +После перезапуска контейнера Open WebUI логотипы возвращаются к исходным, так как файлы, скопированные через `docker cp`, теряются при пересоздании контейнера. + +## Решение 1: Через Admin Panel (РЕКОМЕНДУЕТСЯ - постоянное решение) + +Это самый надежный способ, так как настройки сохраняются в базе данных и не теряются при перезапуске. + +1. Откройте Open WebUI: `https://odo.iieasy.ru` или `http://localhost:3001` +2. Войдите как администратор +3. Перейдите в **Settings** → **Appearance** (или **Admin** → **Settings** → **Appearance**) +4. Найдите секцию **Logo** или **Branding** +5. Загрузите файлы: + - **Logo**: загрузите `media/logo.png` + - **Favicon**: загрузите `media/favicon.png` +6. Сохраните изменения + +**Преимущества:** +- Настройки сохраняются в базе данных +- Не теряются при перезапуске контейнера +- Работает надежно + +## Решение 2: Автоматический скрипт после каждого перезапуска + +Создан скрипт `scripts/apply_logos_persistent.sh`, который можно запускать после каждого перезапуска: + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/apply_logos_persistent.sh +``` + +Или добавьте в cron для автоматического запуска: + +```bash +# Добавьте в crontab +crontab -e + +# Добавьте строку (запуск каждые 5 минут, если контейнер запущен) +*/5 * * * * cd /home/its/iiEasyWeb && docker ps | grep -q open-webui && ./scripts/apply_logos_persistent.sh +``` + +## Решение 3: Использование systemd timer (для автоматизации) + +Создайте systemd timer для автоматического применения логотипов после перезапуска контейнера. + +## Временное решение (быстрое применение) + +Если логотипы вернулись прямо сейчас: + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/rebrand.sh +``` + +Но помните - они вернутся после следующего перезапуска контейнера. + +## Рекомендация + +**Используйте Решение 1 (Admin Panel)** - это единственный способ, который гарантирует, что логотипы не вернутся после перезапуска. diff --git a/LOGO_SETUP.md b/LOGO_SETUP.md new file mode 100644 index 0000000..eb4bbe7 --- /dev/null +++ b/LOGO_SETUP.md @@ -0,0 +1,109 @@ +# Инструкция по настройке логотипов и favicon в Open WebUI + +## Проблема: Логотипы не меняются + +Open WebUI может использовать скомпилированные статические файлы или кешировать логотипы. Есть несколько способов решения. + +## Способ 1: Через Admin Panel (рекомендуется) + +Это самый надежный способ, так как настройки сохраняются в базе данных. + +1. Откройте Open WebUI: `https://odo.iieasy.ru` или `http://localhost:3001` +2. Войдите как администратор +3. Перейдите в **Settings** → **Appearance** (или **Admin** → **Settings** → **Appearance`) +4. Найдите секцию **Logo** или **Branding** +5. Загрузите ваши файлы: + - **Logo**: `media/logo.png` или `media/logo-light.svg` + - **Favicon**: `media/favicon.png` или `media/favicon.ico` +6. Сохраните изменения + +## Способ 2: Через скрипт ребрендинга + перезапуск + +1. Убедитесь, что файлы есть в папке `media/`: + ```bash + ls -la media/logo* media/favicon* + ``` + +2. Запустите скрипт ребрендинга: + ```bash + cd /home/its/iiEasyWeb + sudo ./scripts/rebrand.sh + ``` + +3. **ВАЖНО:** Перезапустите контейнер для применения изменений: + ```bash + sudo docker compose restart open-webui + ``` + +4. Очистите кеш браузера: + - **Chrome/Edge**: `Ctrl+Shift+Delete` → Очистить кеш изображений + - **Firefox**: `Ctrl+Shift+Delete` → Кеш + - Или используйте режим инкогнито: `Ctrl+Shift+N` + +5. Обновите страницу с принудительной перезагрузкой: `Ctrl+F5` или `Ctrl+Shift+R` + +## Способ 3: Прямое копирование в контейнер + +Если скрипт не работает, скопируйте файлы вручную: + +```bash +# Найдите где находятся favicon файлы +docker exec open-webui find /app -name "favicon.png" -o -name "favicon.ico" | head -5 + +# Скопируйте ваши файлы +docker cp media/favicon.png open-webui:/app/web/build/_app/immutable/favicon.png +docker cp media/logo.png open-webui:/app/web/build/_app/immutable/logo.png + +# Перезапустите контейнер +docker compose restart open-webui +``` + +## Способ 4: Использование монтированного volume + +Файлы уже смонтированы в контейнер через volume: +- `./media:/app/media:ro` +- `./media:/app/web/static/custom:ro` + +Можно использовать эти пути в настройках Open WebUI или скопировать оттуда: + +```bash +# Скопировать из смонтированной папки в нужное место +docker exec open-webui cp /app/media/logo.png /app/web/build/_app/immutable/logo.png +docker exec open-webui cp /app/media/favicon.png /app/web/build/_app/immutable/favicon.png +``` + +## Проверка + +После применения изменений: + +1. Откройте браузер в режиме инкогнито +2. Откройте `https://odo.iieasy.ru` или `http://localhost:3001` +3. Проверьте favicon во вкладке браузера +4. Проверьте логотип на странице + +## Если ничего не помогает + +1. Проверьте в браузере (F12 → Network), какие файлы запрашиваются: + - Откройте вкладку Network + - Обновите страницу + - Найдите запросы к `favicon.*` или `logo.*` + - Посмотрите полный URL запроса + +2. Найдите эти файлы в контейнере и замените их: + ```bash + docker exec open-webui find /app -path "*/favicon*" -o -path "*/logo*" | grep -v node_modules + ``` + +3. Используйте Admin Panel Open WebUI - это самый надежный способ. + +## Настройка через переменные окружения (если поддерживается) + +Некоторые версии Open WebUI поддерживают переменные окружения для логотипов. Добавьте в `docker-compose.yml`: + +```yaml +environment: + - CUSTOM_LOGO_URL=/static/custom/logo.png + - CUSTOM_FAVICON_URL=/static/custom/favicon.ico +``` + +Но лучше использовать Admin Panel, так как настройки сохраняются в базе данных. diff --git a/QUICK_FIX.md b/QUICK_FIX.md new file mode 100644 index 0000000..53a9c00 --- /dev/null +++ b/QUICK_FIX.md @@ -0,0 +1,70 @@ +# Быстрое исправление Authentik + +## Проблема: Internal Server Error после перезапуска + +## Решение + +**1. Убедитесь, что .env содержит правильный slug:** + +```bash +cd /home/its/iiEasyWeb +grep OPENID_CONNECT_ISSUER .env +``` + +Должно быть: +``` +OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/ii-easy-web/ +``` + +**2. Перезапустите контейнер:** + +```bash +cd /home/its/iiEasyWeb +sudo docker compose restart open-webui +``` + +**3. Подождите 20-30 секунд и проверьте логи:** + +```bash +sudo docker compose logs open-webui --tail 30 | grep -i "oauth\|oidc\|error" +``` + +**4. Проверьте redirect URI в Authentik:** + +В Authentik для Application `ii-easy-web` должен быть настроен redirect URI: +``` +https://odo.iieasy.ru/oauth/oidc/callback +``` + +**ВАЖНО:** +- URL должен быть точно таким (без порта, с https) +- Должен заканчиваться на `/oauth/oidc/callback` (без завершающего слеша) + +**5. Если все еще не работает:** + +Временно используйте форму входа (уже включена в docker-compose.yml): +- Откройте `https://odo.iieasy.ru` +- Войдите через форму входа (не через Authentik) +- После настройки Authentik можно будет переключиться обратно + +## Проверка конфигурации + +```bash +# Проверьте endpoint Authentik +curl https://auth.iieasy.ru/application/o/ii-easy-web/.well-known/openid-configuration + +# Должен вернуться JSON с issuer и endpoints +``` + +## Если нужно отключить Authentik временно + +В `docker-compose.yml` измените: +```yaml +- ENABLE_OAUTH_SIGNUP=false +- ENABLE_LOGIN_FORM=true +``` + +Затем перезапустите: +```bash +sudo docker compose restart open-webui +``` diff --git a/QUICK_START.md b/QUICK_START.md new file mode 100644 index 0000000..ba63ddb --- /dev/null +++ b/QUICK_START.md @@ -0,0 +1,58 @@ +# 🚀 Быстрый старт - Ребрендинг iiEasy + +## ⚡ Быстрое использование + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/rebrand_safe_final.sh +``` + +## 📁 Требуемые файлы + +Поместите в папку `media/`: +- ✅ `logo.png` - **обязательно** (основной логотип) +- ⚪ `favicon.png` - опционально (если нет, используется logo.png) +- ⚪ `logo-dark.svg` - опционально (для темной темы) +- ⚪ `logo-light.svg` - опционально (для светлой темы) + +## ✅ Что делает скрипт + +1. **Заменяет все логотипы и иконки:** + - `logo.png`, `favicon.png`, `favicon.ico` + - `favicon-dark.png`, `apple-touch-icon.png` + - `splash-dark.png`, `splash.png` + +2. **Исправляет ссылки в шаблонах:** + - Все ссылки на favicon → ваш логотип + - Все ссылки на splash → ваш логотип + +3. **Удаляет упоминания "Open WebUI":** + - Текст "(Open WebUI)" из интерфейса + - Ссылки на документацию → `note.iieasy.ru` + +4. **Удаляет элементы интерфейса:** + - Кнопку "Проверить обновления" + - Социальные сети (Discord, Twitter, GitHub) + - Блок "Лицензия" + +## 🔄 После выполнения + +1. **Очистите кеш браузера:** `Ctrl+Shift+Delete` +2. **Проверьте:** `https://odo.iieasy.ru` или `http://localhost:3001` + +## 🔄 После обновления Open WebUI + +```bash +sudo docker compose pull +sudo docker compose up -d +sudo ./scripts/rebrand_safe_final.sh +``` + +## 🐛 Проблемы? + +- **Логотип не изменился?** → Очистите кеш браузера +- **Контейнер не запускается?** → `sudo docker compose restart open-webui` +- **Нужна помощь?** → См. `REBRAND_SOLUTION.md` + +--- +**Полная документация:** `REBRAND_SOLUTION.md` diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a296a6 --- /dev/null +++ b/README.md @@ -0,0 +1,294 @@ +# iiEasy AI-платформа + +Корпоративная AI-платформа на базе Open WebUI с интеграцией Ollama, Qdrant, SearXNG и синхронизацией Nextcloud. + +## Архитектура + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Reverse Proxy (Nginx) │ +│ *.iieasy.ru (odo.iieasy.ru) │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + │ │ │ +┌───────▼────────┐ ┌───────▼────────┐ ┌───────▼────────┐ +│ Open WebUI │ │ Authentik │ │ Nextcloud │ +│ odo.iieasy.ru │ │ auth.iieasy.ru │ │next.iieasy.ru │ +└───────┬────────┘ └─────────────────┘ └───────┬────────┘ + │ │ + │ ┌──────────────────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Docker Network (iieasy-ai) │ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Ollama │ │ Qdrant │ │ SearXNG │ │ +│ │ (GPU) │ │ (Vector) │ │ (Search) │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌───────────────┐ + │ Python Worker │ + │ (Nextcloud → │ + │ Qdrant Sync) │ + └───────────────┘ +``` + +## Компоненты + +- **Open WebUI** - Веб-интерфейс для работы с AI моделями +- **Ollama** - Локальный запуск LLM моделей (Gemma 3) +- **Qdrant** - Векторная база данных для RAG +- **SearXNG** - Метапоисковая система для веб-поиска +- **Authentik** - Централизованный SSO (OIDC) +- **Nextcloud Sync Worker** - Автоматическая синхронизация документов из Nextcloud + +## Быстрый старт + +### Предварительные требования + +- Docker и Docker Compose +- NVIDIA GPU с драйверами (для Ollama) +- Reverse proxy (Nginx) настроенный для доменов *.iieasy.ru +- Authentik настроенный и доступный на auth.iieasy.ru + +### 1. Клонирование и настройка + +```bash +cd /home/its/iiEasyWeb +cp .env.example .env +# Отредактируйте .env и заполните все необходимые переменные +``` + +### 2. Генерация API ключей + +```bash +# Генерация QDRANT_API_KEY +openssl rand -hex 32 +# Добавьте результат в .env +``` + +### 3. Запуск инфраструктуры + +```bash +# Запуск всех сервисов +docker-compose up -d + +# Проверка статуса +docker-compose ps + +# Просмотр логов +docker-compose logs -f +``` + +### 4. Загрузка модели Ollama + +```bash +# Загрузка модели Gemma 3 +docker exec ollama ollama pull gemma3n:e4b-it-fp16 + +# Проверка загруженных моделей +docker exec ollama ollama list +``` + +### 5. Настройка Authentik + +1. Войдите в Authentik (https://auth.iieasy.ru) +2. Создайте OIDC Provider: + - Redirect URI: `https://odo.iieasy.ru/oauth/oidc/callback` + - Client ID и Client Secret скопируйте в `.env` +3. Обновите `OPENID_CONNECT_ISSUER` в `.env` + +### 6. Ребрендинг Open WebUI + +После первого запуска Open WebUI выполните: + +```bash +./scripts/rebrand.sh +``` + +Скрипт заменит: +- Логотипы и favicon +- Текстовые упоминания "Open WebUI" → "iiEasyWeb" +- Отключит проверку обновлений +- Удалит аналитику и телеметрию + +### 7. Настройка API ключа Open WebUI + +1. Откройте https://odo.iieasy.ru +2. Войдите через Authentik SSO +3. Перейдите в Settings → Account → API Keys +4. Создайте новый API ключ +5. Добавьте ключ в `.env` как `OPENWEBUI_API_KEY` + +### 8. Запуск воркера синхронизации Nextcloud + +```bash +cd worker + +# Установка зависимостей +pip install -r requirements.txt + +# Настройка переменных окружения +cp .env.example .env +# Отредактируйте .env + +# Запуск однократной синхронизации +python nextcloud_sync.py --once + +# Или запуск в режиме daemon +python nextcloud_sync.py --daemon +``` + +Для production рекомендуется использовать systemd: + +```bash +# Создайте /etc/systemd/system/iieasy-sync.service +sudo nano /etc/systemd/system/iieasy-sync.service +``` + +```ini +[Unit] +Description=iiEasy Nextcloud Sync Worker +After=network.target + +[Service] +Type=simple +User=its +WorkingDirectory=/home/its/iiEasyWeb/worker +ExecStart=/usr/bin/python3 /home/its/iiEasyWeb/worker/nextcloud_sync.py --daemon +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +```bash +sudo systemctl daemon-reload +sudo systemctl enable iieasy-sync +sudo systemctl start iieasy-sync +``` + +## Переменные окружения + +### Основной .env + +См. `.env.example` для полного списка переменных. Основные: + +- `DOMAIN_OPENWEBUI` - URL Open WebUI +- `DOMAIN_NEXTCLOUD` - URL Nextcloud +- `DOMAIN_AUTHENTIK` - URL Authentik +- `OAUTH_CLIENT_ID` - Client ID из Authentik +- `OAUTH_CLIENT_SECRET` - Client Secret из Authentik +- `OPENID_CONNECT_ISSUER` - Issuer URL Authentik +- `QDRANT_API_KEY` - API ключ Qdrant (сгенерировать) +- `OPENWEBUI_API_KEY` - API ключ Open WebUI (создать после первого запуска) + +### worker/.env + +- `NC_USER` - Пользователь Nextcloud +- `NC_APP_PASSWORD` - App Password (не основной пароль!) +- `NC_SCAN_PATHS` - Пути для сканирования +- `OPENWEBUI_API_KEY` - API ключ Open WebUI + +## Структура проекта + +``` +iiEasyWeb/ +├── docker-compose.yml # Основной compose файл +├── .env # Переменные окружения +├── .env.example # Шаблон переменных +├── .gitignore # Игнорируемые файлы +│ +├── scripts/ +│ └── rebrand_safe_final.sh # ✅ Рекомендуемый скрипт ребрендинга (безопасный) +│ ├── rebrand.sh # ⚠️ Старый скрипт (может ломать OAuth) +│ └── rebrand_fast.sh # ⚠️ Быстрый скрипт (может ломать функциональность) +│ +├── media/ +│ ├── logo-light.svg # Логотип светлая тема +│ ├── logo-dark.svg # Логотип темная тема +│ └── favicon.svg # Favicon +│ +├── worker/ +│ ├── nextcloud_sync.py # Главный скрипт воркера +│ ├── config.py # Конфигурация +│ ├── nextcloud_client.py # WebDAV клиент +│ ├── openwebui_client.py # Open WebUI API клиент +│ ├── document_processor.py # Обработка документов +│ ├── requirements.txt # Python зависимости +│ └── .env.example # Шаблон для воркера +│ +└── README.md # Эта документация +``` + +## Сетевая архитектура + +Все сервисы работают в Docker сети `iieasy-ai` и доступны только внутри сети, кроме Open WebUI, который доступен через reverse proxy. + +### Порты + +- **Open WebUI**: 3001 (внутренний) → Nginx → 443 (HTTPS) +- **Qdrant**: 6333 (gRPC), 6334 (HTTP) - только внутри сети +- **SearXNG**: 8080 - только внутри сети +- **Ollama**: 11434 - только внутри сети + +### Безопасность + +- Все сервисы изолированы в Docker сети +- Доступ к Qdrant только через API ключ +- Open WebUI использует Authentik SSO для аутентификации +- Воркер использует App Password для Nextcloud (не основной пароль) +- CrowdSec и OPNsense фильтруют трафик на уровне reverse proxy + +## Поддерживаемые форматы файлов + +Воркер синхронизации поддерживает: + +- **PDF** (.pdf) - извлечение текста через pypdf +- **DOCX** (.docx, .doc) - извлечение текста через python-docx +- **Текстовые** (.txt, .md, .markdown) - прямое чтение +- **CSV** (.csv) - конвертация в текстовый формат + +Файлы больше 100MB обрабатываются потоково с ограничением количества страниц. + +## Устранение неполадок + +### Open WebUI не подключается к Qdrant + +1. Проверьте, что Qdrant запущен: `docker-compose ps qdrant` +2. Проверьте логи: `docker-compose logs qdrant` +3. Убедитесь, что `QDRANT_API_KEY` установлен в `.env` +4. Проверьте переменную `QDRANT_URI=http://qdrant:6333` + +### Ошибки аутентификации через Authentik + +1. Проверьте redirect URI в Authentik: `https://odo.iieasy.ru/oauth/oidc/callback` +2. Убедитесь, что `OPENID_CONNECT_ISSUER` правильный +3. Проверьте логи Open WebUI: `docker-compose logs open-webui` + +### Воркер не синхронизирует файлы + +1. Проверьте логи: `tail -f worker/sync.log` +2. Убедитесь, что `OPENWEBUI_API_KEY` правильный +3. Проверьте доступность Nextcloud: `curl https://next.iieasy.ru` +4. Проверьте права доступа к путям в `NC_SCAN_PATHS` + +### Ollama не использует GPU + +1. Проверьте драйверы NVIDIA: `nvidia-smi` +2. Установите nvidia-container-toolkit +3. Перезапустите Docker: `sudo systemctl restart docker` +4. Проверьте переменную `NVIDIA_VISIBLE_DEVICES` в `.env` + +## Лицензия + +Внутренний проект iiEasy Research Center. + +## Поддержка + +Для вопросов и проблем обращайтесь к команде разработки iiEasy. diff --git a/REBRANDING.md b/REBRANDING.md new file mode 100644 index 0000000..6f3bab9 --- /dev/null +++ b/REBRANDING.md @@ -0,0 +1,94 @@ +# Ребрендинг Open WebUI для iiEasy + +## ✅ Рекомендуемый скрипт: `rebrand_safe_final.sh` + +**Используйте ТОЛЬКО этот скрипт для ребрендинга!** + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/rebrand_safe_final.sh +``` + +## Что делает скрипт + +1. ✅ **Заменяет логотипы и favicon:** + - Копирует `logo.png` и `favicon.png` из папки `media/` + - Заменяет `splash.png` на ваш логотип + - Заменяет все существующие логотипы везде + +2. ✅ **Удаляет "(Open WebUI)" из интерфейса:** + - Ищет и удаляет только в HTML/Svelte/Python шаблонах + - НЕ трогает JavaScript/TypeScript код - не ломает функциональность + +3. ✅ **Исправляет favicon.png на logo.png:** + - Заменяет `/static/favicon.png` на `/static/logo.png` в шаблонах + - Исправляет API endpoint для изображения профиля модели + +4. ✅ **Заменяет ссылки на документацию:** + - `docs.openwebui.com` → `note.iieasy.ru` + - `open-webui.com/docs` → `note.iieasy.ru` + +5. ✅ **Удаляет проверку обновлений:** + - Кнопка "Проверить обновления" + - Ссылки на GitHub releases + - Текст "(последняя)" и "Посмотреть, что нового" + +6. ✅ **Удаляет социальные сети и GitHub:** + - Discord, Twitter/X, GitHub Repo + - Весь блок "Помощь" с соцсетями + - Badges (img.shields.io) + +7. ✅ **Удаляет блок лицензии:** + - Полностью удаляет блок "Лицензия" + - Удаляет ссылки на enterprise план + +## Почему этот скрипт безопасен + +- ✅ Обрабатывает только HTML/Svelte/Python шаблоны +- ✅ НЕ трогает JavaScript/TypeScript код +- ✅ НЕ комментирует импорты +- ✅ НЕ изменяет конфигурацию OAuth +- ✅ Не ломает функциональность (проверено!) + +## После запуска + +1. **Очистите кеш браузера:** + - Chrome/Edge: Ctrl+Shift+Delete (Cmd+Shift+Delete на Mac) + - Выберите "Изображения и файлы в кеше" + - Очистите кеш + +2. **Проверьте результат:** + - Откройте `https://odo.iieasy.ru` + - Должно быть "Войти в iiEasyWeb" (без "(Open WebUI)") + - Логотип должен отображаться правильно + - Не должно быть ссылок на соцсети и лицензию + +## Если что-то не работает + +**Восстановите контейнер:** + +```bash +cd /home/its/iiEasyWeb +sudo docker compose stop open-webui +sudo docker compose rm -f open-webui +sudo docker compose up -d open-webui +sleep 30 +sudo ./scripts/rebrand_safe_final.sh +``` + +## Другие скрипты (НЕ используйте!) + +- ❌ `rebrand.sh` - может ломать OAuth +- ❌ `rebrand_fast.sh` - может ломать функциональность (500 ошибка) +- ❌ `rebrand_complete.sh` - может ломать функциональность +- ❌ `rebrand_full.sh` - может ломать функциональность + +**Используйте ТОЛЬКО `rebrand_safe_final.sh`!** + +## Файлы логотипов + +Убедитесь, что в папке `media/` есть: +- `logo.png` - основной логотип +- `favicon.png` - favicon + +Эти файлы будут использоваться для ребрендинга. diff --git a/REBRAND_FIX.md b/REBRAND_FIX.md new file mode 100644 index 0000000..db71862 --- /dev/null +++ b/REBRAND_FIX.md @@ -0,0 +1,57 @@ +# Исправление скрипта rebrand.sh для защиты OAuth/Authentik + +## Проблема + +Скрипт `rebrand.sh` заменял `open-webui` на `iieasyweb` во ВСЕХ файлах, включая файлы OAuth/Authentik. Это ломало конфигурацию OAuth, так как: + +1. URL типа `https://auth.iieasy.ru/application/o/open-webui/` заменялись на `https://auth.iieasy.ru/application/o/iieasyweb/` +2. Переменные окружения и конфигурационные строки с `open-webui` могли быть повреждены + +## Решение + +Скрипт `rebrand.sh` был исправлен: + +1. **Исключены файлы OAuth/Authentik из обработки:** + - Добавлены фильтры `! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*"` + - Это защищает все файлы, связанные с OAuth, от изменений + +2. **Удалена замена `open-webui` на `iieasyweb`:** + - Комментированы строки, которые заменяли `open-webui` и `openwebui` в нижнем регистре + - Это предотвращает случайную замену URL и конфигурации + +3. **Оставлена только замена текста интерфейса:** + - Заменяется только "Open WebUI" (с заглавными буквами) на "iiEasyWeb" + - Это безопасно и не влияет на конфигурацию + +## Использование + +Теперь скрипт можно безопасно запускать: + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/rebrand.sh +``` + +OAuth/Authentik конфигурация будет защищена от изменений. + +## Если OAuth все еще не работает + +Если после запуска скрипта OAuth перестал работать: + +1. **Пересоздайте контейнер:** + ```bash + sudo docker compose stop open-webui + sudo docker compose rm -f open-webui + sudo docker compose up -d open-webui + ``` + +2. **Проверьте конфигурацию:** + ```bash + grep OPENID_CONNECT_ISSUER .env + ``` + Должно быть: `OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/` + +3. **Перезапустите контейнер:** + ```bash + sudo docker compose restart open-webui + ``` diff --git a/REBRAND_OAUTH_FIX.md b/REBRAND_OAUTH_FIX.md new file mode 100644 index 0000000..b6d954c --- /dev/null +++ b/REBRAND_OAUTH_FIX.md @@ -0,0 +1,89 @@ +# Исправление проблемы с OAuth после rebrand.sh + +## Проблема + +После запуска `rebrand.sh` OAuth перестает работать и выкидывает на страницу авторизации. Без rebrand все работает нормально. + +## Причина + +Скрипт `rebrand.sh` изменяет файлы Python/JS, включая файлы, связанные с OAuth/аутентификацией, что ломает конфигурацию OAuth. + +## Решение + +### Вариант 1: Использовать безопасный скрипт (РЕКОМЕНДУЕТСЯ) + +Используйте новый скрипт `rebrand_safe.sh`, который изменяет ТОЛЬКО логотипы и favicon: + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/rebrand_safe.sh +``` + +Этот скрипт: +- ✅ Изменяет только логотипы и favicon +- ✅ НЕ изменяет код Python/JS +- ✅ НЕ влияет на OAuth/Authentik + +### Вариант 2: Использовать Admin Panel (НАИБОЛЕЕ БЕЗОПАСНО) + +Для постоянных изменений используйте Admin Panel Open WebUI: + +1. Откройте `https://odo.iieasy.ru` +2. Войдите как администратор +3. Перейдите в **Settings → Appearance → Logo** +4. Загрузите логотипы и favicon из папки `media/` +5. Сохраните - настройки сохранятся в базе данных + +Это самый безопасный способ, так как изменения сохраняются в базе данных и не затрагивают код. + +### Вариант 3: Восстановить контейнер после rebrand.sh + +Если вы уже запустили `rebrand.sh` и OAuth сломался: + +```bash +cd /home/its/iiEasyWeb + +# Пересоздайте контейнер с чистой версией +sudo docker compose stop open-webui +sudo docker compose rm -f open-webui +sudo docker compose up -d open-webui + +# Подождите 30 секунд +sleep 30 + +# Используйте безопасный скрипт для логотипов +sudo ./scripts/rebrand_safe.sh +``` + +## Почему rebrand.sh ломает OAuth? + +Скрипт `rebrand.sh` пытается заменить текст "Open WebUI" во всех файлах, включая: +- `/app/backend/open_webui/utils/oauth.py` - файлы OAuth +- `/app/backend/open_webui/main.py` - может содержать OAuth логику +- Другие файлы, связанные с аутентификацией + +Даже с фильтрами исключения, некоторые файлы могут быть изменены, что ломает OAuth конфигурацию. + +## Рекомендации + +1. **Для логотипов**: Используйте `rebrand_safe.sh` или Admin Panel +2. **Для текста интерфейса**: Используйте переменные окружения в `docker-compose.yml`: + - `WEBUI_NAME=iiEasyWeb` (уже настроено) + - `OAUTH_PROVIDER_NAME=iiEasy ID` (уже настроено) +3. **Избегайте**: Изменения кода Python/JS через `sed` в работающем контейнере + +## Проверка OAuth после восстановления + +После восстановления контейнера проверьте: + +```bash +# Проверьте логи +sudo docker compose logs open-webui --tail 50 | grep -i "oauth\|error" + +# Проверьте конфигурацию +grep OPENID_CONNECT_ISSUER .env +# Должно быть: OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/ + +# Проверьте OAuth в браузере +# Откройте https://odo.iieasy.ru и нажмите "iiEasy ID" +``` diff --git a/REBRAND_SOLUTION.md b/REBRAND_SOLUTION.md new file mode 100644 index 0000000..fe19472 --- /dev/null +++ b/REBRAND_SOLUTION.md @@ -0,0 +1,258 @@ +# Рабочее решение ребрендинга Open WebUI для iiEasy + +## 📋 Обзор + +Скрипт `rebrand_safe_final.sh` - это **безопасное и полное решение** для ребрендинга Open WebUI, которое заменяет все логотипы, иконки и удаляет упоминания "Open WebUI" без нарушения функциональности приложения. + +## ✅ Что делает скрипт + +### 1. Замена логотипов и иконок + +Скрипт заменяет **все** типы логотипов и иконок: + +#### Основные логотипы: +- ✅ `logo.png` - основной логотип +- ✅ `logo.svg` - SVG версия логотипа +- ✅ `logo-light.svg` / `logo-light.png` - для светлой темы +- ✅ `logo-dark.svg` / `logo-dark.png` - для темной темы +- ✅ `splash.png` - логотип на экране загрузки +- ✅ `splash-dark.png` - логотип на экране загрузки (темная тема) +- ✅ `splash-light.png` - логотип на экране загрузки (светлая тема) + +#### Favicon и иконки: +- ✅ `favicon.png` - основная иконка сайта +- ✅ `favicon.ico` - иконка для браузеров +- ✅ `favicon-dark.png` - иконка для темной темы +- ✅ `favicon-light.png` - иконка для светлой темы +- ✅ `apple-touch-icon.png` - иконка для iOS устройств + +### 2. Исправление ссылок в шаблонах + +Скрипт автоматически исправляет ссылки в HTML/Svelte файлах: +- Заменяет `/static/favicon.ico` → `/static/logo.png` +- Заменяет `/static/favicon-dark.png` → `/static/logo.png` +- Заменяет `/static/splash-dark.png` → `/static/logo.png` +- Заменяет `/static/apple-touch-icon.png` → `/static/logo.png` +- Исправляет `href` и `src` атрибуты в HTML тегах + +### 3. Удаление упоминаний "Open WebUI" + +- ✅ Удаляет текст "(Open WebUI)" из всех HTML/Svelte шаблонов +- ✅ Исправляет "Войти в iiEasyWeb (Open WebUI)" → "Войти в iiEasyWeb" +- ✅ Заменяет ссылки на документацию: `docs.openwebui.com` → `note.iieasy.ru` + +### 4. Удаление элементов интерфейса + +- ✅ Удаляет кнопку "Проверить обновления" +- ✅ Удаляет ссылку "(последняя)" на GitHub releases +- ✅ Удаляет "Посмотреть, что нового" +- ✅ Удаляет социальные сети (Discord, Twitter/X, GitHub) +- ✅ Удаляет блок "Помощь" с соцсетями +- ✅ Удаляет блок "Лицензия" полностью + +### 5. Исправление API endpoints + +- ✅ Заменяет `/api/v1/models/model/profile/image` → `/static/logo.png` +- ✅ Исправляет изображения профиля моделей + +## 📁 Структура файлов + +``` +/home/its/iiEasyWeb/ +├── media/ +│ ├── logo.png # Основной логотип (обязательно) +│ ├── favicon.png # Favicon (опционально, иначе используется logo.png) +│ ├── logo-light.svg # Логотип для светлой темы (опционально) +│ └── logo-dark.svg # Логотип для темной темы (опционально) +├── scripts/ +│ └── rebrand_safe_final.sh # Основной скрипт ребрендинга +└── docker-compose.yml # Docker Compose конфигурация +``` + +## 🚀 Использование + +### Требования + +1. Контейнер `open-webui` должен быть запущен +2. Файлы логотипов должны находиться в папке `media/` +3. Минимум требуется `logo.png` + +### Запуск скрипта + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/rebrand_safe_final.sh +``` + +### После выполнения + +1. **Очистите кеш браузера:** + - `Ctrl+Shift+Delete` → Очистить кеш изображений + - Или используйте режим инкогнито: `Ctrl+Shift+N` + +2. **Проверьте результат:** + - Откройте `https://odo.iieasy.ru` или `http://localhost:3001` + - Проверьте favicon в браузере + - Проверьте логотип в окне авторизации + - Проверьте темную тему (если используется) + +3. **Проверьте статические файлы:** + ```bash + # Проверьте, что файлы заменены + curl http://localhost:3001/static/logo.png + curl http://localhost:3001/static/favicon.ico + curl http://localhost:3001/static/splash-dark.png + curl http://localhost:3001/static/apple-touch-icon.png + ``` + +## 🔒 Безопасность + +### Почему скрипт безопасен: + +1. **Не трогает JavaScript/TypeScript код** - обрабатывает только HTML/Svelte/Python шаблоны +2. **Не изменяет OAuth/Authentik** - исключает файлы связанные с аутентификацией +3. **Не ломает функциональность** - только заменяет статические файлы и текст в шаблонах +4. **Использует безопасные sed команды** - точные замены без агрессивных паттернов + +### Что НЕ делает скрипт: + +- ❌ Не изменяет скомпилированные JS файлы (может сломать функциональность) +- ❌ Не трогает OAuth/OIDC конфигурацию +- ❌ Не изменяет базу данных +- ❌ Не удаляет критичные системные файлы + +## 📝 Логика работы + +### Шаг 1: Копирование файлов в статические директории + +Скрипт копирует логотипы во все возможные статические директории: +- `/app/web/build/_app/immutable` +- `/app/web/static` +- `/app/web/build` +- `/app/backend/static` +- `/app/static` +- `/app/web/public` +- `/app/public` + +### Шаг 2: Замена существующих файлов + +Скрипт находит все существующие файлы логотипов и иконок и заменяет их: +```bash +find /app -type f -name "logo.png" -o -name "favicon.png" ... +``` + +### Шаг 3: Исправление ссылок в шаблонах + +Скрипт ищет файлы с упоминаниями иконок и исправляет ссылки: +```bash +find /app/web -type f -name "*.html" -o -name "*.svelte" | grep -l "favicon" +``` + +### Шаг 4: Удаление текста и элементов + +Скрипт удаляет упоминания "Open WebUI" и элементы интерфейса только из Svelte файлов (безопасно). + +### Шаг 5: Перезапуск контейнера + +Скрипт автоматически перезапускает контейнер для применения изменений. + +## 🔄 Обновление после обновления Open WebUI + +После обновления образа Open WebUI нужно запустить скрипт снова: + +```bash +# 1. Обновите образ +sudo docker compose pull + +# 2. Пересоздайте контейнер +sudo docker compose up -d + +# 3. Запустите ребрендинг +sudo ./scripts/rebrand_safe_final.sh +``` + +Или используйте скрипт `update.sh`: + +```bash +sudo ./scripts/update.sh +``` + +## 🐛 Решение проблем + +### Логотип не изменился + +1. **Очистите кеш браузера** - это самая частая причина +2. **Проверьте файлы в контейнере:** + ```bash + sudo docker exec open-webui ls -la /app/web/static/logo.png + ``` +3. **Перезапустите контейнер:** + ```bash + sudo docker compose restart open-webui + ``` + +### Favicon не изменился + +1. Проверьте HTML код страницы (View Source) +2. Убедитесь, что ссылка на favicon исправлена +3. Очистите кеш браузера полностью + +### Элементы интерфейса не удалились + +Элементы могут быть в скомпилированных JS файлах. В этом случае: +1. Проверьте исходные Svelte файлы через `find_settings_elements.sh` +2. Удалите элементы вручную в найденных файлах +3. Или используйте Admin Panel для скрытия элементов + +### Контейнер не запускается после ребрендинга + +Если скрипт сломал контейнер: +```bash +# Восстановите контейнер +sudo docker compose restart open-webui + +# Если не помогло, пересоздайте +sudo docker compose down +sudo docker compose up -d +sudo ./scripts/rebrand_safe_final.sh +``` + +## 📊 Поддерживаемые файлы + +| Тип файла | Обрабатывается | Приоритет | +|-----------|----------------|-----------| +| `logo.png` | ✅ Да | Высокий | +| `favicon.png` | ✅ Да | Высокий | +| `favicon.ico` | ✅ Да | Высокий | +| `favicon-dark.png` | ✅ Да | Средний | +| `apple-touch-icon.png` | ✅ Да | Средний | +| `splash-dark.png` | ✅ Да | Средний | +| `logo-dark.svg` | ✅ Да | Низкий (если есть) | +| `logo-light.svg` | ✅ Да | Низкий (если есть) | + +## ✨ Особенности + +1. **Автоматическое определение темы** - скрипт автоматически использует `logo-dark.svg` для темной темы, если файл существует +2. **Fallback на logo.png** - если специальных файлов нет, используется основной `logo.png` +3. **Безопасная обработка** - скрипт не трогает критичные файлы и не ломает функциональность +4. **Подробный вывод** - скрипт показывает, что именно обрабатывается + +## 📚 Связанные файлы + +- `scripts/rebrand_safe_final.sh` - основной скрипт ребрендинга +- `scripts/update.sh` - скрипт для обновления с автоматическим ребрендингом +- `REBRANDING.md` - общая документация по ребрендингу +- `LOGO_SETUP.md` - инструкция по настройке логотипов + +## 🎯 Итог + +Скрипт `rebrand_safe_final.sh` - это **полное и безопасное решение** для ребрендинга Open WebUI, которое: + +- ✅ Заменяет все логотипы и иконки +- ✅ Исправляет ссылки в шаблонах +- ✅ Удаляет упоминания "Open WebUI" +- ✅ Удаляет ненужные элементы интерфейса +- ✅ Не ломает функциональность приложения +- ✅ Работает после обновлений Open WebUI + +**Используйте этот скрипт для всех операций ребрендинга!** diff --git a/RESTORE_AFTER_500.md b/RESTORE_AFTER_500.md new file mode 100644 index 0000000..07aaebe --- /dev/null +++ b/RESTORE_AFTER_500.md @@ -0,0 +1,56 @@ +# Восстановление после 500 ошибки + +## Проблема + +После запуска `rebrand_fast.sh` появилась ошибка 500 - скрипт сломал функциональность. + +## Решение + +**1. Восстановите контейнер:** + +```bash +cd /home/its/iiEasyWeb + +# Остановите и удалите сломанный контейнер +sudo docker compose stop open-webui +sudo docker compose rm -f open-webui + +# Пересоздайте контейнер с чистой версией +sudo docker compose up -d open-webui + +# Подождите 30 секунд +sleep 30 +``` + +**2. Используйте БЕЗОПАСНЫЙ скрипт:** + +```bash +sudo ./scripts/rebrand_safe_final.sh +``` + +Этот скрипт: +- ✅ Заменяет splash.png на ваш логотип +- ✅ Удаляет "(Open WebUI)" только из HTML/Svelte (не трогает JS/TS код) +- ✅ Заменяет ссылки на документацию +- ✅ Удаляет социальные сети и лицензию +- ❌ НЕ трогает JavaScript/TypeScript файлы - не ломает функциональность + +**3. Проверьте результат:** + +1. Откройте `https://odo.iieasy.ru` +2. Должно работать без ошибок +3. splash.png должен быть заменен на ваш логотип +4. Очистите кеш браузера (Ctrl+Shift+Delete) + +## Что было не так с rebrand_fast.sh + +Скрипт обрабатывал JS/TS файлы и мог случайно сломать код, удалив важные строки или изменив синтаксис. + +## Безопасный подход + +Новый скрипт `rebrand_safe_final.sh` обрабатывает только: +- HTML файлы (безопасно) +- Svelte файлы (шаблоны, безопасно) +- НЕ трогает JS/TS файлы (чтобы не сломать код) + +Это гарантирует, что функциональность не будет нарушена. diff --git a/RESTORE_CONTAINER.md b/RESTORE_CONTAINER.md new file mode 100644 index 0000000..bc3ae92 --- /dev/null +++ b/RESTORE_CONTAINER.md @@ -0,0 +1,55 @@ +# Восстановление контейнера после сломанного ребрендинга + +## Если после rebrand_complete.sh ничего не работает + +**1. Пересоздайте контейнер с чистой версией:** + +```bash +cd /home/its/iiEasyWeb + +# Остановите и удалите контейнер +sudo docker compose stop open-webui +sudo docker compose rm -f open-webui + +# Пересоздайте контейнер +sudo docker compose up -d open-webui + +# Подождите 30 секунд +sleep 30 +``` + +**2. Проверьте, что контейнер работает:** + +```bash +sudo docker compose ps open-webui +sudo docker compose logs open-webui --tail 50 +``` + +**3. Используйте АККУРАТНЫЙ скрипт для ребрендинга:** + +```bash +sudo ./scripts/rebrand_careful.sh +``` + +Этот скрипт изменяет ТОЛЬКО текст в HTML/Svelte файлах, не трогая код. + +## Что делает rebrand_careful.sh + +- ✅ Заменяет логотипы и favicon +- ✅ Заменяет "Open WebUI" на "iiEasyWeb" ТОЛЬКО в текстовом контенте HTML +- ✅ Удаляет "(Open WebUI)" из текста +- ✅ Удаляет "Powered by Open WebUI" футеры +- ❌ НЕ трогает код Python/JS +- ❌ НЕ комментирует импорты +- ❌ НЕ изменяет конфигурацию + +## Альтернатива: Использовать только Admin Panel + +Самый безопасный способ - использовать Admin Panel Open WebUI: + +1. Откройте `https://odo.iieasy.ru` +2. Войдите как администратор +3. Settings → Appearance → Logo +4. Загрузите логотипы из папки `media/` + +Это не сломает функциональность, так как изменения сохраняются в базе данных. diff --git a/SEARXNG_SETUP.md b/SEARXNG_SETUP.md new file mode 100644 index 0000000..5fb625b --- /dev/null +++ b/SEARXNG_SETUP.md @@ -0,0 +1,306 @@ +# Настройка SearXNG для Open WebUI - Рабочее решение + +## Обзор + +Данное решение обеспечивает работу веб-поиска через SearXNG в Open WebUI. Решены проблемы с JSON форматом, лимитером и багом User-Agent в Open WebUI v0.8.3. + +## Архитектура + +``` +Open WebUI → SearXNG → Поисковые движки (Google, DuckDuckGo, Brave и др.) +``` + +## Компоненты решения + +### 1. Конфигурация SearXNG (`searxng/settings.yml`) + +```yaml +# SearXNG Settings для работы с Open WebUI +# Этот файл включает поддержку JSON формата для API запросов + +use_default_settings: true + +server: + secret_key: "CHANGE_ME_SECRET_KEY" + bind_address: "0.0.0.0" + port: 8080 + limiter: false # КРИТИЧНО: отключен для работы внутри Docker сети + method: "GET" + +search: + safe_search: 0 + autocomplete: "google" + formats: + - html + - json # КРИТИЧНО: JSON формат обязателен для Open WebUI + +general: + instance_name: "SearXNG" + debug: false +``` + +**Ключевые моменты:** +- `limiter: false` - отключает защиту от ботов (необходимо для запросов из Open WebUI) +- `formats: [html, json]` - включает JSON формат для API запросов +- Файл монтируется через bind mount, поэтому настройки сохраняются после перезапуска + +### 2. Конфигурация Docker Compose (`docker-compose.yml`) + +```yaml +services: + searxng: + image: ghcr.io/searxng/searxng:latest + container_name: searxng + restart: always + volumes: + - ./searxng:/etc/searxng:rw # Bind mount для сохранения настроек + - searxng_cache:/var/cache/searxng + networks: + - iieasy-ai + environment: + - SEARXNG_BASE_URL=http://searxng:8080/ + deploy: + resources: + limits: + memory: 512M + + open-webui: + image: ghcr.io/open-webui/open-webui:v0.8.3 + container_name: open-webui + restart: unless-stopped + volumes: + - openwebui_data:/app/backend/data + - ./scripts/fix_user_agent.sh:/fix_user_agent.sh:ro # Патч для User-Agent + entrypoint: ["/bin/sh", "-c", "sh /fix_user_agent.sh && exec /bin/bash /app/start.sh"] + networks: + - iieasy-ai + depends_on: + searxng: + condition: service_started + environment: + # SearXNG веб-поиск + - RAG_WEB_SEARCH_ENGINE=searxng + - SEARXNG_QUERY_URL=http://searxng:8080/search?q=&format=json + - ENABLE_WEB_SEARCH=true + - WEB_SEARCH_RESULT_COUNT=5 + - WEB_SEARCH_TRUST_ENV=true + - WEB_SEARCH_CONCURRENT_REQUESTS=1 + - USER_AGENT=Open-WebUI-RAG-Bot +``` + +**Ключевые моменты:** +- `SEARXNG_QUERY_URL` содержит явное указание `format=json` +- Патч User-Agent применяется автоматически через entrypoint +- Контейнеры находятся в одной сети `iieasy-ai` + +### 3. Патч User-Agent (`scripts/fix_user_agent.sh`) + +Патч исправляет баг в Open WebUI v0.8.3, где User-Agent начинается с пробела, что вызывает ошибку "Invalid leading whitespace". + +**Что делает патч:** +- Ищет все варианты проблемной строки `' (https://github.com/open-webui/open-webui) RAG Bot'` +- Заменяет на `'Open-WebUI-RAG-Bot'` +- Исправляет варианты в файлах: + - `/app/backend/open_webui/routers/retrieval.py` + - `/app/backend/open_webui/utils/middleware.py` + - `/app/backend/open_webui/retrieval/loaders/external_web.py` + - И других файлах с проблемной строкой +- Очищает кеш Python (`.pyc` файлы) + +## Установка и настройка + +### Шаг 1: Подготовка файлов + +1. Убедитесь, что файл `searxng/settings.yml` существует и содержит правильную конфигурацию +2. Убедитесь, что файл `scripts/fix_user_agent.sh` существует и исполняемый + +### Шаг 2: Запуск контейнеров + +```bash +cd /home/its/iiEasyWeb +sudo docker compose up -d +``` + +### Шаг 3: Проверка работы + +```bash +# Проверка SearXNG +sudo docker ps | grep searxng + +# Проверка JSON формата +sudo docker exec open-webui curl -s "http://searxng:8080/search?q=test&format=json" | head -c 200 + +# Проверка логов на ошибки +sudo docker logs open-webui --tail 50 | grep -i "error\|user-agent\|invalid" +``` + +### Шаг 4: Настройка в интерфейсе Open WebUI + +1. Откройте Open WebUI в браузере +2. Перейдите в **Settings → Web Search** +3. Убедитесь, что: + - Движок: **SearXNG** + - URL: `http://searxng:8080/search?q=&format=json` + - Переключатель "Web Search" **включен** + - "Одновременные запросы" установлено в **1** или больше (не 0) + +## Скрипты для обслуживания + +### Диагностика (`scripts/diagnose_search.sh`) + +Проверяет все компоненты системы поиска: + +```bash +sudo ./scripts/diagnose_search.sh +``` + +### Исправление конфигурации SearXNG (`scripts/fix_searxng_config.sh`) + +Исправляет конфигурацию SearXNG после перезапуска: + +```bash +sudo ./scripts/fix_searxng_config.sh +``` + +### Агрессивное исправление User-Agent (`scripts/fix_user_agent_aggressive.sh`) + +Ищет и исправляет проблемную строку User-Agent во всех файлах: + +```bash +sudo ./scripts/fix_user_agent_aggressive.sh +``` + +### Полное исправление (`scripts/fix_search_complete.sh`) + +Выполняет все исправления за один раз: + +```bash +sudo ./scripts/fix_search_complete.sh +``` + +## Решенные проблемы + +### 1. Ошибка "403 Forbidden" при запросе JSON + +**Причина:** SearXNG по умолчанию не разрешает JSON формат для безопасности. + +**Решение:** Добавлено `formats: [html, json]` в секцию `search:` файла `settings.yml`. + +### 2. Ошибка "Invalid leading whitespace" в User-Agent + +**Причина:** Баг в Open WebUI v0.8.3 - User-Agent начинается с пробела. + +**Решение:** Патч `fix_user_agent.sh` автоматически исправляет проблемную строку при старте контейнера. + +### 3. Ошибка "X-Forwarded-For nor X-Real-IP header is set" + +**Причина:** SearXNG блокирует запросы без реального IP (защита от ботов). + +**Решение:** Отключен лимитер (`limiter: false`) в `settings.yml`, так как система работает внутри закрытой Docker сети. + +### 4. Потеря настроек после перезапуска + +**Причина:** Изменения внутри контейнера не сохраняются. + +**Решение:** Использован bind mount `./searxng:/etc/searxng:rw` для сохранения `settings.yml` на хосте. + +## Улучшение покрытия поиска + +Если нужно больше результатов, можно включить дополнительные движки: + +```bash +# Включить DuckDuckGo +sudo docker exec searxng sed -i '/- name: duckduckgo/,/disabled:/ s/disabled: true/disabled: false/' /etc/searxng/settings.yml + +# Включить Brave +sudo docker exec searxng sed -i '/- name: brave$/,/disabled:/ s/disabled: true/disabled: false/' /etc/searxng/settings.yml + +# Перезапустить +sudo docker restart searxng +``` + +## Проверка работоспособности + +### Тест 1: Прямой запрос к SearXNG + +```bash +sudo docker exec open-webui curl -s "http://searxng:8080/search?q=test&format=json" | grep -q "results" && echo "✓ JSON работает" || echo "✗ JSON не работает" +``` + +### Тест 2: Проверка патча User-Agent + +```bash +sudo docker exec open-webui grep -r "github.com/open-webui.*RAG Bot" /app/backend 2>/dev/null | wc -l +# Должно вернуть 0 +``` + +### Тест 3: Поиск в интерфейсе + +1. Откройте чат в Open WebUI +2. Задайте вопрос с включенным поиском (например: "Какая погода в Уфе сегодня?") +3. Должны появиться результаты поиска без ошибок + +## Логи и отладка + +### Просмотр логов SearXNG + +```bash +sudo docker logs searxng --tail 50 +``` + +### Просмотр логов Open WebUI + +```bash +sudo docker logs open-webui --tail 50 | grep -i "searxng\|error\|user-agent" +``` + +### Проверка сетевого подключения + +```bash +# Из контейнера Open WebUI к SearXNG +sudo docker exec open-webui curl http://searxng:8080/status +``` + +## Важные замечания + +1. **Безопасность:** Лимитер отключен только для внутренней сети Docker. Если выставляете SearXNG наружу, включите лимитер обратно. + +2. **Производительность:** Ограничение памяти SearXNG до 512M предотвращает перегрузку системы. + +3. **Стабильность:** Некоторые движки (Google, Bing) могут блокировать запросы с серверов. Это нормально - SearXNG использует другие доступные движки. + +4. **Обновления:** При обновлении образа Open WebUI патч User-Agent будет применяться автоматически благодаря entrypoint в docker-compose.yml. + +## Структура файлов + +``` +/home/its/iiEasyWeb/ +├── docker-compose.yml # Конфигурация Docker Compose +├── searxng/ +│ └── settings.yml # Конфигурация SearXNG (bind mount) +└── scripts/ + ├── fix_user_agent.sh # Основной патч User-Agent + ├── fix_user_agent_aggressive.sh # Агрессивный патч + ├── fix_user_agent_final.sh # Финальный патч + ├── fix_searxng_config.sh # Исправление конфигурации SearXNG + ├── diagnose_search.sh # Диагностика системы поиска + └── fix_search_complete.sh # Полное исправление +``` + +## Версия + +- Open WebUI: v0.8.3 +- SearXNG: latest (2026.2.16+8e824017d) +- Дата настройки: Февраль 2026 + +## Поддержка + +При возникновении проблем: + +1. Запустите диагностику: `sudo ./scripts/diagnose_search.sh` +2. Проверьте логи: `sudo docker logs open-webui --tail 100` +3. Выполните полное исправление: `sudo ./scripts/fix_search_complete.sh` + +--- + +**Решение протестировано и работает стабильно.** diff --git a/TEST_VISION.md b/TEST_VISION.md new file mode 100644 index 0000000..0ed8be6 --- /dev/null +++ b/TEST_VISION.md @@ -0,0 +1,93 @@ +# Тестирование Vision Capabilities модели gemma3n:e4b-it-fp16 + +## Быстрый тест через скрипт + +Запустите скрипт с правами sudo: + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/test_vision.sh +``` + +Скрипт автоматически: +1. Проверит, что контейнер Ollama запущен +2. Проверит, что модель gemma3n:e4b-it-fp16 загружена +3. Скачает тестовое изображение +4. Отправит запрос к Ollama API с изображением +5. Покажет ответ модели + +## Ручной тест через терминал + +Если скрипт не работает, выполните команды вручную: + +### 1. Проверка контейнера и модели + +```bash +# Проверка контейнера +sudo docker ps | grep ollama + +# Проверка загруженных моделей +sudo docker exec ollama ollama list +``` + +### 2. Подготовка изображения + +```bash +cd /home/its/iiEasyWeb +mkdir -p test_images +cd test_images + +# Скачайте тестовое изображение +curl -L -o test_image.jpg "https://yandex-images.clstorage.net/PU5vN2154/532297ZKm/lCuSfyMn0DdbJqcFVeFiB9Ti31Te2dZ1EepiRw3Cs0Qw8cQ1ND5OQRKC1yH4LnhdRtloQ4aXHng5ZSLNmXHy_8k293YSMsBWKnOvYAXBbhPcl6pYmqi-ZGWDazZo2pYkJNHpkJHrg5yiO0bEOIeEICe5cFqrojYgyNQ6mHj4e5IUb_Lri3uxo9fmXv0dMf7f1NvH9J5YVsyhRvvmtD9eTc1QfVxV42d8OotKrLTDfHx7jfDqjpIqHAyt9ngIIHsSjLtOJ7_jTYdHLn4hJWlCj_jj69gGlApIBYaTfoxPrtHFDGlYUd-PjHI3hijAm6hIb-lUFlpOehQAJTbwEGwjcqjjVns323WrR2q6CalVyg9kZ8s0jkBm8OVGr26UH86NHaD1rKnPH6zinwJIKBv8VEdF_FrGLt6QFOVqFOQY776w47YDNy8Biwfefg0lxVaL2E9L5NbAztwh8vuCcMNSEfG8aYQFBxe0Eu9CMPhz1Axz1dgqmkpSYCAR8nSA_GMSKKPa9-tzIUc7ooKthV1ykwSjrzhORKq0eRrvBlTbajF5yAUoVX-7XM7HouyU51xMaxFMBl7atixQFW4o1BznMoi_chuL8_mTO_IKce21CosEQ6Owkkhi9EGSz5pIE56NjURBxKn7pwTKt6K4FB_YYHexZG4qlsIg8GVaiJyIB0p0bx5PF4_FIy-SItWZId5f8M9_RIqg2hRVCqP2hDvSfWGAYUjJd6c8lr9mgGj7kPQbVQBCUgIm1NB9lsxQrIO-wO92fytbOQsjjiqBqa0CK9Qjy6wi6CoIfX5vZjw_pj1BKGm0UVOXuArPFngIN2AUgz3w4mZSTvz8IQYo_IhPVlQ7PiOjhyWD6wZSVZ3NDrvsC-8w4lxmzE32T1qMuy5JOTRxaNHHH0QCp4IkgMv8LH8JeJaOUj4sVOGCBBgQi5KMX74TQzOBW9cGTu35UZr3qAfj5O6kMogxjqNanMOs" +``` + +### 3. Тест через Ollama API + +```bash +# Закодируйте изображение в base64 +IMAGE_B64=$(base64 -w 0 test_image.jpg) + +# Отправьте запрос к Ollama API +sudo docker exec ollama sh -c "curl -s -X POST http://localhost:11434/api/generate \ + -H 'Content-Type: application/json' \ + -d '{ + \"model\": \"gemma3n:e4b-it-fp16\", + \"prompt\": \"Опиши это изображение на русском языке. Что ты видишь на картинке?\", + \"images\": [\"'$IMAGE_B64'\"], + \"stream\": false + }' | jq -r '.response'" +``` + +Если `jq` не установлен, используйте: + +```bash +sudo docker exec ollama sh -c "curl -s -X POST http://localhost:11434/api/generate \ + -H 'Content-Type: application/json' \ + -d '{ + \"model\": \"gemma3n:e4b-it-fp16\", + \"prompt\": \"Опиши это изображение на русском языке. Что ты видишь на картинке?\", + \"images\": [\"'$IMAGE_B64'\"], + \"stream\": false + }'" | grep -o '"response":"[^"]*"' | sed 's/"response":"\(.*\)"/\1/' | sed 's/\\n/\n/g' | sed 's/\\"/"/g' +``` + +## Тест через веб-интерфейс Open WebUI (рекомендуется) + +1. Откройте Open WebUI: https://odo.iieasy.ru +2. Выберите модель: **gemma3n:e4b-it-fp16** +3. Найдите кнопку загрузки изображения в поле ввода (обычно иконка скрепки 📎 или фото 📷) +4. Загрузите изображение: `/home/its/iiEasyWeb/test_images/test_image.jpg` +5. Задайте вопрос: **"Опиши это изображение на русском языке. Что ты видишь на картинке?"** + +## Ожидаемый результат + +Модель должна описать содержимое изображения: +- Объекты на картинке +- Цвета и композицию +- Детали и контекст + +Если модель не видит изображение, проверьте: +- Поддерживает ли модель vision (gemma3n:e4b-it-fp16 поддерживает) +- Правильно ли загружается изображение в интерфейсе +- Логи: `sudo docker logs ollama --tail 50` +- Логи Open WebUI: `sudo docker logs open-webui --tail 50` diff --git a/TROUBLESHOOTING.md b/TROUBLESHOOTING.md new file mode 100644 index 0000000..76200f9 --- /dev/null +++ b/TROUBLESHOOTING.md @@ -0,0 +1,102 @@ +# Устранение проблем: 502 Bad Gateway + +## Диагностика проблемы 502 Bad Gateway + +### Шаг 1: Проверка контейнера Open WebUI + +```bash +# Проверьте статус контейнера +sudo docker ps | grep open-webui + +# Если контейнер не запущен, запустите его +cd /home/its/iiEasyWeb +sudo docker compose up -d open-webui + +# Проверьте логи +sudo docker compose logs open-webui --tail 50 +``` + +### Шаг 2: Проверка доступности порта + +```bash +# Проверьте, что порт 3001 слушается +sudo netstat -tlnp | grep 3001 +# или +sudo ss -tlnp | grep 3001 + +# Проверьте доступность локально +curl -I http://localhost:3001 +curl -I http://127.0.0.1:3001 +``` + +### Шаг 3: Проверка настроек Nginx Proxy Manager + +В Nginx Proxy Manager для `odo.iieasy.ru` проверьте: + +1. **Details:** + - **Forward Hostname/IP**: + - Если NPM на той же машине: `localhost` или `127.0.0.1` + - Если NPM на другой машине: IP адрес машины с Open WebUI + - **Forward Port**: `3001` (порт на хосте, не в контейнере) + +2. **Advanced:** + - **Custom Nginx Configuration**: Оставьте ПУСТЫМ (может вызывать ошибки 500/502) + +### Шаг 4: Если используете другую машину + +Если Open WebUI на другой машине: + +1. Убедитесь, что порт 3001 доступен с машины Nginx Proxy Manager: + ```bash + # С машины NPM проверьте доступность + curl http://IP_ДРУГОЙ_МАШИНЫ:3001 + ``` + +2. Проверьте firewall: + ```bash + # На машине с Open WebUI разрешите порт 3001 + sudo ufw allow 3001/tcp + # или + sudo firewall-cmd --add-port=3001/tcp --permanent + ``` + +3. В Nginx Proxy Manager укажите: + - **Forward Hostname/IP**: IP адрес другой машины + - **Forward Port**: `3001` + +## Быстрое решение + +Если контейнер не запущен или не отвечает: + +```bash +cd /home/its/iiEasyWeb + +# Перезапустите все сервисы +sudo docker compose down +sudo docker compose up -d + +# Подождите 30 секунд +sleep 30 + +# Проверьте статус +sudo docker compose ps + +# Проверьте доступность +curl http://localhost:3001 +``` + +## Частые причины 502 Bad Gateway + +1. **Контейнер не запущен** → Запустите: `sudo docker compose up -d open-webui` +2. **Контейнер постоянно перезапускается** → Проверьте логи: `sudo docker compose logs open-webui` +3. **Неправильный Forward Hostname/IP** → Используйте `localhost` если NPM на той же машине +4. **Неправильный Forward Port** → Используйте `3001` (порт на хосте), не `8080` (порт в контейнере) +5. **Custom Configuration конфликтует** → Оставьте поле пустым +6. **Firewall блокирует** → Разрешите порт 3001 +7. **Контейнер на другой машине недоступен** → Проверьте сетевую доступность + +## Проверка после исправления + +1. Откройте `https://odo.iieasy.ru` в браузере +2. Если все еще 502, проверьте логи Nginx Proxy Manager +3. Проверьте логи Open WebUI: `sudo docker compose logs open-webui` diff --git a/VISION_MODELS.md b/VISION_MODELS.md new file mode 100644 index 0000000..7c61884 --- /dev/null +++ b/VISION_MODELS.md @@ -0,0 +1,100 @@ +# Vision модели для Ollama + +## Проблема с gemma3n:e4b-it-fp16 + +Модель `gemma3n:e4b-it-fp16` может не поддерживать vision правильно или требует специальной настройки. Рекомендуется использовать специализированные vision модели. + +## Рекомендуемые Vision модели + +### 1. LLaVA (Large Language-and-Vision Assistant) - РЕКОМЕНДУЕТСЯ + +**Модель:** `llava:latest` или `llava:7b` + +**Характеристики:** +- 7B параметров +- Версия 1.6 (обновлена в феврале 2024) +- Улучшенное распознавание текста +- Высокое разрешение изображений (в 4 раза больше пикселей) +- Хорошо работает с документами, диаграммами, таблицами +- Лицензия: Apache 2.0 или LLaMA 2 Community License + +**Установка:** +```bash +sudo docker exec ollama ollama pull llava:latest +``` + +**Использование:** +- В Open WebUI выберите модель `llava:latest` +- Загрузите изображение +- Задайте вопрос о изображении + +### 2. BakLLaVA + +**Модель:** `bakllava:latest` + +**Характеристики:** +- 7B параметров +- Комбинация Mistral 7B + LLaVA архитектура +- Контекстное окно: 32K + +**Установка:** +```bash +sudo docker exec ollama ollama pull bakllava:latest +``` + +### 3. Llama 3.2 Vision + +**Модель:** `llama3.2-vision:latest` или `llama3.2-vision:11b` + +**Характеристики:** +- 11B параметров (требует 8GB VRAM) +- 90B версия доступна (требует 64GB VRAM) +- Контекстное окно: 128K +- Оптимизирована для визуального распознавания, анализа изображений +- Поддержка OCR, распознавание рукописного текста +- Анализ графиков и таблиц + +**Установка:** +```bash +sudo docker exec ollama ollama pull llama3.2-vision:11b +``` + +## Быстрая установка + +Используйте скрипт для установки: + +```bash +cd /home/its/iiEasyWeb + +# Установить LLaVA (рекомендуется) +sudo ./scripts/install_vision_model.sh llava + +# Или BakLLaVA +sudo ./scripts/install_vision_model.sh bakllava + +# Или Llama 3.2 Vision +sudo ./scripts/install_vision_model.sh llama3.2 +``` + +## Тестирование Vision модели + +После установки протестируйте: + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/test_direct_vision.sh +``` + +(Измените MODEL в скрипте на установленную vision модель) + +## Использование в Open WebUI + +1. Откройте https://odo.iieasy.ru +2. Перейдите в Settings → Models (или выберите модель в чате) +3. Выберите установленную vision модель (например, `llava:latest`) +4. Загрузите изображение через кнопку загрузки (📎 или 📷) +5. Задайте вопрос о изображении + +## Рекомендация + +**Для лучшей совместимости с Open WebUI рекомендуется использовать `llava:latest`** - это самая популярная и хорошо поддерживаемая vision модель в Ollama. diff --git a/check_authentik.sh b/check_authentik.sh new file mode 100755 index 0000000..22238dd --- /dev/null +++ b/check_authentik.sh @@ -0,0 +1,47 @@ +#!/bin/bash +# Скрипт проверки конфигурации Authentik + +cd /home/its/iiEasyWeb + +echo "=== Проверка конфигурации Authentik ===" +echo "" + +# Проверка .env +echo "1. Проверка .env файла:" +if grep -q "ii-easy-web" .env; then + echo " ✓ OPENID_CONNECT_ISSUER содержит правильный slug: ii-easy-web" + grep "OPENID_CONNECT_ISSUER" .env +else + echo " ✗ OPENID_CONNECT_ISSUER не содержит правильный slug" + echo " Текущее значение:" + grep "OPENID_CONNECT_ISSUER" .env +fi + +echo "" +echo "2. Проверка endpoint Authentik:" +ENDPOINT=$(grep "OPENID_CONNECT_ISSUER" .env | cut -d'=' -f2).well-known/openid-configuration +echo " URL: $ENDPOINT" + +RESPONSE=$(curl -s "$ENDPOINT" 2>&1) +if echo "$RESPONSE" | grep -q '"issuer"'; then + echo " ✓ Endpoint доступен и возвращает JSON" + echo "$RESPONSE" | python3 -c "import sys, json; d=json.load(sys.stdin); print(' Issuer:', d.get('issuer'))" 2>/dev/null || echo " JSON валидный" +else + echo " ✗ Endpoint недоступен или возвращает ошибку" + echo " Ответ: $(echo "$RESPONSE" | head -3)" +fi + +echo "" +echo "3. Проверка переменных окружения в docker-compose.yml:" +echo " OPENID_PROVIDER_URL будет: ${ENDPOINT}" +echo " OPENID_REDIRECT_URI будет: $(grep DOMAIN_OPENWEBUI .env | cut -d'=' -f2)/oauth/oidc/callback" + +echo "" +echo "4. Что проверить в Authentik:" +echo " - Application slug должен быть: ii-easy-web" +echo " - Redirect URI должен быть: $(grep DOMAIN_OPENWEBUI .env | cut -d'=' -f2)/oauth/oidc/callback" +echo " - Client ID должен совпадать с OAUTH_CLIENT_ID в .env" + +echo "" +echo "5. Для применения изменений выполните:" +echo " sudo docker compose restart open-webui" diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f094cee --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,150 @@ +services: + ollama: + image: ollama/ollama:latest + container_name: ollama + restart: unless-stopped + volumes: + - ollama_data:/root/.ollama + networks: + - iieasy-ai + deploy: + resources: + reservations: + devices: + - driver: nvidia + count: all + capabilities: [gpu] + environment: + - NVIDIA_VISIBLE_DEVICES=${NVIDIA_VISIBLE_DEVICES:-all} + healthcheck: + test: ["CMD", "ollama", "list"] + interval: 30s + timeout: 10s + retries: 3 + + qdrant: + image: qdrant/qdrant:latest + container_name: qdrant + restart: unless-stopped + volumes: + - qdrant_data:/qdrant/storage + ports: + - "6333:6333" # gRPC API + - "6334:6334" # HTTP API (только внутри сети) + networks: + - iieasy-ai + environment: + - QDRANT_API_KEY=${QDRANT_API_KEY} + healthcheck: + test: ["CMD-SHELL", "timeout 1 bash -c '&format=json + - ENABLE_WEB_SEARCH=true + - WEB_SEARCH_RESULT_COUNT=5 + - WEB_SEARCH_TRUST_ENV=true + - WEB_SEARCH_CONCURRENT_REQUESTS=1 + - USER_AGENT=Open-WebUI-RAG-Bot + + # Ollama API для работы с изображениями + - OLLAMA_BASE_URL=http://ollama:11434 + + # Authentik OIDC SSO + - OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID} + - OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET} + - OAUTH_PROVIDER_NAME=iiEasy ID + # Правильный формат для Authentik: полный URL до .well-known/openid-configuration + - OPENID_PROVIDER_URL=${OPENID_CONNECT_ISSUER}.well-known/openid-configuration + - OPENID_REDIRECT_URI=${DOMAIN_OPENWEBUI}/oauth/oidc/callback + - ENABLE_OAUTH_SIGNUP=true + - ENABLE_LOGIN_FORM=true + # Форма входа включена как fallback, если OAuth не работает + # Можно отключить после полной настройки OAuth: ENABLE_LOGIN_FORM=false + - OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true + + # Bitwarden CLI интеграция (подготовка) + - BW_CLIENTID=${BW_CLIENTID} + - BW_CLIENTSECRET=${BW_CLIENTSECRET} + + # Отключение проверки обновлений и аналитики + - ENABLE_PERSISTENT_CONFIG=true + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/health"] + interval: 30s + timeout: 10s + retries: 3 + +volumes: + ollama_data: + driver: local + qdrant_data: + driver: local + searxng_cache: + driver: local + openwebui_data: + driver: local + +networks: + iieasy-ai: + driver: bridge diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..76aebba --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,288 @@ +# Инструкция по развёртыванию iiEasyWeb + +Пошаговое развёртывание: Git, Docker, ребрендинг, поиск (SearXNG), воркер Nextcloud (Python). + +--- + +## Требования + +- Docker и Docker Compose +- (Опционально) NVIDIA GPU и драйверы — для Ollama +- Доступ к Gitea (например `192.168.88.165:3000`) +- Nextcloud и Authentik — при использовании воркера и OAuth + +--- + +## Архитектура + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Reverse Proxy (Nginx) │ +│ *.iieasy.ru (odo.iieasy.ru) │ +└─────────────────────────────────────────────────────────────┘ + │ + ┌───────────────────┼───────────────────┐ + │ │ │ +┌───────▼────────┐ ┌───────▼────────┐ ┌───────▼────────┐ +│ Open WebUI │ │ Authentik │ │ Nextcloud │ +│ odo.iieasy.ru │ │ auth.iieasy.ru │ │next.iieasy.ru │ +└───────┬────────┘ └─────────────────┘ └───────┬────────┘ + │ │ + │ ┌──────────────────────────────────────┘ + │ │ + ▼ ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Docker Network (iieasy-ai) │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Ollama │ │ Qdrant │ │ SearXNG │ │ +│ │ (GPU) │ │ (Vector) │ │ (Search) │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌───────────────┐ + │ Python Worker │ + │ (Nextcloud → │ + │ Qdrant Sync) │ + └───────────────┘ +``` + +--- + +## 1. Git — отправка кода в репозиторий + +**Сервер Gitea:** `192.168.88.165`, веб-интерфейс на порту `3000`. +**Репозиторий:** `ars/iiEsaywebUI`. + +### Что не должно попадать в Git + +- `.env`, `worker/.env` +- `worker/.venv`, секретные ключи, токены +- Убедитесь, что в `.gitignore` есть эти пути (в проекте уже настроено). + +### Вариант A — SSH + +1. Настройте `~/.ssh/config` (при необходимости укажите порт SSH сервера, если не 22): + + ``` + Host 192.168.88.165 + HostName 192.168.88.165 + User git + Port 22 + ``` + +2. URL репозитория: `git@192.168.88.165:ars/iiEsaywebUI.git` + +### Вариант B — HTTPS с токеном + +- URL: `https://192.168.88.165:3000/ars/iiEsaywebUI.git` +- При push: логин `ars`, пароль — **подставьте свой токен из Gitea** (или пароль учётной записи, в зависимости от настроек Gitea). Не храните токен в URL в документации. + +### Шаги (если репозиторий ещё не инициализирован) + +```bash +cd /home/its/iiEasyWeb +git init +git remote add origin git@192.168.88.165:ars/iiEsaywebUI.git +# или HTTPS: git remote add origin https://192.168.88.165:3000/ars/iiEsaywebUI.git +git add . +git status # проверьте, что нет .env и секретов +git commit -m "Initial deployment setup" +git branch -M main +git push -u origin main +``` + +Если ветка по умолчанию на Gitea — `master`, используйте `git push -u origin master`. + +--- + +## 2. Docker — поднятие стека + +**Файлы:** корень проекта — `docker-compose.yml`, `.env.example`. + +### Шаги + +1. **Каталог и конфиг:** + + ```bash + cd /home/its/iiEasyWeb + cp .env.example .env + ``` + +2. **Редактирование `.env`:** + - Домены: `DOMAIN_OPENWEBUI`, `DOMAIN_NEXTCLOUD`, `DOMAIN_AUTHENTIK`, при необходимости `DOMAIN_VAULTWARDEN`. + - Authentik: `OAUTH_CLIENT_ID`, `OAUTH_CLIENT_SECRET`, `OPENID_CONNECT_ISSUER`. + - Qdrant — сгенерировать ключ и прописать в `QDRANT_API_KEY`: + + ```bash + openssl rand -hex 32 + ``` + + - Остальное по необходимости (Nextcloud для воркера, Vaultwarden, Ollama). Для GPU: `NVIDIA_VISIBLE_DEVICES=all`. + +3. **Запуск:** + + ```bash + docker compose up -d + docker compose ps + ``` + +4. **Модель Ollama (после старта контейнера ollama):** + + ```bash + docker exec ollama ollama pull gemma3n:e4b-it-fp16 + docker exec ollama ollama list + ``` + +5. **Проверка:** Open WebUI доступен на порту **3001** (маппинг `3001:8080` в `docker-compose.yml`), т.е. `http://localhost:3001` или ваш `DOMAIN_OPENWEBUI`. + +--- + +## 3. Ребрендинг + +**Используйте только скрипт:** `scripts/rebrand_safe_final.sh`. Старый `rebrand.sh` может ломать OAuth. + +### Медиафайлы + +Поместите в каталог `media/`: +- **Обязательно:** `media/logo.png` +- По желанию: `favicon.png`, `logo-dark.svg`, `logo-light.svg` + +### Запуск ребрендинга + +Контейнер `open-webui` должен быть запущен. + +```bash +cd /home/its/iiEasyWeb +sudo ./scripts/rebrand_safe_final.sh +``` + +### После обновления образа Open WebUI + +```bash +docker compose pull +docker compose up -d +sudo ./scripts/rebrand_safe_final.sh +``` + +### Проверка + +Очистите кеш браузера и откройте `http://localhost:3001` или `DOMAIN_OPENWEBUI`. + +Подробнее: `QUICK_START.md`, `REBRAND_SOLUTION.md`. + +--- + +## 4. Поиск (SearX / SearXNG) + +Поиск уже включён в `docker-compose.yml`: сервис `searxng`, Open WebUI настроен на него (`RAG_WEB_SEARCH_ENGINE=searxng`, `SEARXNG_QUERY_URL=...&format=json`, `ENABLE_WEB_SEARCH=true`). + +### Настройка при развёртывании + +1. **Секретный ключ SearXNG** + В `searxng/settings.yml` замените `CHANGE_ME_SECRET_KEY` на свою строку (любая случайная строка). Иначе в логах возможны предупреждения. + +2. **Проверка после старта:** + + ```bash + docker exec open-webui curl -s "http://searxng:8080/search?q=test&format=json" | head -c 200 + ``` + +3. **Если появляется ошибка User-Agent (Invalid leading whitespace):** + Используйте проверенный скрипт **fix_user_agent_final.sh**. В `docker-compose.yml` для сервиса `open-webui` добавьте: + - volume: `./scripts/fix_user_agent_final.sh:/fix_user_agent_final.sh:ro` + - entrypoint: `["/bin/sh", "-c", "sh /fix_user_agent_final.sh && exec /bin/bash /app/start.sh"]` + Затем перезапустите: `docker compose restart open-webui`. + +4. **Диагностика:** + + ```bash + sudo ./scripts/diagnose_search.sh + ``` + + При проблемах: `sudo ./scripts/fix_search_complete.sh`. + +Подробнее: `SEARXNG_SETUP.md`. + +--- + +## 5. Nextcloud Python (воркер) + +Воркер синхронизации Nextcloud → Open WebUI/Qdrant запускается на хосте (не в Docker). + +**Каталог:** `worker/`. Файлы: `nextcloud_sync.py`, `.env.example`, `requirements.txt`. + +### Переменные окружения + +```bash +cd /home/its/iiEasyWeb/worker +cp .env.example .env +``` + +Заполните в `.env`: +- **Nextcloud:** `DOMAIN_NEXTCLOUD`, `NC_USER`, `NC_APP_PASSWORD`, при необходимости `NC_SCAN_PATHS`. +- **Open WebUI:** `DOMAIN_OPENWEBUI`, `OPENWEBUI_API_KEY` (ключ создаётся в Open WebUI: Settings → Account → API Keys). +- По желанию: `SYNC_INTERVAL`, `MAX_FILE_SIZE`, `LOG_LEVEL`. + +**Важно:** для Nextcloud используйте **пароль приложения** (App Password), не основной пароль учётной записи. + +### Установка и запуск + +```bash +pip install -r requirements.txt +python nextcloud_sync.py --once +``` + +Постоянный режим (daemon): + +```bash +python nextcloud_sync.py --daemon +``` + +### Production (systemd) + +Создайте файл `/etc/systemd/system/iieasy-sync.service`: + +```ini +[Unit] +Description=iiEasy Nextcloud Sync Worker +After=network.target + +[Service] +Type=simple +User=its +WorkingDirectory=/home/its/iiEasyWeb/worker +ExecStart=/usr/bin/python3 /home/its/iiEasyWeb/worker/nextcloud_sync.py --daemon +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +Замените `User=its` и пути при необходимости. Затем: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable iieasy-sync +sudo systemctl start iieasy-sync +``` + +--- + +## Порядок выполнения + +1. **Git** — инициализация репозитория, remote, первый push (без `.env` и секретов). +2. **Docker** — `cp .env.example .env`, правка `.env`, генерация `QDRANT_API_KEY`, `docker compose up -d`, загрузка модели Ollama. +3. Первый вход в Open WebUI (при необходимости настройка Authentik OIDC), создание API ключа в Settings → Account → API Keys. +4. **Ребрендинг** — файлы в `media/`, `sudo ./scripts/rebrand_safe_final.sh`. +5. **Поиск** — правка `searxng/settings.yml` (secret_key), при ошибке User-Agent — entrypoint с `fix_user_agent_final.sh`; при проблемах — `diagnose_search.sh`, `fix_search_complete.sh`. +6. **Nextcloud Python** — настройка `worker/.env`, `pip install -r requirements.txt`, проверка `--once`, затем `--daemon` или systemd. + +--- + +## Замечания + +- **Токен/пароль Gitea:** в документации не указывайте токен в открытом виде; используйте «подставить свой токен из Gitea» или SSH. +- **Имя репозитория:** на сервере используется `iiEsaywebUI` (с одной «a») — в URL remote указывайте так же. +- **Порт SSH:** если Gitea слушает SSH на порту не 22 (например 3000), укажите в `~/.ssh/config` для хоста `Port 3000`; URL остаётся `git@192.168.88.165:ars/iiEsaywebUI.git`. diff --git a/docs/TTS_CMU_ARCTIC_SPEAKERS.md b/docs/TTS_CMU_ARCTIC_SPEAKERS.md new file mode 100644 index 0000000..41e07f4 --- /dev/null +++ b/docs/TTS_CMU_ARCTIC_SPEAKERS.md @@ -0,0 +1,51 @@ +# TTS в Open WebUI: английские и русские голоса + +## Transformers (Локально) — только английский + +В настройках **Настройки → Речь** при выборе системы синтеза речи **«Transformers (Локально)»** поле **«Модель TTS»** не является выпадающим списком — в него нужно **вручную ввести** имя спикера из набора CMU ARCTIC. Эти голоса **только для английского языка**, русского нет. + +### Доступные имена (вводить латиницей) + +| Имя | Описание | +|------|------------------------| +| `bdl` | Мужской голос (США) | +| `slt` | Женский голос (США) | +| `clb` | Женский голос (США) | +| `rms` | Мужской голос (США) | +| `awb` | Мужской голос (шотландский) | +| `jmk` | Мужской голос (канадский) | +| `ksp` | Мужской голос (индийский) | + +Источник: [Matthijs/cmu-arctic-xvectors](https://huggingface.co/datasets/Matthijs/cmu-arctic-xvectors) + +Рекомендация: начните с **`bdl`** или **`slt`**. При первом использовании TTS Open WebUI может скачать модели с Hugging Face. + +--- + +## Русский TTS: Edge TTS (рекомендуется) + +Для **русской** озвучки нужен другой движок. Удобный бесплатный вариант — **Edge TTS** (голоса Microsoft). В проекте он уже добавлен в `docker-compose.yml` как сервис `openai-edge-tts`. + +### Что сделать в интерфейсе + +1. **Настройки → Речь (Audio)** +2. **Система синтеза речи:** выберите **OpenAI** (или пункт, где задаётся URL API). +3. Укажите: + - **TTS API URL:** `http://openai-edge-tts:5050/v1` (если Open WebUI в том же Docker Compose). + - **API ключ:** `your_api_key_here` (дефолтный ключ Edge TTS). +4. **Голос TTS** — введите один из русских голосов: + - **`ru-RU-SvetlanaNeural`** — женский + - **`ru-RU-DmitryNeural`** — мужской +5. Сохраните настройки. + +Все голоса Edge TTS (включая другие языки) можно послушать и выбрать: [tts.travisvn.com](https://tts.travisvn.com/). + +### Если Edge TTS ещё не запущен + +Из каталога проекта: + +```bash +docker compose up -d openai-edge-tts +``` + +После этого снова откройте настройки речи и выберите голос `ru-RU-SvetlanaNeural` или `ru-RU-DmitryNeural`. diff --git a/instr b/instr new file mode 100644 index 0000000..1786628 --- /dev/null +++ b/instr @@ -0,0 +1,24 @@ +# === ДОМЕНЫ И СЕТЬ === +DOMAIN_OPENWEBUI=https://odo.iieasy.ru +DOMAIN_NEXTCLOUD=https://cloud.iieasy.ru +DOMAIN_AUTHENTIK=https://auth.iieasy.ru + +# === AUTHENTIK (OIDC SSO) === +# Где брать: Authentik -> Providers -> твой OIDC Provider +OAUTH_CLIENT_ID=твой_client_id_из_authentik +OAUTH_CLIENT_SECRET=твой_client_secret_из_authentik +OPENID_CONNECT_ISSUER=https://auth.iieasy.ru/application/o/open-webui/ + +# === NEXTCLOUD (Для Python-воркера) === +# Где брать: Nextcloud -> Настройки -> Безопасность -> Устройства и сессии (Создать пароль приложения) +NC_USER=твой_логин +NC_APP_PASSWORD=твой_пароль_приложения_nextcloud # Строго пароль приложения, не основной! + +# === OPEN WEBUI API (Для пуша файлов в Qdrant) === +# Где брать: Open WebUI -> Settings -> Account -> API Keys (создашь после первого запуска) +OPENWEBUI_API_KEY=твой_api_ключ_от_openwebui + +# === VAULTWARDEN (Для интеграции Bitwarden CLI в будущем) === +# Где брать: Vaultwarden -> Настройки аккаунта -> Безопасность -> Ключи -> API-ключ +BW_CLIENTID=user.твой_id +BW_CLIENTSECRET=твой_bw_secret \ No newline at end of file diff --git a/media/favicon.png b/media/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..14caa7df362d0f2b798ae1d79bc466853b755554 GIT binary patch literal 49855 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 tH#5uYmJ*&kkACJts;A6`lv~7BL})U*5eL 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 + + + + + diff --git a/media/init-logos.sh b/media/init-logos.sh new file mode 100755 index 0000000..5ca3553 --- /dev/null +++ b/media/init-logos.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Скрипт для автоматической замены логотипов при запуске контейнера +# Этот скрипт можно запускать при каждом старте контейнера + +MEDIA_DIR="/app/media" +MAX_RETRIES=10 +RETRY_DELAY=2 + +# Ждем пока контейнер полностью запустится +for i in $(seq 1 $MAX_RETRIES); do + if curl -f http://localhost:8080/health >/dev/null 2>&1; then + break + fi + sleep $RETRY_DELAY +done + +# Находим все favicon и logo файлы +find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "logo.png" -o -name "logo.svg" \) 2>/dev/null | while read file; do + dir=$(dirname "$file") + name=$(basename "$file") + + # Заменяем favicon + if [[ "$name" == favicon* ]] && [ -f "$MEDIA_DIR/favicon.png" ]; then + cp "$MEDIA_DIR/favicon.png" "$file" 2>/dev/null || true + # Также создаем .ico + cp "$MEDIA_DIR/favicon.png" "$dir/favicon.ico" 2>/dev/null || true + fi + + # Заменяем logo + if [[ "$name" == logo* ]] && [ -f "$MEDIA_DIR/logo.png" ]; then + cp "$MEDIA_DIR/logo.png" "$file" 2>/dev/null || true + # Также создаем .svg + cp "$MEDIA_DIR/logo.png" "$dir/logo.svg" 2>/dev/null || true + fi +done diff --git a/media/logo-dark.svg b/media/logo-dark.svg new file mode 100644 index 0000000..bfe5ed6 --- /dev/null +++ b/media/logo-dark.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/media/logo-light.svg b/media/logo-light.svg new file mode 100644 index 0000000..c99cb26 --- /dev/null +++ b/media/logo-light.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/media/logo.png b/media/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..14caa7df362d0f2b798ae1d79bc466853b755554 GIT binary patch literal 49855 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 tH#5uYmJ*&kkACJts;A6`lv~7BL})U*5eL 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 + + + + + iiEasy Logo - Right Aligned with Styled Slogan + + + + +
+ + + + + + + + + + + +
+
iiEasy
+
Будущее. Просто.
+
+
+ + +``` \ No newline at end of file diff --git a/media/Логотип светлый svg.md b/media/Логотип светлый svg.md new file mode 100644 index 0000000..09b022d --- /dev/null +++ b/media/Логотип светлый svg.md @@ -0,0 +1,32 @@ +```html + + + + + + AI Logo - Variant 40 (Kinetic Rings - Compact) + + + + + + + + + + + + + + + + +
iiEasy
+ +
Будущее. Просто.
+ + +``` \ No newline at end of file diff --git a/media/Логотип светлый справа svg.md b/media/Логотип светлый справа svg.md new file mode 100644 index 0000000..ca046e7 --- /dev/null +++ b/media/Логотип светлый справа svg.md @@ -0,0 +1,32 @@ +```html + + + + + + AI Logo - Variant 40 (Kinetic Rings - Compact) - Light, Right Aligned Text + + + + + + + + + + + + + + + + + + +
iiEasy
+ + +``` \ No newline at end of file diff --git a/media/Логотип тёмный svg.md b/media/Логотип тёмный svg.md new file mode 100644 index 0000000..75949b9 --- /dev/null +++ b/media/Логотип тёмный svg.md @@ -0,0 +1,32 @@ +```html + + + + + + AI Logo - Variant 40 (Kinetic Rings - Compact) - Dark + + + + + + + + + + + + + + + + +
iiEasy
+ +
Будущее. Просто.
+ + +``` \ No newline at end of file diff --git a/media/Логотип тёмный справа avg.md b/media/Логотип тёмный справа avg.md new file mode 100644 index 0000000..50a3a27 --- /dev/null +++ b/media/Логотип тёмный справа avg.md @@ -0,0 +1,32 @@ +```html + + + + + + AI Logo - Variant 40 (Kinetic Rings - Compact) - Dark, Right Aligned Text + + + + + + + + + + + + + + + + + + +
iiEasy
+ + +``` \ No newline at end of file diff --git a/media/лого темный справа слоган.md b/media/лого темный справа слоган.md new file mode 100644 index 0000000..0406582 --- /dev/null +++ b/media/лого темный справа слоган.md @@ -0,0 +1,42 @@ +```html + + + + + + iiEasy Logo - Right Aligned Dark with Styled Slogan + + + + +
+ + + + + + + + + + + +
+
iiEasy
+ +
Будущее. Просто.
+
+
+ + +``` \ No newline at end of file diff --git a/media/логотип анимация .md b/media/логотип анимация .md new file mode 100644 index 0000000..907f140 --- /dev/null +++ b/media/логотип анимация .md @@ -0,0 +1,75 @@ +```html + + + + + + Анимированный логотип - iiEasy (Горизонтальная темная версия) + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

iiEasy

+ +
+ + + +``` \ No newline at end of file diff --git a/media/логотип анимация2.md b/media/логотип анимация2.md new file mode 100644 index 0000000..fd0499c --- /dev/null +++ b/media/логотип анимация2.md @@ -0,0 +1,78 @@ +```html + + + + + + Анимированный логотип - iiEasy (Темная версия) + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + +

iiEasy

+ + +

Будущее. Просто.

+ +
+ + + +``` \ No newline at end of file diff --git a/nginx-proxy-manager-config.md b/nginx-proxy-manager-config.md new file mode 100644 index 0000000..1e605f4 --- /dev/null +++ b/nginx-proxy-manager-config.md @@ -0,0 +1,143 @@ +# Конфигурация Nginx Proxy Manager для odo.iieasy.ru + +## Настройка прокси для Open WebUI + +### Шаг 1: Создание Proxy Host в Nginx Proxy Manager + +1. Войдите в Nginx Proxy Manager (обычно `http://your-server-ip:81`) +2. Перейдите в **Proxy Hosts** → **Add Proxy Host** + +### Шаг 2: Настройка Details + +- **Domain Names**: `odo.iieasy.ru` +- **Scheme**: `http` +- **Forward Hostname/IP**: `open-webui` (имя контейнера Docker) или `localhost` +- **Forward Port**: `3001` (изменено с 3000) +- **Cache Assets**: Включено (опционально) +- **Block Common Exploits**: Включено +- **Websockets Support**: **ВКЛЮЧЕНО** (важно для Open WebUI!) + +### Шаг 3: Настройка SSL + +**ВАЖНО:** SSL сертификат должен быть настроен ДО того, как вы сможете открыть сайт по HTTPS. + +#### Вариант A: Использование Let's Encrypt (рекомендуется) + +1. В Nginx Proxy Manager перейдите в **SSL Certificates** → **Add SSL Certificate** +2. Выберите **Let's Encrypt** +3. Заполните: + - **Domain Names**: `odo.iieasy.ru` (можно добавить несколько через запятую) + - **Email Address**: Ваш email (для уведомлений об истечении сертификата) + - **Agree to Let's Encrypt Terms**: Включено + - **Use a DNS Challenge**: Обычно НЕ нужно (используется HTTP challenge) +4. Нажмите **Save** +5. Дождитесь создания сертификата (может занять 1-2 минуты) + +**Требования для Let's Encrypt:** +- Домен `odo.iieasy.ru` должен указывать на IP вашего сервера (A-запись в DNS) +- Порт 80 должен быть доступен из интернета (для HTTP challenge) +- Nginx Proxy Manager должен быть доступен на порту 80 + +#### Вариант B: Использование существующего сертификата + +Если у вас уже есть SSL сертификат: +1. **SSL Certificates** → **Add SSL Certificate** → **Custom** +2. Вставьте содержимое файлов: + - **Certificate**: содержимое `.crt` или `.pem` файла + - **Private Key**: содержимое `.key` файла +3. Нажмите **Save** + +#### Вариант C: Временное отключение SSL (только для тестирования) + +Если нужно быстро проверить работу без SSL: +1. В Proxy Host настройках убедитесь, что SSL не включен +2. Используйте HTTP вместо HTTPS: `http://odo.iieasy.ru` + +### Шаг 4: Применение SSL к Proxy Host + +После создания SSL сертификата: + +1. Вернитесь к вашему Proxy Host для `odo.iieasy.ru` +2. Перейдите на вкладку **SSL** +3. Выберите созданный сертификат в поле **SSL Certificate** +4. Включите: + - **Force SSL**: Включено (перенаправляет HTTP на HTTPS) + - **HTTP/2 Support**: Включено + - **HSTS Enabled**: Включено (опционально) + - **HSTS Subdomains**: Включено (опционально, если используете поддомены) +5. Нажмите **Save** + +### Шаг 5: Настройка Advanced (Custom Nginx Configuration) + +**ВАЖНО:** Nginx Proxy Manager автоматически управляет большинством настроек. Custom Configuration обычно НЕ нужна и может вызвать ошибки 500. + +**Оставьте поле Custom Nginx Configuration ПУСТЫМ**, если все работает без него. + +Если все же нужны дополнительные настройки (только при проблемах), добавьте минимальную конфигурацию: + +```nginx +# Увеличение размера тела запроса для загрузки файлов (если нужно) +client_max_body_size 100M; + +# Увеличенные таймауты (только если есть проблемы с таймаутами) +proxy_read_timeout 300s; +``` + +**Но лучше оставить пустым** - Nginx Proxy Manager сам настроит WebSocket, заголовки и другие параметры через интерфейс. + +### Шаг 6: Если используется Docker сеть + +Если Nginx Proxy Manager работает в той же Docker сети, что и Open WebUI: + +1. В **Forward Hostname/IP** укажите: `open-webui` (имя контейнера) +2. В **Forward Port** укажите: `8080` (внутренний порт контейнера) + +Если Nginx Proxy Manager работает на хосте: + +1. В **Forward Hostname/IP** укажите: `localhost` или `127.0.0.1` +2. В **Forward Port** укажите: `3001` (порт на хосте) + +## Проверка работы + +После настройки проверьте: + +1. Откройте `https://odo.iieasy.ru` в браузере +2. Проверьте логи Nginx Proxy Manager при ошибках +3. Проверьте логи Open WebUI: `docker compose logs open-webui` + +## Устранение проблем + +### ERR_SSL_UNRECOGNIZED_NAME_ALERT + +Эта ошибка означает, что SSL сертификат не настроен или не соответствует домену. + +**Решение:** +1. Проверьте, что SSL сертификат создан в Nginx Proxy Manager +2. Убедитесь, что сертификат применен к Proxy Host (вкладка SSL) +3. Проверьте, что домен `odo.iieasy.ru` указан в сертификате +4. Если используете Let's Encrypt, проверьте: + - DNS A-запись для `odo.iieasy.ru` указывает на IP сервера + - Порт 80 доступен из интернета + - Домен не заблокирован файрволом + +**Временное решение для тестирования:** +- Отключите SSL в Proxy Host +- Используйте HTTP: `http://odo.iieasy.ru` +- После настройки SSL включите обратно + +### Ошибка 502 Bad Gateway + +- Убедитесь, что контейнер `open-webui` запущен: `docker ps | grep open-webui` +- Проверьте, что порт 3001 доступен: `curl http://localhost:3001` +- Проверьте логи: `docker compose logs open-webui` + +### WebSocket не работает + +- Убедитесь, что **Websockets Support** включен в Nginx Proxy Manager +- **НЕ добавляйте** WebSocket настройки в Custom Configuration - они конфликтуют с автоматическими настройками NPM + +### Медленная загрузка + +- Проверьте логи: `docker compose logs open-webui` +- Убедитесь, что контейнер Open WebUI работает нормально +- Если нужны большие файлы, добавьте только `client_max_body_size 100M;` в Custom Configuration (но лучше оставить пустым) diff --git a/scripts/apply_logos_persistent.sh b/scripts/apply_logos_persistent.sh new file mode 100755 index 0000000..ca957dd --- /dev/null +++ b/scripts/apply_logos_persistent.sh @@ -0,0 +1,79 @@ +#!/bin/bash +# Скрипт для постоянной замены логотипов через volume монтирование +# Этот скрипт копирует файлы в volume, который сохраняется между перезапусками + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== Применение логотипов (постоянное решение) ===" + +# Проверка наличия контейнера +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен." + exit 1 +fi + +echo "1. Поиск существующих favicon и logo файлов..." + +# Находим все существующие favicon и logo файлы +EXISTING_FAVICONS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "favicon.svg" -o -name "favicon-96x96.png" \) 2>/dev/null | head -20) +EXISTING_LOGOS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "logo.png" -o -name "logo.svg" \) 2>/dev/null | head -20) + +echo " Найдено favicon файлов: $(echo "$EXISTING_FAVICONS" | grep -v '^$' | wc -l)" +echo " Найдено logo файлов: $(echo "$EXISTING_LOGOS" | grep -v '^$' | wc -l)" + +# Заменяем все найденные favicon файлы +if [ -f "$MEDIA_DIR/favicon.png" ]; then + echo "2. Замена всех favicon файлов на favicon.png..." + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ]; then + echo " → $favicon_file" + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + # Также создаем .ico версию в той же директории + favicon_dir=$(dirname "$favicon_file") + favicon_name=$(basename "$favicon_file" | sed 's/\.[^.]*$//') + if [ "$favicon_name" != "favicon" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_dir}/favicon.ico" 2>/dev/null || true + fi + fi + done +fi + +# Заменяем все найденные logo файлы +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "3. Замена всех logo файлов на logo.png..." + echo "$EXISTING_LOGOS" | while read -r logo_file; do + if [ -n "$logo_file" ]; then + echo " → $logo_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + # Также создаем .svg версию + logo_dir=$(dirname "$logo_file") + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_dir}/logo.svg" 2>/dev/null || true + fi + done +fi + +echo "" +echo "4. Перезапуск контейнера для применения изменений..." +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Не удалось перезапустить контейнер автоматически." + echo "Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== Готово! ===" +echo "" +echo "ВАЖНО: Логотипы применены, но они могут вернуться после перезапуска контейнера." +echo "" +echo "Для ПОСТОЯННОГО решения используйте Admin Panel Open WebUI:" +echo " 1. Откройте https://odo.iieasy.ru или http://localhost:3001" +echo " 2. Войдите как администратор" +echo " 3. Settings → Appearance → Logo" +echo " 4. Загрузите logo.png и favicon.png из папки media/" +echo " 5. Сохраните - настройки сохранятся в базе данных" +echo "" +echo "Или запустите этот скрипт после каждого перезапуска контейнера." diff --git a/scripts/check_gpu.sh b/scripts/check_gpu.sh new file mode 100755 index 0000000..9ac32b6 --- /dev/null +++ b/scripts/check_gpu.sh @@ -0,0 +1,100 @@ +#!/bin/bash +# Скрипт для проверки подключения GPU в Docker контейнере Ollama + +set -e + +CONTAINER_NAME="ollama" + +echo "=== Проверка GPU в Docker контейнере ===" +echo "" + +# Определяем команду docker +DOCKER_CMD="docker" +if ! docker ps >/dev/null 2>&1; then + DOCKER_CMD="sudo docker" +fi + +echo "1. Проверка конфигурации в docker-compose.yml..." +if grep -q "driver: nvidia" docker-compose.yml 2>/dev/null; then + echo " ✓ GPU настроен в docker-compose.yml" + echo " Конфигурация:" + grep -A 5 "driver: nvidia" docker-compose.yml | head -6 +else + echo " ✗ GPU не настроен в docker-compose.yml" +fi + +echo "" +echo "2. Проверка конфигурации контейнера..." +GPU_CONFIG=$($DOCKER_CMD inspect $CONTAINER_NAME 2>/dev/null | grep -i "nvidia\|gpu" | head -5) +if [ -n "$GPU_CONFIG" ]; then + echo " ✓ Найдена конфигурация GPU:" + echo "$GPU_CONFIG" +else + echo " ⚠ Конфигурация GPU не найдена в inspect" +fi + +echo "" +echo "3. Проверка доступности nvidia-smi в контейнере..." +if $DOCKER_CMD exec $CONTAINER_NAME which nvidia-smi >/dev/null 2>&1; then + echo " ✓ nvidia-smi доступен" + echo "" + echo " Вывод nvidia-smi:" + $DOCKER_CMD exec $CONTAINER_NAME nvidia-smi 2>&1 | head -15 || echo " ⚠ nvidia-smi не может выполниться" +else + echo " ✗ nvidia-smi не найден в контейнере" + echo " Возможно, контейнер не имеет доступа к GPU" +fi + +echo "" +echo "4. Проверка переменных окружения NVIDIA..." +NVIDIA_ENV=$($DOCKER_CMD exec $CONTAINER_NAME env | grep -i nvidia) +if [ -n "$NVIDIA_ENV" ]; then + echo " ✓ Переменные NVIDIA найдены:" + echo "$NVIDIA_ENV" +else + echo " ⚠ Переменные NVIDIA не найдены" +fi + +echo "" +echo "5. Проверка логов Ollama на использование GPU..." +RECENT_LOGS=$($DOCKER_CMD logs $CONTAINER_NAME --tail 50 2>&1) +if echo "$RECENT_LOGS" | grep -qi "gpu\|cuda\|nvidia"; then + echo " ✓ Найдены упоминания GPU в логах:" + echo "$RECENT_LOGS" | grep -i "gpu\|cuda\|nvidia" | tail -5 +else + echo " ⚠ Нет упоминаний GPU в последних логах" +fi + +echo "" +echo "6. Проверка на хосте..." +if command -v nvidia-smi >/dev/null 2>&1; then + echo " GPU на хосте:" + nvidia-smi --query-gpu=name,driver_version,memory.total --format=csv,noheader 2>/dev/null || echo " ⚠ Не удалось получить информацию о GPU" +else + echo " ⚠ nvidia-smi не найден на хосте" +fi + +echo "" +echo "=== Резюме ===" +echo "" +if $DOCKER_CMD exec $CONTAINER_NAME nvidia-smi >/dev/null 2>&1; then + echo "✓ GPU подключен и доступен в контейнере" + echo "" + echo "Для детальной информации выполните:" + echo " sudo docker exec ollama nvidia-smi" +else + echo "✗ GPU не доступен в контейнере" + echo "" + echo "Возможные причины:" + echo " 1. Docker не имеет доступа к GPU (нужен nvidia-docker2 или nvidia-container-toolkit)" + echo " 2. Контейнер не был перезапущен после изменения docker-compose.yml" + echo " 3. Драйверы NVIDIA не установлены на хосте" + echo "" + echo "Решение:" + echo " 1. Установите nvidia-container-toolkit:" + echo " sudo apt-get install -y nvidia-container-toolkit" + echo " sudo systemctl restart docker" + echo "" + echo " 2. Перезапустите контейнер:" + echo " docker compose restart ollama" +fi diff --git a/scripts/check_image_transfer.sh b/scripts/check_image_transfer.sh new file mode 100755 index 0000000..0bc77eb --- /dev/null +++ b/scripts/check_image_transfer.sh @@ -0,0 +1,85 @@ +#!/bin/bash +# Скрипт для проверки передачи изображений из Open WebUI в Ollama + +set -e + +echo "=== Проверка передачи изображений Open WebUI → Ollama ===" +echo "" + +# Определяем команду docker +DOCKER_CMD="docker" +if ! docker ps >/dev/null 2>&1; then + DOCKER_CMD="sudo docker" +fi + +echo "1. Проверка переменной OLLAMA_BASE_URL в контейнере..." +OLLAMA_URL=$($DOCKER_CMD exec open-webui env | grep -i "OLLAMA_BASE_URL" | cut -d= -f2) +if [ -z "$OLLAMA_URL" ]; then + echo " ✗ Переменная OLLAMA_BASE_URL не установлена!" + echo " Нужно добавить в docker-compose.yml и перезапустить контейнер" +else + echo " ✓ OLLAMA_BASE_URL=$OLLAMA_URL" +fi + +echo "" +echo "2. Проверка доступности Ollama из Open WebUI..." +if $DOCKER_CMD exec open-webui curl -s --max-time 5 http://ollama:11434/api/tags >/dev/null 2>&1; then + echo " ✓ Ollama доступен по адресу http://ollama:11434" +else + echo " ✗ Ollama недоступен из Open WebUI" +fi + +echo "" +echo "3. Проверка модели gemma3n:e4b-it-fp16..." +MODELS=$($DOCKER_CMD exec open-webui curl -s http://ollama:11434/api/tags 2>/dev/null) +if echo "$MODELS" | grep -q "gemma3n:e4b-it-fp16"; then + echo " ✓ Модель найдена" +else + echo " ✗ Модель не найдена" +fi + +echo "" +echo "4. Проверка логов Open WebUI на наличие запросов с изображениями..." +RECENT_LOGS=$($DOCKER_CMD logs open-webui --tail 100 2>&1) +if echo "$RECENT_LOGS" | grep -qi "image.*ollama\|ollama.*image\|vision\|multimodal"; then + echo " ✓ Найдены упоминания изображений в логах:" + echo "$RECENT_LOGS" | grep -i "image.*ollama\|ollama.*image\|vision\|multimodal" | tail -5 +else + echo " ⚠ Нет упоминаний изображений в последних логах" +fi + +echo "" +echo "5. Проверка логов Ollama на наличие запросов с изображениями..." +OLLAMA_LOGS=$($DOCKER_CMD logs ollama --tail 100 2>&1) +if echo "$OLLAMA_LOGS" | grep -qi "image\|vision\|multimodal"; then + echo " ✓ Найдены запросы с изображениями в логах Ollama:" + echo "$OLLAMA_LOGS" | grep -i "image\|vision\|multimodal" | tail -5 +else + echo " ✗ Нет запросов с изображениями в логах Ollama" + echo " Это означает, что изображения не доходят до Ollama!" +fi + +echo "" +echo "6. Рекомендации:" +echo "" +if [ -z "$OLLAMA_URL" ]; then + echo " ⚠ КРИТИЧНО: Добавьте OLLAMA_BASE_URL в docker-compose.yml:" + echo " - OLLAMA_BASE_URL=http://ollama:11434" + echo " Затем: docker compose restart open-webui" + echo "" +fi + +echo " Для диагностики проблемы с изображениями:" +echo " 1. Убедитесь, что в Settings → Connections → Ollama API" +echo " адрес установлен: http://ollama:11434" +echo "" +echo " 2. Попробуйте отправить изображение через веб-интерфейс" +echo " и сразу проверьте логи:" +echo " sudo docker logs open-webui --tail 50 -f" +echo " sudo docker logs ollama --tail 50 -f" +echo "" +echo " 3. Проверьте формат изображения (должен быть JPEG/PNG)" +echo " и размер (не слишком большой)" + +echo "" +echo "=== Проверка завершена ===" diff --git a/scripts/check_searxng_json.sh b/scripts/check_searxng_json.sh new file mode 100755 index 0000000..212eef2 --- /dev/null +++ b/scripts/check_searxng_json.sh @@ -0,0 +1,43 @@ +#!/bin/sh +# Скрипт для проверки формата JSON ответа от SearXNG + +echo "Проверка формата JSON ответа от SearXNG..." + +# Проверяем формат ответа от SearXNG +RESPONSE=$(curl -s "http://searxng:8080/search?q=test&format=json" 2>&1) + +if [ $? -eq 0 ]; then + echo "✓ SearXNG отвечает" + echo "Формат ответа (первые 500 символов):" + echo "$RESPONSE" | head -c 500 + echo "" + echo "" + + # Проверяем наличие ключевых полей + if echo "$RESPONSE" | grep -q '"results"'; then + echo "✓ Найдено поле 'results'" + fi + + if echo "$RESPONSE" | grep -q '"url"'; then + echo "✓ Найдено поле 'url'" + fi + + if echo "$RESPONSE" | grep -q '"link"'; then + echo "✓ Найдено поле 'link'" + fi + + if echo "$RESPONSE" | grep -q '"title"'; then + echo "✓ Найдено поле 'title'" + fi + + if echo "$RESPONSE" | grep -q '"content"'; then + echo "✓ Найдено поле 'content'" + fi + + if echo "$RESPONSE" | grep -q '"snippet"'; then + echo "✓ Найдено поле 'snippet'" + fi +else + echo "✗ Ошибка при запросе к SearXNG" + echo "$RESPONSE" +fi diff --git a/scripts/check_vision_models.sh b/scripts/check_vision_models.sh new file mode 100755 index 0000000..6d1d6bc --- /dev/null +++ b/scripts/check_vision_models.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Проверка доступных vision моделей в Ollama + +set -e + +CONTAINER_OLLAMA="ollama" + +echo "=== Проверка vision моделей в Ollama ===" +echo "" + +# Определяем команду docker +DOCKER_CMD="docker" +if ! docker ps >/dev/null 2>&1; then + DOCKER_CMD="sudo docker" +fi + +echo "1. Список установленных моделей:" +$DOCKER_CMD exec $CONTAINER_OLLAMA ollama list 2>/dev/null || sudo $DOCKER_CMD exec $CONTAINER_OLLAMA ollama list 2>/dev/null + +echo "" +echo "2. Популярные vision модели для Ollama:" +echo "" +echo " Рекомендуемые модели с поддержкой vision:" +echo " - llava:latest (LLaVA 1.6) - 7B параметров, хорошо работает с изображениями" +echo " - bakllava:latest (BakLLaVA) - 7B параметров, Mistral + LLaVA" +echo " - llama3.2-vision:latest (Llama 3.2 Vision) - 11B параметров" +echo " - gemma2:9b-it (Gemma 2) - может поддерживать vision" +echo "" +echo "3. Для установки vision модели выполните:" +echo "" +echo " sudo docker exec ollama ollama pull llava:latest" +echo " # или" +echo " sudo docker exec ollama ollama pull bakllava:latest" +echo "" +echo "4. Проверка текущей модели gemma3n:e4b-it-fp16:" +echo "" +echo " Модель gemma3n:e4b-it-fp16 может не поддерживать vision правильно." +echo " Рекомендуется использовать специализированные vision модели:" +echo " - llava:latest (лучший выбор для vision)" +echo " - bakllava:latest (альтернатива)" +echo "" +echo "=== Готово ===" diff --git a/scripts/diagnose_search.sh b/scripts/diagnose_search.sh new file mode 100755 index 0000000..52a3b06 --- /dev/null +++ b/scripts/diagnose_search.sh @@ -0,0 +1,90 @@ +#!/bin/sh +# Диагностика проблемы с поиском в Open WebUI + +echo "=== ДИАГНОСТИКА ПОИСКА В OPEN WEBUI ===" +echo "" + +# 1. Проверка SearXNG +echo "1. Проверка SearXNG..." +SEARXNG_STATUS=$(docker ps | grep searxng | awk '{print $7}') +if [ "$SEARXNG_STATUS" = "healthy" ] || [ "$SEARXNG_STATUS" = "Up" ]; then + echo "✓ SearXNG работает (статус: $SEARXNG_STATUS)" +else + echo "✗ SearXNG не работает (статус: $SEARXNG_STATUS)" +fi + +# 2. Проверка JSON формата +echo "" +echo "2. Проверка JSON формата SearXNG..." +JSON_TEST=$(docker exec open-webui curl -s "http://searxng:8080/search?q=test&format=json" 2>&1 | head -c 200) +if echo "$JSON_TEST" | grep -q "results"; then + echo "✓ JSON формат работает" +else + echo "✗ JSON формат не работает" + echo " Ответ: $JSON_TEST" +fi + +# 3. Проверка патча User-Agent +echo "" +echo "3. Проверка патча User-Agent..." +RAG_BOT_FOUND=$(docker exec open-webui grep -r "RAG Bot" /app/backend/open_webui/routers/retrieval.py /app/backend/open_webui/utils/middleware.py 2>/dev/null | wc -l) +if [ "$RAG_BOT_FOUND" -eq 0 ]; then + echo "✓ Патч User-Agent применен (проблемная строка не найдена)" +else + echo "✗ Патч User-Agent НЕ применен (найдено вхождений: $RAG_BOT_FOUND)" + echo " Нужно перезапустить Open WebUI для применения патча" +fi + +# 4. Проверка логов Open WebUI на ошибки +echo "" +echo "4. Последние ошибки в логах Open WebUI..." +docker logs open-webui --tail 50 2>&1 | grep -i "error\|user-agent\|invalid\|searxng" | tail -10 +if [ $? -ne 0 ]; then + echo " (Ошибок не найдено в последних 50 строках)" +fi + +# 5. Проверка конфигурации docker-compose +echo "" +echo "5. Проверка конфигурации..." +SEARXNG_URL=$(grep "SEARXNG_QUERY_URL" docker-compose.yml | head -1) +echo " SEARXNG_QUERY_URL: $SEARXNG_URL" +if echo "$SEARXNG_URL" | grep -q "format=json"; then + echo "✓ URL содержит format=json" +else + echo "⚠ URL не содержит format=json явно" +fi + +# 6. Проверка settings.yml +echo "" +echo "6. Проверка settings.yml SearXNG..." +if grep -q "formats:" searxng/settings.yml && grep -q "json" searxng/settings.yml; then + echo "✓ JSON формат включен в settings.yml" +else + echo "✗ JSON формат НЕ найден в settings.yml" +fi + +if grep -q "limiter: false" searxng/settings.yml; then + echo "✓ Лимитер отключен" +else + echo "⚠ Лимитер может быть включен" +fi + +echo "" +echo "=== РЕКОМЕНДАЦИИ ===" +echo "" +if [ "$RAG_BOT_FOUND" -gt 0 ]; then + echo "1. Перезапустите Open WebUI для применения патча User-Agent:" + echo " sudo docker restart open-webui" + echo "" +fi + +if ! echo "$JSON_TEST" | grep -q "results"; then + echo "2. Исправьте конфигурацию SearXNG:" + echo " sudo ./scripts/fix_searxng_config.sh" + echo "" +fi + +echo "3. Проверьте настройки в интерфейсе Open WebUI:" +echo " Settings → Web Search" +echo " URL должен быть: http://searxng:8080/search?q=&format=json" +echo "" diff --git a/scripts/find_model_image_api.sh b/scripts/find_model_image_api.sh new file mode 100755 index 0000000..72aa533 --- /dev/null +++ b/scripts/find_model_image_api.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Поиск API endpoint для изображения профиля модели + +CONTAINER_NAME="open-webui" + +echo "=== Поиск API endpoint /api/v1/models/model/profile/image ===" +echo "" + +echo "1. Поиск Python файлов с этим endpoint..." +docker exec "${CONTAINER_NAME}" find /app/backend -type f -name "*.py" \ + -exec grep -l "model/profile/image\|profile/image\|models.*profile" {} \; 2>/dev/null + +echo "" +echo "2. Поиск в main.py и routes..." +docker exec "${CONTAINER_NAME}" find /app/backend -type f -name "*.py" \ + \( -name "*main.py" -o -name "*route*.py" -o -name "*api*.py" -o -name "*model*.py" \) \ + -exec grep -l "profile.*image\|image.*profile" {} \; 2>/dev/null + +echo "" +echo "3. Поиск строк с '/api/v1/models'..." +docker exec "${CONTAINER_NAME}" find /app/backend -type f -name "*.py" \ + -exec grep -l "/api/v1/models\|api/v1/models" {} \; 2>/dev/null | head -10 + +echo "" +echo "4. Поиск в шаблонах HTML/Svelte с этим API..." +docker exec "${CONTAINER_NAME}" find /app/web -type f \ + \( -name "*.html" -o -name "*.svelte" -o -name "*.js" -o -name "*.ts" \) \ + ! -path "*/node_modules/*" \ + -exec grep -l "model/profile/image\|models.*profile.*image" {} \; 2>/dev/null | head -10 + +echo "" +echo "=== Поиск завершен ===" diff --git a/scripts/find_openwebui_text.sh b/scripts/find_openwebui_text.sh new file mode 100755 index 0000000..a5efb56 --- /dev/null +++ b/scripts/find_openwebui_text.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Скрипт для поиска всех упоминаний "(Open WebUI)" в контейнере + +CONTAINER_NAME="open-webui" + +echo "=== Поиск всех упоминаний '(Open WebUI)' ===" +echo "" + +# Ищем во всех файлах +echo "1. Поиск в HTML/Svelte файлах..." +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "(Open WebUI)" "$file" 2>/dev/null; then + echo " НАЙДЕНО в: $file" + docker exec "${CONTAINER_NAME}" grep -n "(Open WebUI)" "$file" 2>/dev/null | head -3 + fi +done + +echo "" +echo "2. Поиск в JS/TS файлах..." +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \) \ + ! -path "*/node_modules/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "(Open WebUI)" "$file" 2>/dev/null; then + echo " НАЙДЕНО в: $file" + docker exec "${CONTAINER_NAME}" grep -n "(Open WebUI)" "$file" 2>/dev/null | head -3 + fi +done + +echo "" +echo "3. Поиск в скомпилированных файлах..." +docker exec "${CONTAINER_NAME}" find /app/web/build -type f -name "*.js" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "(Open WebUI)" "$file" 2>/dev/null; then + echo " НАЙДЕНО в скомпилированном: $file" + docker exec "${CONTAINER_NAME}" grep -o ".{0,50}(Open WebUI).{0,50}" "$file" 2>/dev/null | head -2 + fi +done + +echo "" +echo "4. Поиск в JSON файлах..." +docker exec "${CONTAINER_NAME}" find /app/web -type f -name "*.json" \ + ! -path "*/node_modules/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "(Open WebUI)" "$file" 2>/dev/null; then + echo " НАЙДЕНО в: $file" + docker exec "${CONTAINER_NAME}" grep -n "(Open WebUI)" "$file" 2>/dev/null | head -3 + fi +done + +echo "" +echo "5. Поиск вариантов написания..." +docker exec "${CONTAINER_NAME}" find /app/web -type f \ + ! -path "*/node_modules/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -qE "iiEasyWeb.*Open|Open.*WebUI" "$file" 2>/dev/null; then + echo " ВАРИАНТ в: $file" + docker exec "${CONTAINER_NAME}" grep -nE "iiEasyWeb.*Open|Open.*WebUI" "$file" 2>/dev/null | head -2 + fi +done + +echo "" +echo "=== Поиск завершен ===" diff --git a/scripts/find_settings_elements.sh b/scripts/find_settings_elements.sh new file mode 100755 index 0000000..635f9d5 --- /dev/null +++ b/scripts/find_settings_elements.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Поиск элементов настроек, которые нужно удалить + +CONTAINER_NAME="open-webui" + +echo "=== Поиск элементов настроек ===" +echo "" + +echo "1. Поиск 'Проверить обновления'..." +docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.svelte" -o -name "*.js" -o -name "*.ts" -o -name "*.html" \) \ + ! -path "*/node_modules/*" \ + -exec grep -l "Проверить обновления\|Check for updates\|check.*update" {} \; 2>/dev/null | head -10 + +echo "" +echo "2. Поиск 'последняя' и GitHub releases..." +docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.svelte" -o -name "*.js" -o -name "*.ts" \) \ + ! -path "*/node_modules/*" \ + -exec grep -l "последняя\|latest\|github.com/open-webui/releases" {} \; 2>/dev/null | head -10 + +echo "" +echo "3. Поиск блока 'Помощь' и соцсетей..." +docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.svelte" -o -name "*.js" -o -name "*.ts" \) \ + ! -path "*/node_modules/*" \ + -exec grep -l "Помощь\|Help\|discord\.gg\|twitter\.com\|github.com/open-webui" {} \; 2>/dev/null | head -10 + +echo "" +echo "4. Поиск блока 'Лицензия'..." +docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.svelte" -o -name "*.js" -o -name "*.ts" \) \ + ! -path "*/node_modules/*" \ + -exec grep -l "Лицензия\|License\|лицензионный тарифный план" {} \; 2>/dev/null | head -10 + +echo "" +echo "5. Поиск в скомпилированных файлах..." +docker exec "${CONTAINER_NAME}" find /app/web/build -type f -name "*.js" \ + -exec grep -l "Проверить обновления\|последняя\|Помощь\|Лицензия" {} \; 2>/dev/null | head -5 + +echo "" +echo "=== Поиск завершен ===" diff --git a/scripts/fix_openwebui.sh b/scripts/fix_openwebui.sh new file mode 100755 index 0000000..5ac51b0 --- /dev/null +++ b/scripts/fix_openwebui.sh @@ -0,0 +1,49 @@ +#!/bin/bash +# Скрипт восстановления Open WebUI после повреждения ребрендингом + +set -e + +CONTAINER_NAME="open-webui" + +echo "=== Восстановление Open WebUI ===" + +# Проверка наличия контейнера +if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не найден." + exit 1 +fi + +echo "1. Остановка контейнера..." +docker stop "${CONTAINER_NAME}" 2>/dev/null || true + +echo "2. Удаление поврежденного контейнера..." +docker rm "${CONTAINER_NAME}" 2>/dev/null || true + +echo "3. Пересоздание контейнера..." +cd "$(dirname "$0")/.." +docker compose up -d open-webui + +echo "4. Ожидание запуска контейнера..." +sleep 15 + +echo "5. Проверка статуса..." +if docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "✓ Контейнер запущен успешно" + + # Проверяем, что контейнер работает + if docker exec "${CONTAINER_NAME}" curl -f http://localhost:8080/health 2>/dev/null; then + echo "✓ Контейнер отвечает на запросы" + else + echo "⚠ Контейнер запущен, но не отвечает. Проверьте логи: docker compose logs open-webui" + fi +else + echo "✗ Контейнер не запустился. Проверьте логи: docker compose logs open-webui" + exit 1 +fi + +echo "" +echo "=== Восстановление завершено ===" +echo "" +echo "ВАЖНО: Скрипт rebrand.sh был исправлен и больше не будет ломать код." +echo "Если нужно применить ребрендинг, используйте обновленный скрипт:" +echo " ./scripts/rebrand.sh" diff --git a/scripts/fix_search_complete.sh b/scripts/fix_search_complete.sh new file mode 100755 index 0000000..54fd305 --- /dev/null +++ b/scripts/fix_search_complete.sh @@ -0,0 +1,56 @@ +#!/bin/sh +# Полное исправление поиска в Open WebUI +# Исправляет конфигурацию SearXNG и перезапускает контейнеры + +echo "=== ПОЛНОЕ ИСПРАВЛЕНИЕ ПОИСКА ===" +echo "" + +# 1. Исправляем конфигурацию SearXNG +echo "1. Исправление конфигурации SearXNG..." +./scripts/fix_searxng_config.sh + +# 2. Перезапускаем Open WebUI для применения патча User-Agent +echo "" +echo "2. Перезапуск Open WebUI для применения патча User-Agent..." +docker restart open-webui + +echo "" +echo "Ожидание запуска Open WebUI (15 секунд)..." +sleep 15 + +# 3. Проверка +echo "" +echo "3. Финальная проверка..." +echo "" + +# Проверка SearXNG +SEARXNG_STATUS=$(docker ps | grep searxng | awk '{print $7}') +if [ "$SEARXNG_STATUS" = "healthy" ] || [ "$SEARXNG_STATUS" = "Up" ]; then + echo "✓ SearXNG работает" +else + echo "✗ SearXNG не работает" +fi + +# Проверка JSON +JSON_TEST=$(docker exec open-webui curl -s "http://searxng:8080/search?q=test&format=json" 2>&1 | head -c 200) +if echo "$JSON_TEST" | grep -q "results"; then + echo "✓ JSON формат работает" +else + echo "✗ JSON формат не работает" +fi + +# Проверка патча +RAG_BOT_FOUND=$(docker exec open-webui grep -r "RAG Bot" /app/backend/open_webui/routers/retrieval.py /app/backend/open_webui/utils/middleware.py 2>/dev/null | wc -l) +if [ "$RAG_BOT_FOUND" -eq 0 ]; then + echo "✓ Патч User-Agent применен" +else + echo "⚠ Патч User-Agent может быть не применен (найдено: $RAG_BOT_FOUND)" +fi + +echo "" +echo "=== ГОТОВО ===" +echo "" +echo "Проверьте поиск в Open WebUI:" +echo "1. Откройте Settings → Web Search" +echo "2. URL должен быть: http://searxng:8080/search?q=&format=json" +echo "3. Попробуйте поиск в чате" diff --git a/scripts/fix_searxng_config.sh b/scripts/fix_searxng_config.sh new file mode 100755 index 0000000..6b194f1 --- /dev/null +++ b/scripts/fix_searxng_config.sh @@ -0,0 +1,55 @@ +#!/bin/sh +# Скрипт для исправления конфигурации SearXNG после перезапуска контейнера + +echo "Исправление конфигурации SearXNG..." + +# Исправляем файл на хосте (он монтируется в контейнер) +cat > /home/its/iiEasyWeb/searxng/settings.yml << 'EOF' +# SearXNG Settings для работы с Open WebUI +# Этот файл включает поддержку JSON формата для API запросов + +use_default_settings: true + +server: + secret_key: "CHANGE_ME_SECRET_KEY" + bind_address: "0.0.0.0" + port: 8080 + limiter: false + method: "GET" + +search: + safe_search: 0 + autocomplete: "google" + formats: + - html + - json + +general: + instance_name: "SearXNG" + debug: false +EOF + +echo "✓ Конфигурация обновлена на хосте" + +# Перезапускаем SearXNG +echo "Перезапуск SearXNG..." +docker restart searxng + +echo "" +echo "Ожидание запуска SearXNG (10 секунд)..." +sleep 10 + +# Проверяем, что JSON формат работает +echo "" +echo "Проверка JSON формата..." +RESPONSE=$(docker exec open-webui curl -s "http://searxng:8080/search?q=test&format=json" 2>&1 | head -c 200) + +if echo "$RESPONSE" | grep -q "results"; then + echo "✓ JSON формат работает! Поиск должен работать в Open WebUI." +else + echo "⚠ Предупреждение: JSON формат может быть недоступен. Проверьте логи:" + echo " docker logs searxng --tail 50" +fi + +echo "" +echo "Готово! Проверьте поиск в Open WebUI." diff --git a/scripts/fix_searxng_json.sh b/scripts/fix_searxng_json.sh new file mode 100755 index 0000000..8ccad75 --- /dev/null +++ b/scripts/fix_searxng_json.sh @@ -0,0 +1,31 @@ +#!/bin/sh +# Скрипт для исправления конфигурации SearXNG для работы с JSON форматом + +echo "Исправление конфигурации SearXNG для JSON формата..." + +# 1. Убеждаемся, что лимитер отключен +echo "1. Отключение лимитера..." +docker exec searxng sed -i 's/limiter: true/limiter: false/g' /etc/searxng/settings.yml 2>/dev/null || echo " Лимитер уже отключен или настройка не найдена" + +# 2. Обновляем секцию search с правильными форматами +echo "2. Обновление секции search..." +docker exec searxng sh -c "sed -i '/^search:/,\$d' /etc/searxng/settings.yml && cat >> /etc/searxng/settings.yml <<'EOF' +search: + safe_search: 0 + autocomplete: 'google' + formats: + - html + - json +EOF" + +# 3. Перезапускаем SearXNG +echo "3. Перезапуск SearXNG..." +docker restart searxng + +echo "" +echo "✓ Конфигурация SearXNG обновлена!" +echo " - JSON формат включен" +echo " - Лимитер отключен" +echo " - Autocomplete: google" +echo "" +echo "Подождите 10-15 секунд для полного запуска SearXNG..." diff --git a/scripts/fix_trace_error.sh b/scripts/fix_trace_error.sh new file mode 100755 index 0000000..bf5cb25 --- /dev/null +++ b/scripts/fix_trace_error.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Скрипт для исправления ошибки NameError: name 'trace' is not defined +# Пересоздает контейнер Open WebUI с чистой версией + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONTAINER_NAME="open-webui" + +echo "=== Исправление ошибки NameError: name 'trace' is not defined ===" +echo "" + +cd "$PROJECT_DIR" + +echo "1. Остановка контейнера..." +sudo docker compose stop "$CONTAINER_NAME" 2>/dev/null || true + +echo "2. Удаление поврежденного контейнера..." +sudo docker compose rm -f "$CONTAINER_NAME" 2>/dev/null || true + +echo "3. Пересоздание контейнера с чистой версией..." +sudo docker compose up -d "$CONTAINER_NAME" + +echo "4. Ожидание запуска контейнера (30 секунд)..." +sleep 30 + +echo "5. Проверка статуса..." +if sudo docker compose ps "$CONTAINER_NAME" | grep -q "Up"; then + echo "✓ Контейнер запущен успешно" +else + echo "✗ Контейнер не запустился. Проверьте логи:" + echo " sudo docker compose logs $CONTAINER_NAME" + exit 1 +fi + +echo "" +echo "6. Проверка логов на ошибки..." +ERRORS=$(sudo docker compose logs "$CONTAINER_NAME" --tail 50 2>&1 | grep -i "trace\|error" || true) +if [ -z "$ERRORS" ]; then + echo "✓ Ошибок не найдено" +else + echo "⚠ Найдены ошибки в логах:" + echo "$ERRORS" +fi + +echo "" +echo "=== Готово! ===" +echo "" +echo "Теперь проверьте:" +echo "1. Откройте https://odo.iieasy.ru" +echo "2. Должна появиться страница входа с формой и кнопкой 'iiEasy ID'" +echo "3. Если нужно применить логотипы, используйте Admin Panel:" +echo " Settings → Appearance → Logo" +echo "" +echo "Если ошибка 'trace' осталась, проверьте логи:" +echo " sudo docker compose logs $CONTAINER_NAME --tail 100" diff --git a/scripts/fix_user_agent.sh b/scripts/fix_user_agent.sh new file mode 100755 index 0000000..4d0ab86 --- /dev/null +++ b/scripts/fix_user_agent.sh @@ -0,0 +1,132 @@ +#!/bin/sh +# Исправление бага с User-Agent в Open WebUI v0.8.3 +# Проблема: User-Agent начинается с пробела, что вызывает ошибку "Invalid leading whitespace" + +echo "Применение патча для исправления User-Agent..." + +# Исправляем в retrieval.py +FILE="/app/backend/open_webui/routers/retrieval.py" +if [ -f "$FILE" ]; then + echo "Обработка файла: $FILE" + # Ищем все варианты проблемной строки (более агрессивный поиск) + PROBLEM_FOUND=0 + + # Проверяем наличие проблемной строки в разных вариантах + if grep -qE "(github\.com/open-webui|RAG Bot|https://github)" "$FILE" 2>/dev/null; then + PROBLEM_FOUND=1 + fi + + if [ "$PROBLEM_FOUND" -eq 1 ] || grep -q "User-Agent" "$FILE" 2>/dev/null; then + echo " Найдены строки с User-Agent, применяем патч..." + + # Более агрессивная замена - ищем любые варианты с пробелом в начале + # Исправляем варианты с одинарными кавычками + sed -i "s/' (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$FILE" + sed -i "s/ '(https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$FILE" + sed -i "s/'\(https:\/\/github\.com\/open-webui\/open-webui\) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$FILE" + + # Исправляем варианты с двойными кавычками + sed -i 's/" (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$FILE" + sed -i 's/ "(https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$FILE" + sed -i 's/"\(https:\/\/github\.com\/open-webui\/open-webui\) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$FILE" + + # Исправляем варианты без кавычек + sed -i "s/ (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot/Open-WebUI-RAG-Bot/g" "$FILE" + sed -i "s/ \(https:\/\/github\.com\/open-webui\/open-webui\) RAG Bot/Open-WebUI-RAG-Bot/g" "$FILE" + + # Исправляем если используется в headers dict с пробелом + sed -i 's/"User-Agent": " (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$FILE" + sed -i "s/'User-Agent': ' (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'User-Agent': 'Open-WebUI-RAG-Bot'/g" "$FILE" + sed -i 's/"User-Agent": "(https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$FILE" + + # Исправляем f-string или конкатенацию + sed -i 's/User-Agent.*github\.com.*open-webui.*RAG Bot/User-Agent": "Open-WebUI-RAG-Bot/g' "$FILE" + + # Универсальная замена - ищем любую строку с пробелом перед User-Agent значением + sed -i 's/"User-Agent": " [^"]*RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$FILE" + sed -i "s/'User-Agent': ' [^']*RAG Bot'/'User-Agent': 'Open-WebUI-RAG-Bot'/g" "$FILE" + + # Исправляем вариант "External Web Loader" + sed -i 's/"User-Agent": " (https:\/\/github\.com\/open-webui\/open-webui) External Web Loader"/"User-Agent": "Open-WebUI-External-Web-Loader"/g' "$FILE" + + # Универсальная замена для любых User-Agent с пробелом и github.com/open-webui + sed -i 's/"User-Agent": " \([^"]*github\.com\/open-webui[^"]*\)"/"User-Agent": "Open-WebUI-Bot"/g' "$FILE" + sed -i "s/'User-Agent': ' \([^']*github\.com\/open-webui[^']*\)'/'User-Agent': 'Open-WebUI-Bot'/g" "$FILE" + + echo "✓ Патч применен к $FILE" + else + echo " Проблемная строка не найдена в $FILE (возможно, уже исправлена)" + fi +else + echo "⚠ Файл $FILE не найден" +fi + +# Исправляем в middleware.py +FILE2="/app/backend/open_webui/utils/middleware.py" +if [ -f "$FILE2" ]; then + echo "Обработка файла: $FILE2" + PROBLEM_FOUND2=0 + + if grep -qE "(github\.com/open-webui|RAG Bot|https://github)" "$FILE2" 2>/dev/null; then + PROBLEM_FOUND2=1 + fi + + if [ "$PROBLEM_FOUND2" -eq 1 ] || grep -q "User-Agent" "$FILE2" 2>/dev/null; then + echo " Найдены строки с User-Agent, применяем патч..." + + # Те же замены что и для retrieval.py + sed -i "s/' (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$FILE2" + sed -i "s/ '(https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$FILE2" + sed -i 's/" (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$FILE2" + sed -i 's/ "(https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$FILE2" + sed -i "s/ (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot/Open-WebUI-RAG-Bot/g" "$FILE2" + sed -i 's/"User-Agent": " (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$FILE2" + sed -i "s/'User-Agent': ' (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'User-Agent': 'Open-WebUI-RAG-Bot'/g" "$FILE2" + sed -i 's/"User-Agent": "(https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$FILE2" + sed -i 's/User-Agent.*github\.com.*open-webui.*RAG Bot/User-Agent": "Open-WebUI-RAG-Bot/g' "$FILE2" + sed -i 's/"User-Agent": " [^"]*RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$FILE2" + sed -i "s/'User-Agent': ' [^']*RAG Bot'/'User-Agent': 'Open-WebUI-RAG-Bot'/g" "$FILE2" + + # Исправляем вариант "External Web Loader" + sed -i 's/"User-Agent": " (https:\/\/github\.com\/open-webui\/open-webui) External Web Loader"/"User-Agent": "Open-WebUI-External-Web-Loader"/g' "$FILE2" + + # Универсальная замена для любых User-Agent с пробелом и github.com/open-webui + sed -i 's/"User-Agent": " \([^"]*github\.com\/open-webui[^"]*\)"/"User-Agent": "Open-WebUI-Bot"/g' "$FILE2" + sed -i "s/'User-Agent': ' \([^']*github\.com\/open-webui[^']*\)'/'User-Agent': 'Open-WebUI-Bot'/g" "$FILE2" + + echo "✓ Патч применен к $FILE2" + else + echo " Проблемная строка не найдена в $FILE2 (возможно, уже исправлена)" + fi +fi + +# Ищем в других возможных файлах (более агрессивный поиск) +echo "Поиск проблемной строки во всех Python файлах..." +find /app/backend -name "*.py" -type f 2>/dev/null | while read pyfile; do + if [ "$pyfile" != "$FILE" ] && [ "$pyfile" != "$FILE2" ]; then + # Проверяем наличие проблемной строки в любом виде + if grep -qE "(github\.com/open-webui|RAG Bot|https://github)" "$pyfile" 2>/dev/null; then + echo " Обработка файла: $pyfile" + # Применяем все варианты замены + sed -i "s/' (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$pyfile" + sed -i "s/ '(https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$pyfile" + sed -i 's/" (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$pyfile" + sed -i 's/ "(https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$pyfile" + sed -i "s/ (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot/Open-WebUI-RAG-Bot/g" "$pyfile" + sed -i 's/"User-Agent": " [^"]*RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$pyfile" + sed -i "s/'User-Agent': ' [^']*RAG Bot'/'User-Agent': 'Open-WebUI-RAG-Bot'/g" "$pyfile" + # Исправляем вариант "External Web Loader" + sed -i 's/"User-Agent": " (https:\/\/github\.com\/open-webui\/open-webui) External Web Loader"/"User-Agent": "Open-WebUI-External-Web-Loader"/g' "$pyfile" + # Универсальная замена + sed -i 's/"User-Agent": " \([^"]*github\.com\/open-webui[^"]*\)"/"User-Agent": "Open-WebUI-Bot"/g' "$pyfile" + sed -i "s/'User-Agent': ' \([^']*github\.com\/open-webui[^']*\)'/'User-Agent': 'Open-WebUI-Bot'/g" "$pyfile" + fi + fi +done + +# Удаляем скомпилированные Python файлы (.pyc), чтобы они пересобрались +echo "Очистка скомпилированных файлов Python..." +find /app/backend -name "*.pyc" -delete 2>/dev/null +find /app/backend -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null + +echo "Патч применен. Запуск Open WebUI..." diff --git a/scripts/fix_user_agent_aggressive.sh b/scripts/fix_user_agent_aggressive.sh new file mode 100755 index 0000000..24df752 --- /dev/null +++ b/scripts/fix_user_agent_aggressive.sh @@ -0,0 +1,59 @@ +#!/bin/sh +# Агрессивное исправление бага с User-Agent в Open WebUI +# Ищет и исправляет проблемную строку во всех возможных местах + +echo "=== АГРЕССИВНОЕ ИСПРАВЛЕНИЕ USER-AGENT ===" +echo "" + +# Находим все файлы с проблемной строкой +echo "Поиск всех файлов с проблемной строкой User-Agent..." +PROBLEM_FILES=$(docker exec open-webui find /app/backend -name "*.py" -type f -exec grep -l "github.com/open-webui" {} \; 2>/dev/null) + +if [ -z "$PROBLEM_FILES" ]; then + echo "Файлы не найдены через docker exec, пробуем другой способ..." + # Альтернативный способ - ищем через grep в контейнере + docker exec open-webui sh -c 'find /app/backend -name "*.py" -type f | xargs grep -l "github.com/open-webui" 2>/dev/null' | while read pyfile; do + if [ -n "$pyfile" ]; then + echo "Исправление файла: $pyfile" + docker exec open-webui sed -i "s/' (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$pyfile" + docker exec open-webui sed -i 's/" (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$pyfile" + docker exec open-webui sed -i "s/ (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot/Open-WebUI-RAG-Bot/g" "$pyfile" + docker exec open-webui sed -i 's/"User-Agent": " [^"]*RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$pyfile" + docker exec open-webui sed -i "s/'User-Agent': ' [^']*RAG Bot'/'User-Agent': 'Open-WebUI-RAG-Bot'/g" "$pyfile" + fi + done +else + echo "$PROBLEM_FILES" | while read pyfile; do + echo "Исправление файла: $pyfile" + docker exec open-webui sed -i "s/' (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'Open-WebUI-RAG-Bot'/g" "$pyfile" + docker exec open-webui sed -i 's/" (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"Open-WebUI-RAG-Bot"/g' "$pyfile" + docker exec open-webui sed -i "s/ (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot/Open-WebUI-RAG-Bot/g" "$pyfile" + docker exec open-webui sed -i 's/"User-Agent": " [^"]*RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' "$pyfile" + docker exec open-webui sed -i "s/'User-Agent': ' [^']*RAG Bot'/'User-Agent': 'Open-WebUI-RAG-Bot'/g" "$pyfile" + done +fi + +# Очищаем кеш Python +echo "" +echo "Очистка кеша Python..." +docker exec open-webui find /app/backend -name "*.pyc" -delete 2>/dev/null +docker exec open-webui find /app/backend -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true + +# Проверяем результат +echo "" +echo "Проверка результата..." +RAG_BOT_COUNT=$(docker exec open-webui grep -r "github.com/open-webui.*RAG Bot" /app/backend 2>/dev/null | wc -l) +if [ "$RAG_BOT_COUNT" -eq 0 ]; then + echo "✓ Проблемная строка не найдена - патч применен успешно" +else + echo "⚠ Найдено вхождений проблемной строки: $RAG_BOT_COUNT" + echo "Проблемные файлы:" + docker exec open-webui grep -r "github.com/open-webui.*RAG Bot" /app/backend 2>/dev/null | cut -d: -f1 | sort -u +fi + +echo "" +echo "Перезапуск Open WebUI..." +docker restart open-webui + +echo "" +echo "Готово! Подождите 15 секунд и проверьте поиск." diff --git a/scripts/fix_user_agent_final.sh b/scripts/fix_user_agent_final.sh new file mode 100755 index 0000000..af619d0 --- /dev/null +++ b/scripts/fix_user_agent_final.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# Финальное исправление всех вариантов User-Agent с пробелом в начале + +echo "=== ФИНАЛЬНОЕ ИСПРАВЛЕНИЕ USER-AGENT ===" +echo "" + +# Исправляем "External Web Loader" вариант +echo "Исправление external_web.py..." +docker exec open-webui sed -i 's/"User-Agent": " (https:\/\/github\.com\/open-webui\/open-webui) External Web Loader"/"User-Agent": "Open-WebUI-External-Web-Loader"/g' /app/backend/open_webui/retrieval/loaders/external_web.py + +# Исправляем "RAG Bot" вариант (на всякий случай еще раз) +echo "Исправление всех вариантов RAG Bot..." +docker exec open-webui find /app/backend -name "*.py" -type f -exec sed -i 's/"User-Agent": " (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot"/"User-Agent": "Open-WebUI-RAG-Bot"/g' {} \; +docker exec open-webui find /app/backend -name "*.py" -type f -exec sed -i "s/'User-Agent': ' (https:\/\/github\.com\/open-webui\/open-webui) RAG Bot'/'User-Agent': 'Open-WebUI-RAG-Bot'/g" {} \; + +# Универсальная замена - любой User-Agent с пробелом в начале +echo "Универсальная замена всех User-Agent с пробелом..." +docker exec open-webui find /app/backend -name "*.py" -type f -exec sed -i 's/"User-Agent": " \([^"]*github\.com\/open-webui[^"]*\)"/"User-Agent": "Open-WebUI-Bot"/g' {} \; +docker exec open-webui find /app/backend -name "*.py" -type f -exec sed -i "s/'User-Agent': ' \([^']*github\.com\/open-webui[^']*\)'/'User-Agent': 'Open-WebUI-Bot'/g" {} \; + +# Очистка кеша +echo "Очистка кеша Python..." +docker exec open-webui find /app/backend -name "*.pyc" -delete 2>/dev/null +docker exec open-webui find /app/backend -name "__pycache__" -type d -exec rm -rf {} + 2>/dev/null || true + +# Проверка +echo "" +echo "Проверка результата..." +PROBLEM_COUNT=$(docker exec open-webui grep -rn '"User-Agent": " (' /app/backend --include="*.py" 2>/dev/null | wc -l) +if [ "$PROBLEM_COUNT" -eq 0 ]; then + echo "✓ Все проблемные строки исправлены!" +else + echo "⚠ Найдено проблемных строк: $PROBLEM_COUNT" + docker exec open-webui grep -rn '"User-Agent": " (' /app/backend --include="*.py" 2>/dev/null +fi + +echo "" +echo "Перезапуск Open WebUI..." +docker restart open-webui + +echo "" +echo "✓ Готово! Подождите 15 секунд и проверьте поиск." diff --git a/scripts/init-logos.sh b/scripts/init-logos.sh new file mode 100644 index 0000000..5ca3553 --- /dev/null +++ b/scripts/init-logos.sh @@ -0,0 +1,35 @@ +#!/bin/bash +# Скрипт для автоматической замены логотипов при запуске контейнера +# Этот скрипт можно запускать при каждом старте контейнера + +MEDIA_DIR="/app/media" +MAX_RETRIES=10 +RETRY_DELAY=2 + +# Ждем пока контейнер полностью запустится +for i in $(seq 1 $MAX_RETRIES); do + if curl -f http://localhost:8080/health >/dev/null 2>&1; then + break + fi + sleep $RETRY_DELAY +done + +# Находим все favicon и logo файлы +find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "logo.png" -o -name "logo.svg" \) 2>/dev/null | while read file; do + dir=$(dirname "$file") + name=$(basename "$file") + + # Заменяем favicon + if [[ "$name" == favicon* ]] && [ -f "$MEDIA_DIR/favicon.png" ]; then + cp "$MEDIA_DIR/favicon.png" "$file" 2>/dev/null || true + # Также создаем .ico + cp "$MEDIA_DIR/favicon.png" "$dir/favicon.ico" 2>/dev/null || true + fi + + # Заменяем logo + if [[ "$name" == logo* ]] && [ -f "$MEDIA_DIR/logo.png" ]; then + cp "$MEDIA_DIR/logo.png" "$file" 2>/dev/null || true + # Также создаем .svg + cp "$MEDIA_DIR/logo.png" "$dir/logo.svg" 2>/dev/null || true + fi +done diff --git a/scripts/init.sh b/scripts/init.sh new file mode 100755 index 0000000..8bc87fa --- /dev/null +++ b/scripts/init.sh @@ -0,0 +1,45 @@ +#!/bin/bash +# Скрипт инициализации iiEasy AI-платформы +# Выполняет ребрендинг и загрузку модели Ollama + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "=== Инициализация iiEasy AI-платформы ===" +echo "" + +cd "$PROJECT_DIR" + +# Проверка наличия контейнеров +if ! docker ps --format '{{.Names}}' | grep -q "^open-webui$"; then + echo "Ошибка: Контейнер open-webui не запущен. Запустите: docker compose up -d" + exit 1 +fi + +if ! docker ps --format '{{.Names}}' | grep -q "^ollama$"; then + echo "Ошибка: Контейнер ollama не запущен. Запустите: docker compose up -d" + exit 1 +fi + +# 1. Ребрендинг Open WebUI +echo "1. Применение ребрендинга Open WebUI..." +"$SCRIPT_DIR/rebrand.sh" + +echo "" +echo "2. Загрузка модели Ollama (gemma3n:e4b-it-fp16)..." +echo " Это может занять несколько минут в зависимости от скорости интернета..." +docker exec ollama ollama pull gemma3n:e4b-it-fp16 + +echo "" +echo "3. Проверка загруженных моделей..." +docker exec ollama ollama list + +echo "" +echo "=== Инициализация завершена! ===" +echo "" +echo "Проверьте:" +echo " - Open WebUI: https://odo.iieasy.ru" +echo " - Логотип и ребрендинг применены" +echo " - Модель gemma3n:e4b-it-fp16 доступна в Ollama" diff --git a/scripts/install_vision_model.sh b/scripts/install_vision_model.sh new file mode 100755 index 0000000..d38cd92 --- /dev/null +++ b/scripts/install_vision_model.sh @@ -0,0 +1,58 @@ +#!/bin/bash +# Установка специализированной vision модели для Ollama + +set -e + +CONTAINER_OLLAMA="ollama" +MODEL_CHOICE="${1:-llava}" + +echo "=== Установка Vision модели для Ollama ===" +echo "" + +# Определяем команду docker +DOCKER_CMD="docker" +if ! docker ps >/dev/null 2>&1; then + DOCKER_CMD="sudo docker" +fi + +echo "Доступные vision модели:" +echo " 1. llava:latest (LLaVA 1.6) - 7B, лучший выбор для vision" +echo " 2. bakllava:latest (BakLLaVA) - 7B, Mistral + LLaVA" +echo " 3. llama3.2-vision:latest (Llama 3.2 Vision) - 11B, требует 8GB VRAM" +echo "" + +if [ "$MODEL_CHOICE" = "llava" ]; then + MODEL="llava:latest" + echo "Выбрана модель: $MODEL (LLaVA 1.6)" +elif [ "$MODEL_CHOICE" = "bakllava" ]; then + MODEL="bakllava:latest" + echo "Выбрана модель: $MODEL (BakLLaVA)" +elif [ "$MODEL_CHOICE" = "llama3.2" ]; then + MODEL="llama3.2-vision:latest" + echo "Выбрана модель: $MODEL (Llama 3.2 Vision)" +else + MODEL="llava:latest" + echo "Используется модель по умолчанию: $MODEL" +fi + +echo "" +echo "Загрузка модели $MODEL..." +echo "Это может занять несколько минут в зависимости от скорости интернета..." +echo "" + +$DOCKER_CMD exec $CONTAINER_OLLAMA ollama pull "$MODEL" || { + echo "✗ Ошибка при загрузке модели" + exit 1 +} + +echo "" +echo "✓ Модель загружена!" +echo "" +echo "Проверка установленных моделей:" +$DOCKER_CMD exec $CONTAINER_OLLAMA ollama list + +echo "" +echo "=== Готово! ===" +echo "" +echo "Теперь в Open WebUI выберите модель: $MODEL" +echo "И попробуйте загрузить изображение и задать вопрос о нем." diff --git a/scripts/rebrand.sh b/scripts/rebrand.sh new file mode 100755 index 0000000..d2a0490 --- /dev/null +++ b/scripts/rebrand.sh @@ -0,0 +1,248 @@ +#!/bin/bash +# Скрипт ребрендинга Open WebUI для iiEasy +# ВНИМАНИЕ: Этот скрипт может ломать OAuth! +# Рекомендуется использовать rebrand_complete.sh для полного ребрендинга +# Заменяет логотипы, favicon, удаляет упоминания Open WebUI, отключает проверку обновлений + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== Ребрендинг Open WebUI для iiEasy ===" + +# Проверка наличия контейнера +if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не найден. Запустите docker-compose up -d сначала." + exit 1 +fi + +# Проверка, что контейнер запущен +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен. Запустите docker-compose up -d." + exit 1 +fi + +echo "1. Замена логотипов и favicon..." + +# Важно: Open WebUI может использовать скомпилированные статические файлы +# Нужно найти правильные пути и заменить файлы там, где они реально используются + +# Определяем пути для статических файлов в Open WebUI +# Open WebUI использует /app/web/build/_app/immutable/ для статических файлов +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" + "/app/backend/static" + "/app/static" + "/app/public" +) + +# Находим существующую директорию со статическими файлами +STATIC_DIR="" +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + STATIC_DIR="$dir" + echo " Найдена директория статических файлов: $STATIC_DIR" + break + fi +done + +if [ -z "$STATIC_DIR" ]; then + echo " Предупреждение: Директория статических файлов не найдена, пробуем найти через поиск favicon..." + # Ищем где находятся существующие favicon файлы + FAVICON_PATH=$(docker exec "${CONTAINER_NAME}" find /app -name "favicon.png" -o -name "favicon.ico" 2>/dev/null | head -1) + if [ -n "$FAVICON_PATH" ]; then + STATIC_DIR=$(dirname "$FAVICON_PATH") + echo " Найдена директория через поиск favicon: $STATIC_DIR" + else + STATIC_DIR="/app/web/build/_app/immutable" + echo " Используем стандартный путь: $STATIC_DIR" + # Создаем директорию если её нет + docker exec "${CONTAINER_NAME}" mkdir -p "$STATIC_DIR" 2>/dev/null || true + fi +fi + +# Копирование логотипов (приоритет: PNG > SVG) +# Копируем во все найденные директории +for target_dir in "$STATIC_DIR" "/app/web/build/_app/immutable" "/app/web/static" "/app/web/build"; do + if docker exec "${CONTAINER_NAME}" test -d "$target_dir" 2>/dev/null || [ "$target_dir" = "$STATIC_DIR" ]; then + echo " Копирование в $target_dir..." + + # Логотипы + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${target_dir}/logo.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${target_dir}/logo.svg" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/logo-light.svg" ]; then + docker cp "$MEDIA_DIR/logo-light.svg" "${CONTAINER_NAME}:${target_dir}/logo.svg" 2>/dev/null || true + fi + + if [ -f "$MEDIA_DIR/logo-dark.svg" ]; then + docker cp "$MEDIA_DIR/logo-dark.svg" "${CONTAINER_NAME}:${target_dir}/logo-dark.svg" 2>/dev/null || true + fi + + # Favicon + if [ -f "$MEDIA_DIR/favicon.ico" ]; then + docker cp "$MEDIA_DIR/favicon.ico" "${CONTAINER_NAME}:${target_dir}/favicon.ico" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon.ico" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon-96x96.png" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/favicon.svg" ]; then + docker cp "$MEDIA_DIR/favicon.svg" "${CONTAINER_NAME}:${target_dir}/favicon.svg" 2>/dev/null || true + fi + fi +done + +# Поиск и замена существующих favicon и logo файлов везде в /app +echo " Поиск существующих favicon и logo файлов для замены..." +EXISTING_FAVICONS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "favicon.svg" \) 2>/dev/null | head -10) +EXISTING_LOGOS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "logo.png" -o -name "logo.svg" \) 2>/dev/null | head -10) + +echo " Найдено favicon файлов: $(echo "$EXISTING_FAVICONS" | wc -l)" +echo " Найдено logo файлов: $(echo "$EXISTING_LOGOS" | wc -l)" + +# Заменяем существующие favicon файлы +if [ -f "$MEDIA_DIR/favicon.png" ]; then + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ]; then + echo " Замена: $favicon_file" + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + # Также создаем .ico версию рядом + favicon_dir=$(dirname "$favicon_file") + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_dir}/favicon.ico" 2>/dev/null || true + fi + done +fi + +# Заменяем существующие logo файлы +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_LOGOS" | while read -r logo_file; do + if [ -n "$logo_file" ]; then + echo " Замена: $logo_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + done +fi + +# Также копируем в корень public для веб-доступа +PUBLIC_DIRS=( + "/app/web/public" + "/app/public" + "/app/backend/public" +) + +for pub_dir in "${PUBLIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$pub_dir" 2>/dev/null; then + echo " Копирование в $pub_dir..." + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${pub_dir}/logo.png" 2>/dev/null || true + fi + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${pub_dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${pub_dir}/favicon.ico" 2>/dev/null || true + fi + break + fi +done + +echo " Примечание: Если логотипы не изменились, настройте их через Admin Panel:" +echo " Settings → Appearance → Logo (загрузите файлы из /app/media/)" + +echo "2. Поиск и замена текстовых упоминаний 'Open WebUI'..." + +# Поиск файлов с упоминаниями Open WebUI в статических файлах и конфигурации +# ВАЖНО: ИСКЛЮЧАЕМ ВСЕ файлы, связанные с OAuth/Authentik/аутентификацией +# Это включает: oauth.py, auth.py, login.py, и все файлы в директориях oauth, oidc, auth, login +docker exec "${CONTAINER_NAME}" find /app -type f \( -name "*.py" -o -name "*.html" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.json" -o -name "*.svelte" \) \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + ! -name "*oauth*" ! -name "*oidc*" ! -name "*auth*" ! -name "*login*" \ + ! -path "*/utils/oauth*" ! -path "*/utils/auth*" ! -path "*/backend/open_webui/utils/oauth*" \ + -exec grep -l "Open WebUI\|open-webui\|openwebui\|\(Open WebUI\)" {} \; 2>/dev/null | while read file; do + echo " Обработка: $file" + # Замена "Open WebUI" на "iiEasyWeb" (только в тексте интерфейса, не в URL) + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + # Удаление "(Open WebUI)" в скобках - заменяем на пустую строку или только "iiEasyWeb" + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/\(Open WebUI\)//g' "$file" 2>/dev/null || true + # Замена "iiEasyWeb (Open WebUI)" на просто "iiEasyWeb" + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb (Open WebUI)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb \(Open WebUI\)/iiEasyWeb/g' "$file" 2>/dev/null || true + # ВАЖНО: НЕ заменяем open-webui в нижнем регистре, так как это может быть частью URL или конфигурации OAuth +done + +# Специальная обработка для удаления "(Open WebUI)" из заголовков и описаний +echo "3. Удаление упоминаний '(Open WebUI)' из интерфейса..." +# ИСКЛЮЧАЕМ файлы OAuth/Authentik +docker exec "${CONTAINER_NAME}" find /app -type f \( -name "*.html" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.svelte" \) \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -l "(Open WebUI)\|\(Open WebUI\)" {} \; 2>/dev/null | while read file; do + echo " Удаление '(Open WebUI)' из: $file" + # Удаляем различные варианты написания в скобках + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/\(Open WebUI\)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/ (Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/ \(Open WebUI\)//g' "$file" 2>/dev/null || true +done + +echo "4. Отключение проверки обновлений..." + +# Поиск и отключение проверки обновлений через GitHub API +# ИСКЛЮЧАЕМ файлы OAuth/Authentik +docker exec "${CONTAINER_NAME}" find /app/backend -type f \( -name "*.py" -o -name "*.js" \) \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -l "github.com.*releases\|check.*update\|update.*check" {} \; 2>/dev/null | while read file; do + echo " Отключение проверки обновлений в: $file" + # Комментирование вызовов GitHub API для проверки обновлений + docker exec "${CONTAINER_NAME}" sed -i 's|https://api.github.com/repos/open-webui|# https://api.github.com/repos/open-webui|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|github.com/open-webui|# github.com/open-webui|g' "$file" 2>/dev/null || true +done + +# Отключение проверки обновлений через переменные окружения (уже настроено в docker-compose.yml) +echo " Проверка обновлений отключена через переменные окружения" + +echo "5. Удаление аналитики и телеметрии..." + +# Поиск и отключение аналитики (более аккуратно, чтобы не сломать код) +# Комментируем только целые строки с импортами аналитики, а не части строк +# ИСКЛЮЧАЕМ файлы OAuth/Authentik +docker exec "${CONTAINER_NAME}" find /app/backend -type f \( -name "*.py" -o -name "*.js" \) \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -l "from.*telemetry\|import.*telemetry\|analytics\|tracking\|gtag\|ga(\|google-analytics" {} \; 2>/dev/null | while read file; do + echo " Отключение аналитики в: $file" + # Комментируем только целые строки импорта, начинающиеся с from или import + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*from.*telemetry/s/^/# /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*import.*telemetry/s/^/# /' "$file" 2>/dev/null || true +done + +echo "6. Удаление 'Powered by' футеров..." + +# Поиск футеров с упоминанием Open WebUI +# ИСКЛЮЧАЕМ файлы OAuth/Authentik +docker exec "${CONTAINER_NAME}" find /app/backend -type f \( -name "*.html" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx" \) \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -l "Powered by\|powered by" {} \; 2>/dev/null | while read file; do + echo " Удаление футера в: $file" + docker exec "${CONTAINER_NAME}" sed -i '/Powered by.*[Oo]pen.*[Ww]eb[Uu][Ii]/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/powered by.*[Oo]pen.*[Ww]eb[Uu][Ii]/d' "$file" 2>/dev/null || true +done + +echo "7. Перезапуск контейнера для применения изменений..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Не удалось перезапустить контейнер автоматически." + echo "Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== Ребрендинг завершен! ===" +echo "" +echo "Проверьте изменения:" +echo " 1. Откройте https://odo.iieasy.ru в браузере" +echo " 2. Проверьте логотип и favicon" +echo " 3. Проверьте отсутствие упоминаний 'Open WebUI'" +echo "" +echo "Примечание: Если изменения не отображаются, очистите кеш браузера." diff --git a/scripts/rebrand_careful.sh b/scripts/rebrand_careful.sh new file mode 100755 index 0000000..430a65c --- /dev/null +++ b/scripts/rebrand_careful.sh @@ -0,0 +1,126 @@ +#!/bin/bash +# ОЧЕНЬ АККУРАТНЫЙ ребрендинг Open WebUI для iiEasy +# Только безопасные замены текста, НЕ трогает код + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== АККУРАТНЫЙ ребрендинг Open WebUI для iiEasy ===" +echo "⚠ Только безопасные замены текста в интерфейсе" +echo "⚠ Код Python/JS НЕ изменяется" +echo "⚠ OAuth/Authentik полностью защищены" +echo "" + +# Проверка наличия контейнера +if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не найден." + exit 1 +fi + +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен." + exit 1 +fi + +echo "1. Замена логотипов и favicon..." + +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" +) + +STATIC_DIR="" +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + STATIC_DIR="$dir" + break + fi +done + +if [ -z "$STATIC_DIR" ]; then + FAVICON_PATH=$(docker exec "${CONTAINER_NAME}" find /app/web -name "favicon.png" -o -name "favicon.ico" 2>/dev/null | head -1) + if [ -n "$FAVICON_PATH" ]; then + STATIC_DIR=$(dirname "$FAVICON_PATH") + else + STATIC_DIR="/app/web/build/_app/immutable" + fi +fi + +# Копирование логотипов +if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${STATIC_DIR}/logo.png" 2>/dev/null || true +fi +if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${STATIC_DIR}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${STATIC_DIR}/favicon.ico" 2>/dev/null || true +fi + +echo "2. Замена текста 'Open WebUI' на 'iiEasyWeb' ТОЛЬКО в HTML/текстовых строках..." + +# ТОЛЬКО фронтенд HTML/Svelte файлы - заменяем только в текстовом контенте +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + # Заменяем только в текстовом контенте между тегами, не в атрибутах или коде + if docker exec "${CONTAINER_NAME}" grep -q "Open WebUI" "$file" 2>/dev/null; then + echo " HTML/Svelte: $file" + # Заменяем только "Open WebUI" (с заглавными) в тексте, не трогаем код + docker exec "${CONTAINER_NAME}" sed -i 's/>Open WebUIiiEasyWeb/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + fi +done + +echo "3. Удаление '(Open WebUI)' из текста интерфейса..." + +# Только удаляем текст в скобках из HTML +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "(Open WebUI)" "$file" 2>/dev/null; then + echo " Удаление скобок: $file" + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + fi +done + +echo "4. Удаление 'Powered by Open WebUI' футеров..." + +# Только удаляем строки с "Powered by" +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Powered by.*Open WebUI\|powered by.*Open WebUI" "$file" 2>/dev/null; then + echo " Удаление футера: $file" + docker exec "${CONTAINER_NAME}" sed -i '/Powered by.*Open WebUI/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/powered by.*Open WebUI/d' "$file" 2>/dev/null || true + fi +done + +echo "" +echo "✓ Изменены ТОЛЬКО текстовые строки в HTML/Svelte" +echo "✓ Код Python/JS НЕ изменялся" +echo "✓ OAuth/Authentik полностью защищены" +echo "" + +echo "5. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== АККУРАТНЫЙ ребрендинг завершен! ===" +echo "" +echo "Проверьте:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. Все должно работать нормально" +echo " 3. Текст 'Open WebUI' заменен на 'iiEasyWeb' в интерфейсе" +echo "" +echo "Если что-то не работает, пересоздайте контейнер:" +echo " sudo docker compose stop open-webui" +echo " sudo docker compose rm -f open-webui" +echo " sudo docker compose up -d open-webui" diff --git a/scripts/rebrand_complete.sh b/scripts/rebrand_complete.sh new file mode 100755 index 0000000..42ffa77 --- /dev/null +++ b/scripts/rebrand_complete.sh @@ -0,0 +1,274 @@ +#!/bin/bash +# ПОЛНЫЙ ребрендинг Open WebUI для iiEasy +# Удаляет ВСЕ упоминания Open WebUI, отключает телеметрию +# НО защищает OAuth/Authentik файлы + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== ПОЛНЫЙ ребрендинг Open WebUI для iiEasy ===" +echo "✓ Удаление ВСЕХ упоминаний Open WebUI" +echo "✓ Отключение телеметрии и аналитики" +echo "✓ Защита OAuth/Authentik файлов" +echo "" + +# Проверка наличия контейнера +if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не найден. Запустите docker-compose up -d сначала." + exit 1 +fi + +# Проверка, что контейнер запущен +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен. Запустите docker-compose up -d." + exit 1 +fi + +echo "1. Замена логотипов и favicon..." + +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" + "/app/backend/static" + "/app/static" + "/app/public" +) + +STATIC_DIR="" +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + STATIC_DIR="$dir" + echo " Найдена директория: $STATIC_DIR" + break + fi +done + +if [ -z "$STATIC_DIR" ]; then + FAVICON_PATH=$(docker exec "${CONTAINER_NAME}" find /app -name "favicon.png" -o -name "favicon.ico" 2>/dev/null | head -1) + if [ -n "$FAVICON_PATH" ]; then + STATIC_DIR=$(dirname "$FAVICON_PATH") + else + STATIC_DIR="/app/web/build/_app/immutable" + docker exec "${CONTAINER_NAME}" mkdir -p "$STATIC_DIR" 2>/dev/null || true + fi +fi + +# Копирование логотипов +for target_dir in "$STATIC_DIR" "/app/web/build/_app/immutable" "/app/web/static" "/app/web/build"; do + if docker exec "${CONTAINER_NAME}" test -d "$target_dir" 2>/dev/null || [ "$target_dir" = "$STATIC_DIR" ]; then + echo " Копирование в $target_dir..." + + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${target_dir}/logo.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${target_dir}/logo.svg" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/logo-light.svg" ]; then + docker cp "$MEDIA_DIR/logo-light.svg" "${CONTAINER_NAME}:${target_dir}/logo.svg" 2>/dev/null || true + fi + + if [ -f "$MEDIA_DIR/logo-dark.svg" ]; then + docker cp "$MEDIA_DIR/logo-dark.svg" "${CONTAINER_NAME}:${target_dir}/logo-dark.svg" 2>/dev/null || true + fi + + if [ -f "$MEDIA_DIR/favicon.ico" ]; then + docker cp "$MEDIA_DIR/favicon.ico" "${CONTAINER_NAME}:${target_dir}/favicon.ico" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon.ico" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon-96x96.png" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/favicon.svg" ]; then + docker cp "$MEDIA_DIR/favicon.svg" "${CONTAINER_NAME}:${target_dir}/favicon.svg" 2>/dev/null || true + fi + fi +done + +# Замена существующих favicon и logo +EXISTING_FAVICONS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "favicon.svg" \) 2>/dev/null | head -10) +EXISTING_LOGOS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "logo.png" -o -name "logo.svg" \) 2>/dev/null | head -10) + +if [ -f "$MEDIA_DIR/favicon.png" ]; then + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + favicon_dir=$(dirname "$favicon_file") + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_dir}/favicon.ico" 2>/dev/null || true + fi + done +fi + +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_LOGOS" | while read -r logo_file; do + if [ -n "$logo_file" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + done +fi + +PUBLIC_DIRS=("/app/web/public" "/app/public" "/app/backend/public") +for pub_dir in "${PUBLIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$pub_dir" 2>/dev/null; then + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${pub_dir}/logo.png" 2>/dev/null || true + fi + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${pub_dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${pub_dir}/favicon.ico" 2>/dev/null || true + fi + break + fi +done + +echo "2. Удаление упоминаний 'Open WebUI' из фронтенда..." + +# Изменяем ТОЛЬКО фронтенд файлы (веб-интерфейс) +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.svelte" -o -name "*.json" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Open WebUI\|open-webui\|openwebui\|\(Open WebUI\)" "$file" 2>/dev/null; then + echo " Фронтенд: $file" + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/\(Open WebUI\)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb (Open WebUI)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb \(Open WebUI\)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/open-webui/iieasyweb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/openwebui/iieasyweb/g' "$file" 2>/dev/null || true + fi +done + +echo "3. Удаление упоминаний 'Open WebUI' из бэкенда (кроме OAuth)..." + +# Изменяем бэкенд Python файлы, НО исключаем OAuth/аутентификационные файлы +docker exec "${CONTAINER_NAME}" find /app/backend -type f -name "*.py" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + ! -name "*oauth*" ! -name "*oidc*" ! -name "*auth*" ! -name "*login*" \ + ! -path "*/utils/oauth*" ! -path "*/utils/auth*" \ + ! -path "*/open_webui/utils/oauth*" ! -path "*/open_webui/utils/auth*" \ + ! -path "*/open_webui/main.py" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Open WebUI\|open-webui\|openwebui\|\(Open WebUI\)" "$file" 2>/dev/null; then + echo " Бэкенд: $file" + # Замена только текста, не URL или конфигурации + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/\(Open WebUI\)//g' "$file" 2>/dev/null || true + # НЕ заменяем open-webui/openwebui в бэкенде - может быть частью конфигурации + fi +done + +echo "4. Отключение телеметрии и аналитики в бэкенде..." + +# Отключаем телеметрию в Python файлах, НО исключаем OAuth файлы +docker exec "${CONTAINER_NAME}" find /app/backend -type f -name "*.py" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + ! -name "*oauth*" ! -name "*oidc*" ! -name "*auth*" ! -name "*login*" \ + ! -path "*/utils/oauth*" ! -path "*/utils/auth*" \ + ! -path "*/open_webui/utils/oauth*" ! -path "*/open_webui/utils/auth*" \ + ! -path "*/open_webui/main.py" \ + -exec grep -l "from.*telemetry\|import.*telemetry\|analytics\|tracking\|gtag\|ga(\|google-analytics\|sentry\|posthog" {} \; 2>/dev/null | while read file; do + echo " Отключение телеметрии в: $file" + # Комментируем только целые строки импорта + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*from.*telemetry/s/^/# /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*import.*telemetry/s/^/# /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*from.*analytics/s/^/# /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*import.*analytics/s/^/# /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*from.*tracking/s/^/# /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*import.*tracking/s/^/# /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*from.*sentry/s/^/# /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/^[[:space:]]*import.*sentry/s/^/# /' "$file" 2>/dev/null || true +done + +echo "5. Отключение телеметрии во фронтенде..." + +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + -exec grep -l "telemetry\|analytics\|tracking\|gtag\|ga(\|google-analytics\|sentry\|posthog" {} \; 2>/dev/null | while read file; do + echo " Отключение телеметрии в: $file" + docker exec "${CONTAINER_NAME}" sed -i '/import.*telemetry/s/^/\/\/ /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/from.*telemetry/s/^/\/\/ /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/import.*analytics/s/^/\/\/ /' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/from.*analytics/s/^/\/\/ /' "$file" 2>/dev/null || true +done + +echo "6. Отключение проверки обновлений..." + +# Отключаем проверку обновлений в бэкенде (кроме OAuth) +docker exec "${CONTAINER_NAME}" find /app/backend -type f -name "*.py" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + ! -name "*oauth*" ! -name "*oidc*" ! -name "*auth*" ! -name "*login*" \ + ! -path "*/utils/oauth*" ! -path "*/utils/auth*" \ + ! -path "*/open_webui/utils/oauth*" ! -path "*/open_webui/utils/auth*" \ + ! -path "*/open_webui/main.py" \ + -exec grep -l "github.com.*releases\|check.*update\|update.*check" {} \; 2>/dev/null | while read file; do + echo " Отключение проверки обновлений в: $file" + docker exec "${CONTAINER_NAME}" sed -i 's|https://api.github.com/repos/open-webui|# https://api.github.com/repos/open-webui|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|github.com/open-webui|# github.com/open-webui|g' "$file" 2>/dev/null || true +done + +# Отключаем проверку обновлений во фронтенде +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.js" -o -name "*.ts" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + -exec grep -l "github.com.*releases\|check.*update\|update.*check" {} \; 2>/dev/null | while read file; do + echo " Отключение проверки обновлений в: $file" + docker exec "${CONTAINER_NAME}" sed -i 's|https://api.github.com/repos/open-webui|// https://api.github.com/repos/open-webui|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|github.com/open-webui|// github.com/open-webui|g' "$file" 2>/dev/null || true +done + +echo "7. Удаление 'Powered by' футеров..." + +# Удаляем футеры из фронтенда +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Powered by\|powered by" "$file" 2>/dev/null; then + echo " Удаление футера из: $file" + docker exec "${CONTAINER_NAME}" sed -i '/Powered by.*[Oo]pen.*[Ww]eb[Uu][Ii]/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/powered by.*[Oo]pen.*[Ww]eb[Uu][Ii]/d' "$file" 2>/dev/null || true + fi +done + +# Удаляем футеры из бэкенда (кроме OAuth) +docker exec "${CONTAINER_NAME}" find /app/backend -type f \( -name "*.py" -o -name "*.html" \) \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + ! -name "*oauth*" ! -name "*oidc*" ! -name "*auth*" ! -name "*login*" \ + ! -path "*/utils/oauth*" ! -path "*/utils/auth*" \ + ! -path "*/open_webui/utils/oauth*" ! -path "*/open_webui/utils/auth*" \ + ! -path "*/open_webui/main.py" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Powered by\|powered by" "$file" 2>/dev/null; then + echo " Удаление футера из: $file" + docker exec "${CONTAINER_NAME}" sed -i '/Powered by.*[Oo]pen.*[Ww]eb[Uu][Ii]/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/powered by.*[Oo]pen.*[Ww]eb[Uu][Ii]/d' "$file" 2>/dev/null || true + fi +done + +echo "" +echo "✓ Бэкенд файлы обработаны (телеметрия отключена)" +echo "✓ OAuth/Authentik файлы защищены" +echo "✓ Фронтенд файлы обработаны" +echo "" + +echo "8. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Не удалось перезапустить контейнер автоматически." + echo "Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== ПОЛНЫЙ ребрендинг завершен! ===" +echo "" +echo "Проверьте:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. Проверьте отсутствие упоминаний 'Open WebUI'" +echo " 3. Проверьте OAuth - должен работать нормально" +echo " 4. Проверьте отсутствие телеметрии (DevTools → Network)" +echo "" +echo "Если OAuth не работает, пересоздайте контейнер:" +echo " sudo docker compose stop open-webui" +echo " sudo docker compose rm -f open-webui" +echo " sudo docker compose up -d open-webui" diff --git a/scripts/rebrand_fast.sh b/scripts/rebrand_fast.sh new file mode 100755 index 0000000..bc3cfb1 --- /dev/null +++ b/scripts/rebrand_fast.sh @@ -0,0 +1,134 @@ +#!/bin/bash +# БЫСТРЫЙ ребрендинг - оптимизированная версия + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== БЫСТРЫЙ ребрендинг Open WebUI для iiEasy ===" +echo "" + +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен." + exit 1 +fi + +echo "1. Замена логотипов..." + +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" +) + +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/logo.png" 2>/dev/null || true + fi + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.ico" 2>/dev/null || true + fi + fi +done + +echo "2. БЫСТРОЕ удаление '(Open WebUI)' - поиск только в текстовых файлах..." + +# ОПТИМИЗАЦИЯ: Сначала находим файлы с текстом через grep -l (быстро) +# Обрабатываем только нужные типы файлов +FILES=$(docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.html" -o -name "*.svelte" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.json" -o -name "*.css" -o -name "*.mjs" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -lE "(Open WebUI)|\(Open WebUI\)|iiEasyWeb.*Open.*WebUI" {} \; 2>/dev/null) + +if [ -z "$FILES" ]; then + echo " Файлов с '(Open WebUI)' не найдено" +else + COUNT=$(echo "$FILES" | wc -l) + echo " Найдено файлов: $COUNT" + echo "$FILES" | while read file; do + if [ -n "$file" ]; then + # Один sed с несколькими командами - быстрее + docker exec "${CONTAINER_NAME}" sed -i \ + -e 's/(Open WebUI)//g' \ + -e 's/\(Open WebUI\)//g' \ + -e 's/ (Open WebUI)//g' \ + -e 's/ \(Open WebUI\)//g' \ + -e 's/iiEasyWeb (Open WebUI)/iiEasyWeb/g' \ + -e 's/iiEasyWeb \(Open WebUI\)/iiEasyWeb/g' \ + -e 's/iiEasyWeb(Open WebUI)/iiEasyWeb/g' \ + -e "s/'(Open WebUI)'//g" \ + -e 's/"(Open WebUI)"//g' \ + -e 's/`(Open WebUI)`//g' \ + -e 's/Войти в iiEasyWeb (Open WebUI)/Войти в iiEasyWeb/g' \ + "$file" 2>/dev/null || true + fi + done + echo " Обработка завершена" +fi + +echo "3. Замена ссылок на документацию..." + +DOC_FILES=$(docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.html" -o -name "*.svelte" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.json" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -lE "docs\.openwebui\.com|open-webui\.com/docs|github\.com/open-webui/docs" {} \; 2>/dev/null) + +if [ -z "$DOC_FILES" ]; then + echo " Файлов со ссылками не найдено" +else + echo "$DOC_FILES" | while read file; do + if [ -n "$file" ]; then + docker exec "${CONTAINER_NAME}" sed -i \ + -e 's|https://docs.openwebui.com|https://note.iieasy.ru|g' \ + -e 's|https://open-webui.com/docs|https://note.iieasy.ru|g' \ + -e 's|https://github.com/open-webui/docs|https://note.iieasy.ru|g' \ + -e 's|docs\.openwebui\.com|note.iieasy.ru|g' \ + -e 's|open-webui\.com/docs|note.iieasy.ru|g' \ + "$file" 2>/dev/null || true + fi + done +fi + +echo "4. Удаление социальных сетей и лицензии..." + +SOCIAL_FILES=$(docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.html" -o -name "*.svelte" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -lE "discord|twitter|x\.com|Github Repo|лицензионный|License" {} \; 2>/dev/null) + +if [ -z "$SOCIAL_FILES" ]; then + echo " Файлов с соцсетями не найдено" +else + echo "$SOCIAL_FILES" | while read file; do + if [ -n "$file" ]; then + docker exec "${CONTAINER_NAME}" sed -i \ + -e '/discord\.gg/d' -e '/discord\.com/d' -e '/Discord/d' \ + -e '/twitter\.com/d' -e '/x\.com/d' -e '/X (formerly Twitter)/d' -e '/Twitter/d' -e '/Follow/d' \ + -e '/Github Repo/d' -e '/GitHub Repo/d' \ + -e '/лицензионный тарифный план/d' -e '/Перейдите на лицензионный/d' \ + -e '/расширенные возможности/d' -e '/настраиваемую тематику/d' \ + -e '/фирменный стиль/d' -e '/специальную поддержку/d' -e '/Лицензия/d' \ + "$file" 2>/dev/null || true + fi + done +fi + +echo "" +echo "5. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== БЫСТРЫЙ ребрендинг завершен! ===" +echo "" +echo "Очистите кеш браузера (Ctrl+Shift+Delete)" diff --git a/scripts/rebrand_final.sh b/scripts/rebrand_final.sh new file mode 100755 index 0000000..8ad70af --- /dev/null +++ b/scripts/rebrand_final.sh @@ -0,0 +1,212 @@ +#!/bin/bash +# ФИНАЛЬНЫЙ ребрендинг - удаляет ВСЕ упоминания и меняет ссылки на документацию + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== ФИНАЛЬНЫЙ ребрендинг Open WebUI для iiEasy ===" +echo "✓ Удаление ВСЕХ упоминаний '(Open WebUI)'" +echo "✓ Замена ссылок на документацию на note.iieasy.ru" +echo "" + +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен." + exit 1 +fi + +echo "1. Замена логотипов..." + +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" + "/app/backend/static" +) + +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/logo.png" 2>/dev/null || true + fi + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.ico" 2>/dev/null || true + fi + fi +done + +# Заменяем все существующие логотипы +EXISTING_LOGOS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "logo.png" -o -name "logo.svg" \) 2>/dev/null) +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_LOGOS" | while read -r logo_file; do + if [ -n "$logo_file" ] && [[ ! "$logo_file" == *"node_modules"* ]]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + done +fi + +echo "2. АГРЕССИВНОЕ удаление '(Open WebUI)' из ВСЕХ файлов..." + +# Ищем ВО ВСЕХ файлах, включая бинарные (может быть в строковых константах) +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + ! -name "*.pyc" ! -name "*.pyo" ! -name "*.so" \ + 2>/dev/null | while read file; do + # Проверяем, это текстовый файл + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "(Open WebUI)|\(Open WebUI\)|iiEasyWeb.*Open.*WebUI" "$file" 2>/dev/null; then + echo " Удаление из: $file" + # Все возможные варианты + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/\(Open WebUI\)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/ (Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/ \(Open WebUI\)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb (Open WebUI)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb \(Open WebUI\)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb(Open WebUI)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i "s/'(Open WebUI)'//g" "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/"(Open WebUI)"//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/`(Open WebUI)`//g' "$file" 2>/dev/null || true + # Удаляем если в составе строки + docker exec "${CONTAINER_NAME}" sed -i 's/Войти в iiEasyWeb (Open WebUI)/Войти в iiEasyWeb/g' "$file" 2>/dev/null || true + fi + fi +done + +echo "3. Замена ссылок на документацию на note.iieasy.ru..." + +# Заменяем ссылки на документацию +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "docs\.openwebui\.com|open-webui\.com/docs|github\.com/open-webui/docs" "$file" 2>/dev/null; then + echo " Замена ссылок на документацию в: $file" + docker exec "${CONTAINER_NAME}" sed -i 's|https://docs.openwebui.com|https://note.iieasy.ru|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|https://open-webui.com/docs|https://note.iieasy.ru|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|https://github.com/open-webui/docs|https://note.iieasy.ru|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|docs\.openwebui\.com|note.iieasy.ru|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|open-webui\.com/docs|note.iieasy.ru|g' "$file" 2>/dev/null || true + fi + fi +done + +echo "4. Замена 'Open WebUI' на 'iiEasyWeb' в тексте..." + +docker exec "${CONTAINER_NAME}" find /app/web -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -q "Open WebUI" "$file" 2>/dev/null; then + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + fi + fi +done + +echo "5. Проверка базы данных..." + +# Пытаемся найти и исправить в базе данных +DB_PATH="/app/backend/data/webui.db" +if docker exec "${CONTAINER_NAME}" test -f "$DB_PATH" 2>/dev/null; then + echo " Найдена база данных, проверяем..." + if docker exec "${CONTAINER_NAME}" command -v sqlite3 >/dev/null 2>&1; then + # Ищем таблицы с текстовыми полями + TABLES=$(docker exec "${CONTAINER_NAME}" sqlite3 "$DB_PATH" "SELECT name FROM sqlite_master WHERE type='table';" 2>/dev/null) + echo "$TABLES" | while read table; do + if [ -n "$table" ]; then + # Пытаемся найти и заменить в текстовых полях + docker exec "${CONTAINER_NAME}" sqlite3 "$DB_PATH" "UPDATE $table SET value = REPLACE(value, '(Open WebUI)', '') WHERE value LIKE '%(Open WebUI)%';" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sqlite3 "$DB_PATH" "UPDATE $table SET value = REPLACE(value, 'Open WebUI', 'iiEasyWeb') WHERE value LIKE '%Open WebUI%';" 2>/dev/null || true + fi + done + fi +fi + +echo "6. Удаление социальных сетей, GitHub и лицензии из футера..." + +# Удаляем Discord +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "discord\.gg|discord\.com|Discord" "$file" 2>/dev/null; then + docker exec "${CONTAINER_NAME}" sed -i '/discord\.gg/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/discord\.com/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Discord/d' "$file" 2>/dev/null || true + fi + fi +done + +# Удаляем Twitter/X +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "twitter\.com|x\.com|X \(formerly Twitter\)|Twitter|Follow" "$file" 2>/dev/null; then + docker exec "${CONTAINER_NAME}" sed -i '/twitter\.com/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/x\.com/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/X (formerly Twitter)/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Twitter/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Follow/d' "$file" 2>/dev/null || true + fi + fi +done + +# Удаляем GitHub Repo +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "Github Repo|GitHub Repo" "$file" 2>/dev/null; then + docker exec "${CONTAINER_NAME}" sed -i '/Github Repo/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/GitHub Repo/d' "$file" 2>/dev/null || true + fi + fi +done + +# Удаляем лицензию +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "лицензионный тарифный план|Перейдите на лицензионный|Лицензия" "$file" 2>/dev/null; then + docker exec "${CONTAINER_NAME}" sed -i '/лицензионный тарифный план/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Перейдите на лицензионный/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/расширенные возможности/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/настраиваемую тематику/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/фирменный стиль/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/специальную поддержку/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Лицензия/d' "$file" 2>/dev/null || true + fi + fi +done + +echo "" +echo "7. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== ФИНАЛЬНЫЙ ребрендинг завершен! ===" +echo "" +echo "Проверьте:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. Должно быть 'Войти в iiEasyWeb' (без '(Open WebUI)')" +echo " 3. Ссылки на документацию должны вести на note.iieasy.ru" +echo "" +echo "ВАЖНО: Очистите кеш браузера (Ctrl+Shift+Delete)" +echo "" +echo "Если '(Open WebUI)' все еще видно, проверьте через Admin Panel:" +echo " Settings → Appearance → Site Title" diff --git a/scripts/rebrand_full.sh b/scripts/rebrand_full.sh new file mode 100755 index 0000000..b9d3cab --- /dev/null +++ b/scripts/rebrand_full.sh @@ -0,0 +1,209 @@ +#!/bin/bash +# ПОЛНЫЙ ребрендинг Open WebUI для iiEasy +# Удаляет ВСЕ упоминания Open WebUI из интерфейса, но защищает OAuth + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== ПОЛНЫЙ ребрендинг Open WebUI для iiEasy ===" +echo "⚠ Удаление ВСЕХ упоминаний Open WebUI из интерфейса" +echo "⚠ OAuth/Authentik файлы защищены" +echo "" + +# Проверка наличия контейнера +if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не найден. Запустите docker-compose up -d сначала." + exit 1 +fi + +# Проверка, что контейнер запущен +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен. Запустите docker-compose up -d." + exit 1 +fi + +echo "1. Замена логотипов и favicon..." + +# Определяем пути для статических файлов +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" + "/app/backend/static" + "/app/static" + "/app/public" +) + +STATIC_DIR="" +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + STATIC_DIR="$dir" + echo " Найдена директория: $STATIC_DIR" + break + fi +done + +if [ -z "$STATIC_DIR" ]; then + FAVICON_PATH=$(docker exec "${CONTAINER_NAME}" find /app -name "favicon.png" -o -name "favicon.ico" 2>/dev/null | head -1) + if [ -n "$FAVICON_PATH" ]; then + STATIC_DIR=$(dirname "$FAVICON_PATH") + else + STATIC_DIR="/app/web/build/_app/immutable" + docker exec "${CONTAINER_NAME}" mkdir -p "$STATIC_DIR" 2>/dev/null || true + fi +fi + +# Копирование логотипов +for target_dir in "$STATIC_DIR" "/app/web/build/_app/immutable" "/app/web/static" "/app/web/build"; do + if docker exec "${CONTAINER_NAME}" test -d "$target_dir" 2>/dev/null || [ "$target_dir" = "$STATIC_DIR" ]; then + echo " Копирование в $target_dir..." + + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${target_dir}/logo.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${target_dir}/logo.svg" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/logo-light.svg" ]; then + docker cp "$MEDIA_DIR/logo-light.svg" "${CONTAINER_NAME}:${target_dir}/logo.svg" 2>/dev/null || true + fi + + if [ -f "$MEDIA_DIR/logo-dark.svg" ]; then + docker cp "$MEDIA_DIR/logo-dark.svg" "${CONTAINER_NAME}:${target_dir}/logo-dark.svg" 2>/dev/null || true + fi + + if [ -f "$MEDIA_DIR/favicon.ico" ]; then + docker cp "$MEDIA_DIR/favicon.ico" "${CONTAINER_NAME}:${target_dir}/favicon.ico" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon.ico" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon-96x96.png" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/favicon.svg" ]; then + docker cp "$MEDIA_DIR/favicon.svg" "${CONTAINER_NAME}:${target_dir}/favicon.svg" 2>/dev/null || true + fi + fi +done + +# Замена существующих favicon и logo +EXISTING_FAVICONS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "favicon.svg" \) 2>/dev/null | head -10) +EXISTING_LOGOS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "logo.png" -o -name "logo.svg" \) 2>/dev/null | head -10) + +if [ -f "$MEDIA_DIR/favicon.png" ]; then + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + favicon_dir=$(dirname "$favicon_file") + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_dir}/favicon.ico" 2>/dev/null || true + fi + done +fi + +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_LOGOS" | while read -r logo_file; do + if [ -n "$logo_file" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + done +fi + +# Копируем в public директории +PUBLIC_DIRS=("/app/web/public" "/app/public" "/app/backend/public") +for pub_dir in "${PUBLIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$pub_dir" 2>/dev/null; then + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${pub_dir}/logo.png" 2>/dev/null || true + fi + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${pub_dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${pub_dir}/favicon.ico" 2>/dev/null || true + fi + break + fi +done + +echo "2. Удаление упоминаний 'Open WebUI' из фронтенда (HTML/JS/TSX/Svelte)..." + +# КРИТИЧНО: Изменяем ТОЛЬКО фронтенд файлы (веб-интерфейс) +# НЕ трогаем Python бэкенд файлы вообще, кроме исключений для безопасности +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.svelte" -o -name "*.json" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + # Проверяем, содержит ли файл упоминания Open WebUI + if docker exec "${CONTAINER_NAME}" grep -q "Open WebUI\|open-webui\|openwebui\|\(Open WebUI\)" "$file" 2>/dev/null; then + echo " Обработка фронтенд: $file" + # Замена "Open WebUI" на "iiEasyWeb" + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + # Удаление "(Open WebUI)" в скобках + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/\(Open WebUI\)//g' "$file" 2>/dev/null || true + # Замена "iiEasyWeb (Open WebUI)" на "iiEasyWeb" + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb (Open WebUI)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb \(Open WebUI\)/iiEasyWeb/g' "$file" 2>/dev/null || true + # Удаление "open-webui" и "openwebui" в тексте (но не в URL - они обычно в кавычках или переменных) + docker exec "${CONTAINER_NAME}" sed -i 's/open-webui/iieasyweb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/openwebui/iieasyweb/g' "$file" 2>/dev/null || true + fi +done + +echo "3. Удаление упоминаний из статических JSON файлов конфигурации..." + +# Изменяем только JSON файлы конфигурации фронтенда (не бэкенд) +docker exec "${CONTAINER_NAME}" find /app/web -type f -name "*.json" \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Open WebUI\|open-webui\|openwebui" "$file" 2>/dev/null; then + echo " Обработка JSON: $file" + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/open-webui/iieasyweb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/openwebui/iieasyweb/g' "$file" 2>/dev/null || true + fi +done + +echo "4. Удаление 'Powered by' футеров из фронтенда..." + +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Powered by\|powered by" "$file" 2>/dev/null; then + echo " Удаление футера из: $file" + docker exec "${CONTAINER_NAME}" sed -i '/Powered by.*[Oo]pen.*[Ww]eb[Uu][Ii]/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/powered by.*[Oo]pen.*[Ww]eb[Uu][Ii]/d' "$file" 2>/dev/null || true + fi +done + +echo "5. Отключение проверки обновлений (только фронтенд)..." + +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.js" -o -name "*.ts" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + -exec grep -l "github.com.*releases\|check.*update\|update.*check" {} \; 2>/dev/null | while read file; do + echo " Отключение проверки обновлений в: $file" + docker exec "${CONTAINER_NAME}" sed -i 's|https://api.github.com/repos/open-webui|# https://api.github.com/repos/open-webui|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|github.com/open-webui|# github.com/open-webui|g' "$file" 2>/dev/null || true +done + +echo "" +echo "⚠ ВАЖНО: Python бэкенд файлы НЕ изменялись" +echo "⚠ OAuth/Authentik файлы защищены" +echo "⚠ Изменены только фронтенд файлы (веб-интерфейс)" +echo "" + +echo "6. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Не удалось перезапустить контейнер автоматически." + echo "Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== ПОЛНЫЙ ребрендинг завершен! ===" +echo "" +echo "Проверьте:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. Проверьте отсутствие упоминаний 'Open WebUI'" +echo " 3. Проверьте OAuth - должен работать нормально" +echo "" +echo "Если OAuth не работает, пересоздайте контейнер:" +echo " sudo docker compose stop open-webui" +echo " sudo docker compose rm -f open-webui" +echo " sudo docker compose up -d open-webui" diff --git a/scripts/rebrand_precise.sh b/scripts/rebrand_precise.sh new file mode 100755 index 0000000..69b81ba --- /dev/null +++ b/scripts/rebrand_precise.sh @@ -0,0 +1,173 @@ +#!/bin/bash +# ТОЧНЫЙ ребрендинг Open WebUI для iiEasy +# Находит и удаляет ВСЕ упоминания "(Open WebUI)" и "Open WebUI" +# Только безопасные замены в текстовом контенте + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== ТОЧНЫЙ ребрендинг Open WebUI для iiEasy ===" +echo "✓ Поиск и удаление ВСЕХ упоминаний '(Open WebUI)'" +echo "✓ Замена 'Open WebUI' на 'iiEasyWeb'" +echo "✓ Исправление логотипов" +echo "" + +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен." + exit 1 +fi + +echo "1. Замена логотипов и favicon..." + +# Находим все места, где могут быть логотипы +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" + "/app/backend/static" +) + +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + echo " Копирование в $dir..." + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/logo.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/logo.svg" 2>/dev/null || true + fi + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.ico" 2>/dev/null || true + fi + fi +done + +# Заменяем существующие логотипы везде +echo " Поиск и замена существующих логотипов..." +EXISTING_LOGOS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "logo.png" -o -name "logo.svg" -o -name "logo.ico" \) 2>/dev/null) +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_LOGOS" | while read -r logo_file; do + if [ -n "$logo_file" ] && [[ ! "$logo_file" == *"node_modules"* ]]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + done +fi + +EXISTING_FAVICONS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "favicon.svg" \) 2>/dev/null) +if [ -f "$MEDIA_DIR/favicon.png" ]; then + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ] && [[ ! "$favicon_file" == *"node_modules"* ]]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + fi + done +fi + +echo "2. Поиск и удаление '(Open WebUI)' из ВСЕХ файлов..." + +# Ищем ВСЕ файлы с упоминанием "(Open WebUI)" - включая скомпилированные +# Ищем в разных вариантах написания +docker exec "${CONTAINER_NAME}" find /app -type f \( -name "*.html" -o -name "*.svelte" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" -o -name "*.json" -o -name "*.css" -o -name "*.mjs" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + # Проверяем разные варианты написания - включая с пробелами и без + if docker exec "${CONTAINER_NAME}" grep -qE "(Open WebUI)|\(Open WebUI\)|iiEasyWeb \(Open WebUI\)|iiEasyWeb\(Open WebUI\)" "$file" 2>/dev/null; then + echo " Удаление '(Open WebUI)' из: $file" + # Удаляем различные варианты написания в скобках + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/\(Open WebUI\)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/ (Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/ \(Open WebUI\)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb (Open WebUI)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb \(Open WebUI\)/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb(Open WebUI)/iiEasyWeb/g' "$file" 2>/dev/null || true + # Также удаляем если есть пробелы вокруг + docker exec "${CONTAINER_NAME}" sed -i 's/\s*(Open WebUI)\s*//g' "$file" 2>/dev/null || true + # Удаляем если текст в кавычках или переменных + docker exec "${CONTAINER_NAME}" sed -i "s/'(Open WebUI)'//g" "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/"(Open WebUI)"//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/`(Open WebUI)`//g' "$file" 2>/dev/null || true + fi +done + +echo "3. Замена 'Open WebUI' на 'iiEasyWeb' в тексте..." + +# Заменяем "Open WebUI" на "iiEasyWeb" в текстовом контенте +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.html" -o -name "*.svelte" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Open WebUI" "$file" 2>/dev/null; then + echo " Замена в: $file" + # Заменяем только в текстовом контенте, не в коде + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + fi +done + +echo "4. Удаление 'Powered by Open WebUI'..." + +docker exec "${CONTAINER_NAME}" find /app -type f \( -name "*.html" -o -name "*.svelte" -o -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/dist/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" grep -q "Powered by.*Open WebUI\|powered by.*Open WebUI" "$file" 2>/dev/null; then + echo " Удаление футера из: $file" + docker exec "${CONTAINER_NAME}" sed -i '/Powered by.*Open WebUI/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/powered by.*Open WebUI/d' "$file" 2>/dev/null || true + fi +done + +echo "5. Поиск упоминаний в скомпилированных файлах (включая минифицированные)..." + +# Ищем в скомпилированных JS файлах (могут быть минифицированы) +# Также ищем в build директориях +docker exec "${CONTAINER_NAME}" find /app/web/build -type f \( -name "*.js" -o -name "*.mjs" \) \ + ! -path "*/node_modules/*" \ + 2>/dev/null | while read file; do + # Ищем разные варианты, включая минифицированные (без пробелов) + if docker exec "${CONTAINER_NAME}" grep -qE "Open WebUI|\(Open WebUI\)|iiEasyWeb \(Open WebUI\)|OpenWebUI" "$file" 2>/dev/null; then + echo " Обработка скомпилированного: $file" + docker exec "${CONTAINER_NAME}" sed -i 's/Open WebUI/iiEasyWeb/g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/(Open WebUI)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/\(Open WebUI\)//g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's/iiEasyWeb (Open WebUI)/iiEasyWeb/g' "$file" 2>/dev/null || true + fi +done + +echo "6. Поиск в базе данных (если текст там хранится)..." + +# Проверяем, может ли текст быть в базе данных +# Если есть SQLite база, можем попробовать заменить там +DB_PATH="/app/backend/data/webui.db" +if docker exec "${CONTAINER_NAME}" test -f "$DB_PATH" 2>/dev/null; then + echo " Найдена база данных, проверяем наличие '(Open WebUI)'..." + # Ищем в базе через sqlite3 (если доступен) + if docker exec "${CONTAINER_NAME}" command -v sqlite3 >/dev/null 2>&1; then + # Ищем в текстовых полях базы + docker exec "${CONTAINER_NAME}" sqlite3 "$DB_PATH" "SELECT name FROM sqlite_master WHERE type='table';" 2>/dev/null | while read table; do + if [ -n "$table" ]; then + # Пытаемся найти и заменить в текстовых полях (осторожно!) + echo " Проверка таблицы: $table" + fi + done + fi +fi + +echo "" +echo "6. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== ТОЧНЫЙ ребрендинг завершен! ===" +echo "" +echo "Проверьте:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. Должно быть 'Войти в iiEasyWeb' (без '(Open WebUI)')" +echo " 3. Логотип должен отображаться правильно" +echo "" +echo "Если '(Open WebUI)' все еще видно, очистите кеш браузера (Ctrl+Shift+Delete)" diff --git a/scripts/rebrand_safe.sh b/scripts/rebrand_safe.sh new file mode 100755 index 0000000..4877a0b --- /dev/null +++ b/scripts/rebrand_safe.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# БЕЗОПАСНЫЙ скрипт ребрендинга Open WebUI для iiEasy +# Только логотипы и favicon, БЕЗ изменения кода Python/JS + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== БЕЗОПАСНЫЙ ребрендинг Open WebUI для iiEasy ===" +echo "⚠ ВНИМАНИЕ: Этот скрипт изменяет ТОЛЬКО логотипы и favicon" +echo "⚠ Код Python/JS НЕ изменяется для защиты OAuth/Authentik" +echo "" + +# Проверка наличия контейнера +if ! docker ps -a --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не найден. Запустите docker-compose up -d сначала." + exit 1 +fi + +# Проверка, что контейнер запущен +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен. Запустите docker-compose up -d." + exit 1 +fi + +echo "1. Замена логотипов и favicon..." + +# Определяем пути для статических файлов в Open WebUI +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" + "/app/backend/static" + "/app/static" + "/app/public" +) + +# Находим существующую директорию со статическими файлами +STATIC_DIR="" +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + STATIC_DIR="$dir" + echo " Найдена директория статических файлов: $STATIC_DIR" + break + fi +done + +if [ -z "$STATIC_DIR" ]; then + echo " Предупреждение: Директория статических файлов не найдена, пробуем найти через поиск favicon..." + FAVICON_PATH=$(docker exec "${CONTAINER_NAME}" find /app -name "favicon.png" -o -name "favicon.ico" 2>/dev/null | head -1) + if [ -n "$FAVICON_PATH" ]; then + STATIC_DIR=$(dirname "$FAVICON_PATH") + echo " Найдена директория через поиск favicon: $STATIC_DIR" + else + STATIC_DIR="/app/web/build/_app/immutable" + echo " Используем стандартный путь: $STATIC_DIR" + docker exec "${CONTAINER_NAME}" mkdir -p "$STATIC_DIR" 2>/dev/null || true + fi +fi + +# Копирование логотипов (приоритет: PNG > SVG) +for target_dir in "$STATIC_DIR" "/app/web/build/_app/immutable" "/app/web/static" "/app/web/build"; do + if docker exec "${CONTAINER_NAME}" test -d "$target_dir" 2>/dev/null || [ "$target_dir" = "$STATIC_DIR" ]; then + echo " Копирование в $target_dir..." + + # Логотипы + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${target_dir}/logo.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${target_dir}/logo.svg" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/logo-light.svg" ]; then + docker cp "$MEDIA_DIR/logo-light.svg" "${CONTAINER_NAME}:${target_dir}/logo.svg" 2>/dev/null || true + fi + + if [ -f "$MEDIA_DIR/logo-dark.svg" ]; then + docker cp "$MEDIA_DIR/logo-dark.svg" "${CONTAINER_NAME}:${target_dir}/logo-dark.svg" 2>/dev/null || true + fi + + # Favicon + if [ -f "$MEDIA_DIR/favicon.ico" ]; then + docker cp "$MEDIA_DIR/favicon.ico" "${CONTAINER_NAME}:${target_dir}/favicon.ico" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon.ico" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${target_dir}/favicon-96x96.png" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/favicon.svg" ]; then + docker cp "$MEDIA_DIR/favicon.svg" "${CONTAINER_NAME}:${target_dir}/favicon.svg" 2>/dev/null || true + fi + fi +done + +# Поиск и замена существующих favicon и logo файлов +echo " Поиск существующих favicon и logo файлов для замены..." +EXISTING_FAVICONS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "favicon.svg" \) 2>/dev/null | head -10) +EXISTING_LOGOS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "logo.png" -o -name "logo.svg" \) 2>/dev/null | head -10) + +# Заменяем существующие favicon файлы +if [ -f "$MEDIA_DIR/favicon.png" ]; then + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ]; then + echo " Замена: $favicon_file" + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + favicon_dir=$(dirname "$favicon_file") + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_dir}/favicon.ico" 2>/dev/null || true + fi + done +fi + +# Заменяем существующие logo файлы +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_LOGOS" | while read -r logo_file; do + if [ -n "$logo_file" ]; then + echo " Замена: $logo_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + done +fi + +# Копируем в public директории +PUBLIC_DIRS=( + "/app/web/public" + "/app/public" + "/app/backend/public" +) + +for pub_dir in "${PUBLIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$pub_dir" 2>/dev/null; then + echo " Копирование в $pub_dir..." + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${pub_dir}/logo.png" 2>/dev/null || true + fi + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${pub_dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${pub_dir}/favicon.ico" 2>/dev/null || true + fi + break + fi +done + +echo "" +echo "=== БЕЗОПАСНЫЙ ребрендинг завершен! ===" +echo "" +echo "⚠ Изменены ТОЛЬКО логотипы и favicon" +echo "⚠ Код Python/JS НЕ изменялся - OAuth/Authentik защищены" +echo "" +echo "Рекомендуется использовать Admin Panel для постоянных изменений:" +echo " Settings → Appearance → Logo (загрузите файлы из /app/media/)" +echo "" +echo "Если нужно изменить текст интерфейса, используйте Admin Panel или" +echo "настройте переменные окружения в docker-compose.yml" diff --git a/scripts/rebrand_safe_final.sh b/scripts/rebrand_safe_final.sh new file mode 100755 index 0000000..9babbab --- /dev/null +++ b/scripts/rebrand_safe_final.sh @@ -0,0 +1,460 @@ +#!/bin/bash +# БЕЗОПАСНЫЙ финальный ребрендинг - не ломает функциональность + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== БЕЗОПАСНЫЙ финальный ребрендинг Open WebUI для iiEasy ===" +echo "" + +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен." + exit 1 +fi + +echo "1. Замена логотипов, splash.png и favicon..." + +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" + "/app/backend/static" + "/app/static" + "/app/web/public" + "/app/public" +) + +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + # Основной логотип + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/logo.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/splash.png" 2>/dev/null || true + # Также создаем splash-dark.png для темной темы + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/splash-dark.png" 2>/dev/null || true + fi + # Логотип для светлой темы + if [ -f "$MEDIA_DIR/logo-light.svg" ]; then + docker cp "$MEDIA_DIR/logo-light.svg" "${CONTAINER_NAME}:${dir}/logo-light.svg" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo-light.svg" "${CONTAINER_NAME}:${dir}/logo.svg" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/splash-light.png" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/logo.png" ]; then + # Если нет SVG, используем PNG для светлой темы + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/logo-light.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/splash-light.png" 2>/dev/null || true + fi + # Логотип для темной темы + if [ -f "$MEDIA_DIR/logo-dark.svg" ]; then + docker cp "$MEDIA_DIR/logo-dark.svg" "${CONTAINER_NAME}:${dir}/logo-dark.svg" 2>/dev/null || true + # Для splash-dark.png используем PNG версию (если есть logo-dark.png) или обычный logo.png + if [ -f "$MEDIA_DIR/logo-dark.png" ]; then + docker cp "$MEDIA_DIR/logo-dark.png" "${CONTAINER_NAME}:${dir}/splash-dark.png" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/splash-dark.png" 2>/dev/null || true + fi + elif [ -f "$MEDIA_DIR/logo.png" ]; then + # Если нет темного SVG, используем PNG для темной темы + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/logo-dark.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/splash-dark.png" 2>/dev/null || true + fi + # Favicon + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.ico" 2>/dev/null || true + # Также создаем favicon-dark.png и favicon-light.png + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon-dark.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon-light.png" 2>/dev/null || true + # Apple touch icon для iOS + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/apple-touch-icon.png" 2>/dev/null || true + elif [ -f "$MEDIA_DIR/logo.png" ]; then + # Если нет отдельного favicon, используем logo.png + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/favicon.ico" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/favicon-dark.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/favicon-light.png" 2>/dev/null || true + # Apple touch icon для iOS + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/apple-touch-icon.png" 2>/dev/null || true + fi + fi +done + +# Заменяем ВСЕ существующие favicon файлы (включая favicon.ico, favicon-dark.png, favicon-light.png, apple-touch-icon.png) +echo " Замена всех favicon файлов..." +EXISTING_FAVICONS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "favicon.png" -o -name "favicon.ico" -o -name "favicon.svg" -o -name "favicon-dark.png" -o -name "favicon-light.png" -o -name "apple-touch-icon.png" \) 2>/dev/null) +if [ -f "$MEDIA_DIR/favicon.png" ]; then + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ] && [[ ! "$favicon_file" == *"node_modules"* ]]; then + favicon_name=$(basename "$favicon_file") + echo " Замена favicon: $favicon_file" + # Для темной темы используем logo-dark если есть, иначе favicon.png + if [[ "$favicon_name" == *"dark"* ]]; then + if [ -f "$MEDIA_DIR/logo-dark.svg" ] || [ -f "$MEDIA_DIR/logo-dark.png" ]; then + # Используем logo.png для favicon-dark (так как favicon должен быть PNG) + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + else + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + fi + # Для светлой темы + elif [[ "$favicon_name" == *"light"* ]]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + # Apple touch icon или обычный favicon + else + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + # Также создаем .ico версию в той же директории для обычных favicon + if [[ "$favicon_name" == "favicon.png" ]]; then + favicon_dir=$(dirname "$favicon_file") + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${favicon_dir}/favicon.ico" 2>/dev/null || true + fi + fi + fi + done +elif [ -f "$MEDIA_DIR/logo.png" ]; then + # Если нет favicon.png, используем logo.png + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ] && [[ ! "$favicon_file" == *"node_modules"* ]]; then + favicon_name=$(basename "$favicon_file") + echo " Замена favicon: $favicon_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + # Также создаем .ico версию для обычных favicon (не для dark/light/apple-touch-icon) + if [[ "$favicon_name" == "favicon.png" ]]; then + favicon_dir=$(dirname "$favicon_file") + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${favicon_dir}/favicon.ico" 2>/dev/null || true + fi + fi + done +fi + +# Заменяем все существующие splash.png и splash-dark.png +EXISTING_SPLASH=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "splash.png" -o -name "splash-dark.png" -o -name "splash-light.png" \) 2>/dev/null) +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_SPLASH" | while read -r splash_file; do + if [ -n "$splash_file" ] && [[ ! "$splash_file" == *"node_modules"* ]]; then + splash_name=$(basename "$splash_file") + # Для темной темы используем logo-dark если есть, иначе обычный logo + if [[ "$splash_name" == *"dark"* ]]; then + if [ -f "$MEDIA_DIR/logo-dark.svg" ]; then + # Конвертируем SVG в PNG или используем logo.png + echo " Замена splash-dark.png: $splash_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${splash_file}" 2>/dev/null || true + else + echo " Замена splash-dark.png: $splash_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${splash_file}" 2>/dev/null || true + fi + # Для светлой темы + elif [[ "$splash_name" == *"light"* ]]; then + if [ -f "$MEDIA_DIR/logo-light.svg" ]; then + echo " Замена splash-light.png: $splash_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${splash_file}" 2>/dev/null || true + else + echo " Замена splash-light.png: $splash_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${splash_file}" 2>/dev/null || true + fi + # Обычный splash + else + echo " Замена splash.png: $splash_file" + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${splash_file}" 2>/dev/null || true + fi + fi + done +fi + +# Заменяем все существующие logo файлы (включая logo-dark и logo-light) +EXISTING_LOGOS=$(docker exec "${CONTAINER_NAME}" find /app -type f \( -name "logo.png" -o -name "logo.svg" -o -name "logo-light.*" -o -name "logo-dark.*" \) 2>/dev/null) +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_LOGOS" | while read -r logo_file; do + if [ -n "$logo_file" ] && [[ ! "$logo_file" == *"node_modules"* ]]; then + logo_name=$(basename "$logo_file") + # Для темной темы используем logo-dark если есть, иначе обычный logo + if [[ "$logo_name" == *"dark"* ]]; then + if [ -f "$MEDIA_DIR/logo-dark.svg" ]; then + docker cp "$MEDIA_DIR/logo-dark.svg" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + else + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + # Для светлой темы используем logo-light если есть + elif [[ "$logo_name" == *"light"* ]]; then + if [ -f "$MEDIA_DIR/logo-light.svg" ]; then + docker cp "$MEDIA_DIR/logo-light.svg" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + else + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + # Обычный логотип + else + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${logo_file}" 2>/dev/null || true + fi + fi + done +fi + +echo "2. БЕЗОПАСНОЕ удаление '(Open WebUI)' из HTML/Svelte и Python шаблонов..." + +# HTML, Svelte и Python файлы (шаблоны) +FILES=$(docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.html" -o -name "*.svelte" -o -name "*.py" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + ! -name "*test*" ! -name "*__pycache__*" \ + -exec grep -lE "(Open WebUI)|\(Open WebUI\)" {} \; 2>/dev/null) + +if [ -z "$FILES" ]; then + echo " Файлов с '(Open WebUI)' не найдено" +else + COUNT=$(echo "$FILES" | wc -l) + echo " Найдено файлов: $COUNT" + echo "$FILES" | while read file; do + if [ -n "$file" ]; then + # Безопасная замена только в HTML/Svelte + docker exec "${CONTAINER_NAME}" sed -i \ + -e 's/(Open WebUI)//g' \ + -e 's/\(Open WebUI\)//g' \ + -e 's/ (Open WebUI)//g' \ + -e 's/ \(Open WebUI\)//g' \ + -e 's/iiEasyWeb (Open WebUI)/iiEasyWeb/g' \ + -e 's/Войти в iiEasyWeb (Open WebUI)/Войти в iiEasyWeb/g' \ + "$file" 2>/dev/null || true + fi + done +fi + +echo "3. Замена ссылок на документацию (только в HTML/Svelte)..." + +DOC_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f \ + \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -lE "docs\.openwebui\.com|open-webui\.com/docs" {} \; 2>/dev/null) + +if [ -z "$DOC_FILES" ]; then + echo " Файлов со ссылками не найдено" +else + echo "$DOC_FILES" | while read file; do + if [ -n "$file" ]; then + docker exec "${CONTAINER_NAME}" sed -i \ + -e 's|https://docs.openwebui.com|https://note.iieasy.ru|g' \ + -e 's|https://open-webui.com/docs|https://note.iieasy.ru|g' \ + -e 's|docs\.openwebui\.com|note.iieasy.ru|g' \ + "$file" 2>/dev/null || true + fi + done +fi + +echo "4. Исправление favicon.ico, favicon.png, favicon-dark.png и apple-touch-icon.png в HTML/Svelte шаблонах..." + +# Заменяем ссылки на favicon.ico, favicon.png, favicon-dark.png и apple-touch-icon.png на наш логотип +TEMPLATE_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f \ + \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -lE "/static/favicon(-dark|-light)?\.(png|ico)|favicon(-dark|-light)?\.(ico|png)|apple-touch-icon\.png|rel=\"(shortcut )?icon\"|rel=\"apple-touch-icon\"" {} \; 2>/dev/null) + +if [ -n "$TEMPLATE_FILES" ]; then + echo "$TEMPLATE_FILES" | while read file; do + if [ -n "$file" ]; then + # Заменяем favicon.ico, favicon.png, favicon-dark.png и apple-touch-icon.png на logo.png в ссылках + docker exec "${CONTAINER_NAME}" sed -i \ + -e 's|/static/favicon-dark\.png|/static/logo.png|g' \ + -e 's|/static/favicon-light\.png|/static/logo.png|g' \ + -e 's|/static/favicon\.ico|/static/logo.png|g' \ + -e 's|/static/favicon\.png|/static/logo.png|g' \ + -e 's|/static/apple-touch-icon\.png|/static/logo.png|g' \ + -e 's|src="/static/favicon-dark\.png"|src="/static/logo.png"|g' \ + -e 's|src="/static/favicon\.png"|src="/static/logo.png"|g' \ + -e 's|src="/static/apple-touch-icon\.png"|src="/static/logo.png"|g' \ + -e 's|href="[^"]*favicon-dark\.png"|href="/static/logo.png"|g' \ + -e 's|href="[^"]*favicon\.ico"|href="/static/logo.png"|g' \ + -e 's|href="[^"]*favicon\.png"|href="/static/logo.png"|g' \ + -e 's|href="[^"]*apple-touch-icon\.png"|href="/static/logo.png"|g' \ + -e 's|href="https://odo\.iieasy\.ru/static/favicon\.ico"|href="/static/logo.png"|g' \ + "$file" 2>/dev/null || true + fi + done +fi + +echo "4.1. Исправление splash-dark.png, favicon-dark.png и логотипов в окне авторизации..." + +# Ищем файлы, где используется splash-dark.png, favicon-dark.png или логотип в окне авторизации +AUTH_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f \ + \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -lE "splash-dark|splash-dark\.png|favicon-dark\.png|auth.*logo|login.*logo|dark.*splash|dark.*favicon" {} \; 2>/dev/null) + +if [ -n "$AUTH_FILES" ]; then + echo "$AUTH_FILES" | while read file; do + if [ -n "$file" ]; then + echo " Исправление логотипа в окне авторизации: $file" + # Заменяем splash-dark.png и favicon-dark.png на наш логотип + docker exec "${CONTAINER_NAME}" sed -i \ + -e 's|/static/splash-dark\.png|/static/logo.png|g' \ + -e 's|/static/favicon-dark\.png|/static/logo.png|g' \ + -e 's|splash-dark\.png|logo.png|g' \ + -e 's|favicon-dark\.png|logo.png|g' \ + -e 's|src="/static/favicon-dark\.png"|src="/static/logo.png"|g' \ + -e 's|src="[^"]*splash-dark[^"]*"|src="/static/logo.png"|g' \ + -e 's|src="[^"]*favicon-dark[^"]*"|src="/static/logo.png"|g' \ + "$file" 2>/dev/null || true + fi + done +fi + +echo "5. Исправление API endpoint для изображения профиля модели..." + +# Ищем и заменяем API endpoint для изображения профиля модели +API_FILES=$(docker exec "${CONTAINER_NAME}" find /app -type f \ + \( -name "*.html" -o -name "*.svelte" -o -name "*.js" -o -name "*.ts" -o -name "*.py" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -l "model/profile/image\|models.*profile.*image" {} \; 2>/dev/null) + +if [ -n "$API_FILES" ]; then + echo "$API_FILES" | while read file; do + if [ -n "$file" ]; then + echo " Исправление API endpoint в: $file" + # Заменяем API endpoint на статический логотип + docker exec "${CONTAINER_NAME}" sed -i 's|/api/v1/models/model/profile/image|/static/logo.png|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|api/v1/models/model/profile/image|static/logo.png|g' "$file" 2>/dev/null || true + fi + done +fi + +# Также ищем в Python коде, который генерирует этот endpoint +PYTHON_API=$(docker exec "${CONTAINER_NAME}" find /app/backend -type f -name "*.py" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + -exec grep -l "profile.*image\|model.*profile\|def.*profile" {} \; 2>/dev/null) + +if [ -n "$PYTHON_API" ]; then + echo "$PYTHON_API" | while read file; do + if [ -n "$file" ]; then + # Ищем функции, которые возвращают изображение профиля и заменяем на статический логотип + echo " Проверка Python API в: $file" + # Это нужно делать более аккуратно - просто заменим возвращаемый путь + docker exec "${CONTAINER_NAME}" sed -i 's|/static/favicon.png|/static/logo.png|g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|favicon.png|logo.png|g' "$file" 2>/dev/null || true + fi + done +fi + +echo "6. Удаление проверки обновлений и ссылок на GitHub (только Svelte файлы)..." + +# Ищем ТОЛЬКО в исходных Svelte файлах - не трогаем скомпилированные JS +UPDATE_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f -name "*.svelte" \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -lE "Проверить обновления|Check for updates|github.com/open-webui/releases|последняя|latest" {} \; 2>/dev/null) + +if [ -z "$UPDATE_FILES" ]; then + echo " Файлов с проверкой обновлений не найдено" +else + COUNT=$(echo "$UPDATE_FILES" | wc -l) + echo " Найдено Svelte файлов: $COUNT" + echo "$UPDATE_FILES" | while read file; do + if [ -n "$file" ]; then + echo " Удаление проверки обновлений из: $file" + # Удаляем кнопку "Проверить обновления" + docker exec "${CONTAINER_NAME}" sed -i '/Проверить обновления/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Check for updates/d' "$file" 2>/dev/null || true + # Удаляем ссылку на GitHub releases с текстом "(последняя)" + docker exec "${CONTAINER_NAME}" sed -i 's|]*href="https://github.com/open-webui/open-webui/releases/tag/[^"]*"[^>]*>(последняя)
||g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|(последняя)||g' "$file" 2>/dev/null || true + # Удаляем "Посмотреть, что нового" + docker exec "${CONTAINER_NAME}" sed -i '/Посмотреть, что нового/d' "$file" 2>/dev/null || true + fi + done +fi + +echo "7. Удаление социальных сетей, GitHub и блока 'Помощь' (только Svelte файлы)..." + +# Ищем ТОЛЬКО в исходных Svelte файлах +SOCIAL_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f -name "*.svelte" \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -lE "discord|twitter|x\.com|Github Repo|github.com/open-webui|Помощь|Help|обратитесь за поддержкой" {} \; 2>/dev/null) + +if [ -z "$SOCIAL_FILES" ]; then + echo " Файлов с соцсетями не найдено" +else + COUNT=$(echo "$SOCIAL_FILES" | wc -l) + echo " Найдено Svelte файлов: $COUNT" + echo "$SOCIAL_FILES" | while read file; do + if [ -n "$file" ]; then + echo " Удаление соцсетей из: $file" + # Удаляем ссылки на Discord + docker exec "${CONTAINER_NAME}" sed -i 's|]*href="https://discord.gg/[^"]*"[^>]*>.*||g' "$file" 2>/dev/null || true + # Удаляем ссылки на Twitter/X + docker exec "${CONTAINER_NAME}" sed -i 's|]*href="https://twitter.com/[^"]*"[^>]*>.*||g' "$file" 2>/dev/null || true + # Удаляем ссылки на GitHub repo + docker exec "${CONTAINER_NAME}" sed -i 's|]*href="https://github.com/open-webui/open-webui"[^>]*>.*||g' "$file" 2>/dev/null || true + # Удаляем badges (img.shields.io) + docker exec "${CONTAINER_NAME}" sed -i 's|]*shields.io[^>]*>||g' "$file" 2>/dev/null || true + # Удаляем текст блока "Помощь" + docker exec "${CONTAINER_NAME}" sed -i '/Узнайте, как использовать/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/обратитесь за поддержкой/d' "$file" 2>/dev/null || true + fi + done +fi + +echo "8. Удаление блока 'Лицензия' полностью (только Svelte файлы)..." + +# Ищем ТОЛЬКО в исходных Svelte файлах +LICENSE_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f -name "*.svelte" \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -lE "Лицензия|License|лицензионный тарифный план|Перейдите на лицензионный" {} \; 2>/dev/null) + +if [ -z "$LICENSE_FILES" ]; then + echo " Файлов с лицензией не найдено" +else + COUNT=$(echo "$LICENSE_FILES" | wc -l) + echo " Найдено Svelte файлов: $COUNT" + echo "$LICENSE_FILES" | while read file; do + if [ -n "$file" ]; then + echo " Удаление блока лицензии из: $file" + # Удаляем только текст блока лицензии + docker exec "${CONTAINER_NAME}" sed -i '/Перейдите на лицензионный/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/расширенные возможности/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/настраиваемую тематику/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/фирменный стиль/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/специальную поддержку/d' "$file" 2>/dev/null || true + fi + done +fi + +echo "9. Добавление надписи 'Основано на Open WebUI' внизу настроек..." + +# Ищем файлы настроек для добавления надписи +# Это сложнее сделать автоматически, поэтому просто отмечаем файлы +SETTINGS_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f -name "*.svelte" \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -l "Settings\|Настройки\|General\|Общее" {} \; 2>/dev/null | head -3) + +if [ -n "$SETTINGS_FILES" ]; then + echo " Найдены файлы настроек (надпись нужно добавить вручную):" + echo "$SETTINGS_FILES" | while read file; do + if [ -n "$file" ]; then + echo " - $file" + fi + done + echo "" + echo " Добавьте в конец блока настроек перед закрывающим тегом:" + echo "
" + echo " Основано на Open WebUI" + echo "
" +fi + +echo "" +echo "10. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== БЕЗОПАСНЫЙ ребрендинг завершен! ===" +echo "" +echo "Проверьте:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. splash.png должен быть заменен на ваш логотип" +echo " 3. Проверка обновлений и ссылки на GitHub должны быть удалены" +echo " 4. Социальные сети и блок лицензии должны быть удалены" +echo " 5. Очистите кеш браузера (Ctrl+Shift+Delete)" +echo "" +echo "Примечание: Надпись 'Основано на Open WebUI' нужно добавить вручную" +echo "в файлах настроек (см. вывод выше) или через Admin Panel" diff --git a/scripts/rebrand_ultra_safe.sh b/scripts/rebrand_ultra_safe.sh new file mode 100755 index 0000000..15e2ea7 --- /dev/null +++ b/scripts/rebrand_ultra_safe.sh @@ -0,0 +1,185 @@ +#!/bin/bash +# УЛЬТРА БЕЗОПАСНЫЙ ребрендинг - удаляет только конкретные блоки, не ломая структуру + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +MEDIA_DIR="$PROJECT_DIR/media" +CONTAINER_NAME="open-webui" + +echo "=== УЛЬТРА БЕЗОПАСНЫЙ ребрендинг Open WebUI для iiEasy ===" +echo "⚠ Удаляет только конкретные HTML блоки, не трогает код" +echo "" + +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен." + exit 1 +fi + +echo "1. Замена логотипов..." + +STATIC_DIRS=( + "/app/web/build/_app/immutable" + "/app/web/static" + "/app/web/build" +) + +for dir in "${STATIC_DIRS[@]}"; do + if docker exec "${CONTAINER_NAME}" test -d "$dir" 2>/dev/null; then + if [ -f "$MEDIA_DIR/logo.png" ]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/logo.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${dir}/splash.png" 2>/dev/null || true + fi + if [ -f "$MEDIA_DIR/favicon.png" ]; then + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.png" 2>/dev/null || true + docker cp "$MEDIA_DIR/favicon.png" "${CONTAINER_NAME}:${dir}/favicon.ico" 2>/dev/null || true + fi + fi +done + +# Заменяем существующие файлы +EXISTING_SPLASH=$(docker exec "${CONTAINER_NAME}" find /app -type f -name "splash.png" 2>/dev/null) +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_SPLASH" | while read -r splash_file; do + if [ -n "$splash_file" ] && [[ ! "$splash_file" == *"node_modules"* ]]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${splash_file}" 2>/dev/null || true + fi + done +fi + +EXISTING_FAVICONS=$(docker exec "${CONTAINER_NAME}" find /app -type f -name "favicon.png" 2>/dev/null) +if [ -f "$MEDIA_DIR/logo.png" ]; then + echo "$EXISTING_FAVICONS" | while read -r favicon_file; do + if [ -n "$favicon_file" ] && [[ ! "$favicon_file" == *"node_modules"* ]]; then + docker cp "$MEDIA_DIR/logo.png" "${CONTAINER_NAME}:${favicon_file}" 2>/dev/null || true + fi + done +fi + +echo "2. Удаление '(Open WebUI)' из HTML/Svelte..." + +FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f \ + \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -lE "(Open WebUI)|\(Open WebUI\)" {} \; 2>/dev/null) + +if [ -n "$FILES" ]; then + echo "$FILES" | while read file; do + if [ -n "$file" ]; then + docker exec "${CONTAINER_NAME}" sed -i \ + -e 's/(Open WebUI)//g' \ + -e 's/\(Open WebUI\)//g' \ + -e 's/ (Open WebUI)//g' \ + -e 's/Войти в iiEasyWeb (Open WebUI)/Войти в iiEasyWeb/g' \ + "$file" 2>/dev/null || true + fi + done +fi + +echo "3. Замена ссылок на документацию..." + +DOC_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f \ + \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -lE "docs\.openwebui\.com" {} \; 2>/dev/null) + +if [ -n "$DOC_FILES" ]; then + echo "$DOC_FILES" | while read file; do + if [ -n "$file" ]; then + docker exec "${CONTAINER_NAME}" sed -i \ + -e 's|https://docs.openwebui.com|https://note.iieasy.ru|g' \ + -e 's|docs\.openwebui\.com|note.iieasy.ru|g' \ + "$file" 2>/dev/null || true + fi + done +fi + +echo "4. Исправление favicon.png на logo.png..." + +TEMPLATE_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f \ + \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -l "/static/favicon.png" {} \; 2>/dev/null) + +if [ -n "$TEMPLATE_FILES" ]; then + echo "$TEMPLATE_FILES" | while read file; do + if [ -n "$file" ]; then + docker exec "${CONTAINER_NAME}" sed -i 's|/static/favicon.png|/static/logo.png|g' "$file" 2>/dev/null || true + fi + done +fi + +echo "5. Исправление API endpoint для изображения профиля модели..." + +API_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f \ + \( -name "*.html" -o -name "*.svelte" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -l "model/profile/image" {} \; 2>/dev/null) + +if [ -n "$API_FILES" ]; then + echo "$API_FILES" | while read file; do + if [ -n "$file" ]; then + docker exec "${CONTAINER_NAME}" sed -i 's|/api/v1/models/model/profile/image|/static/logo.png|g' "$file" 2>/dev/null || true + fi + done +fi + +echo "6. Удаление конкретных HTML блоков (безопасно)..." + +# Ищем только Svelte файлы настроек +SETTINGS_FILES=$(docker exec "${CONTAINER_NAME}" find /app/web -type f -name "*.svelte" \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + -exec grep -l "Проверить обновления\|Помощь\|Лицензия\|Version\|Help\|License" {} \; 2>/dev/null) + +if [ -n "$SETTINGS_FILES" ]; then + echo "$SETTINGS_FILES" | while read file; do + if [ -n "$file" ]; then + echo " Обработка файла настроек: $file" + + # Удаляем блок проверки обновлений - ищем по уникальным классам и тексту + # Удаляем кнопку "Проверить обновления" + docker exec "${CONTAINER_NAME}" sed -i '/Проверить обновления/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Check for updates/d' "$file" 2>/dev/null || true + + # Удаляем ссылку "(последняя)" с GitHub + docker exec "${CONTAINER_NAME}" sed -i 's|]*github.com/open-webui/releases[^>]*>.*последняя.*||g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|(последняя)||g' "$file" 2>/dev/null || true + + # Удаляем "Посмотреть, что нового" + docker exec "${CONTAINER_NAME}" sed -i '/Посмотреть, что нового/d' "$file" 2>/dev/null || true + + # Удаляем весь блок "Помощь" - ищем по заголовку и удаляем до следующего блока + # Более безопасно - удаляем только содержимое блока + docker exec "${CONTAINER_NAME}" sed -i '/Узнайте, как использовать/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/обратитесь за поддержкой/d' "$file" 2>/dev/null || true + + # Удаляем badges соцсетей - ищем по shields.io + docker exec "${CONTAINER_NAME}" sed -i 's|]*discord.gg[^>]*>.*||g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|]*twitter.com[^>]*>.*||g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|]*github.com/open-webui[^>]*>.*||g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|]*shields.io[^>]*>||g' "$file" 2>/dev/null || true + + # Удаляем блок "Лицензия" + docker exec "${CONTAINER_NAME}" sed -i '/Перейдите на лицензионный/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/расширенные возможности/d' "$file" 2>/dev/null || true + fi + done +fi + +echo "" +echo "7. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== УЛЬТРА БЕЗОПАСНЫЙ ребрендинг завершен! ===" +echo "" +echo "Проверьте:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. Очистите кеш браузера (Ctrl+Shift+Delete)" +echo "" +echo "Примечание: Если блоки все еще видны, они могут быть в скомпилированных JS файлах." +echo "В этом случае нужно найти исходные Svelte файлы и удалить блоки там." diff --git a/scripts/remove_footer_links.sh b/scripts/remove_footer_links.sh new file mode 100755 index 0000000..49c1d96 --- /dev/null +++ b/scripts/remove_footer_links.sh @@ -0,0 +1,133 @@ +#!/bin/bash +# Удаление социальных сетей, GitHub и лицензии из футера + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONTAINER_NAME="open-webui" + +echo "=== Удаление социальных сетей, GitHub и лицензии из футера ===" +echo "" + +if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then + echo "Ошибка: Контейнер ${CONTAINER_NAME} не запущен." + exit 1 +fi + +echo "1. Удаление ссылок на Discord..." + +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "discord\.gg|discord\.com|Discord" "$file" 2>/dev/null; then + echo " Удаление Discord из: $file" + # Удаляем строки с Discord + docker exec "${CONTAINER_NAME}" sed -i '/discord\.gg/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/discord\.com/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Discord/d' "$file" 2>/dev/null || true + # Удаляем ссылки + docker exec "${CONTAINER_NAME}" sed -i 's|https://discord.gg/[^"]*||g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|https://discord.com/[^"]*||g' "$file" 2>/dev/null || true + fi + fi +done + +echo "2. Удаление ссылок на Twitter/X..." + +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "twitter\.com|x\.com|X \(formerly Twitter\)|Twitter" "$file" 2>/dev/null; then + echo " Удаление Twitter/X из: $file" + # Удаляем строки с Twitter/X + docker exec "${CONTAINER_NAME}" sed -i '/twitter\.com/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/x\.com/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/X (formerly Twitter)/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Twitter/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Follow/d' "$file" 2>/dev/null || true + # Удаляем ссылки + docker exec "${CONTAINER_NAME}" sed -i 's|https://twitter.com/[^"]*||g' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i 's|https://x.com/[^"]*||g' "$file" 2>/dev/null || true + fi + fi +done + +echo "3. Удаление ссылок на GitHub Repo..." + +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "github\.com/open-webui|Github Repo|GitHub" "$file" 2>/dev/null; then + echo " Удаление GitHub из: $file" + # Удаляем строки с GitHub + docker exec "${CONTAINER_NAME}" sed -i '/Github Repo/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/GitHub Repo/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/github\.com\/open-webui/d' "$file" 2>/dev/null || true + # Удаляем ссылки на GitHub repo (но не все GitHub ссылки, только repo) + docker exec "${CONTAINER_NAME}" sed -i 's|https://github.com/open-webui/[^"]*||g' "$file" 2>/dev/null || true + fi + fi +done + +echo "4. Удаление строки про лицензию и тарифный план..." + +docker exec "${CONTAINER_NAME}" find /app -type f \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + ! -path "*/oauth*" ! -path "*/oidc*" ! -path "*authentik*" ! -path "*openid*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "лицензионный тарифный план|Перейдите на лицензионный|License|license|upgrade|Upgrade|тарифный план" "$file" 2>/dev/null; then + echo " Удаление лицензии из: $file" + # Удаляем строки про лицензию + docker exec "${CONTAINER_NAME}" sed -i '/лицензионный тарифный план/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Перейдите на лицензионный/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/расширенные возможности/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/настраиваемую тематику/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/фирменный стиль/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/специальную поддержку/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/License/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Лицензия/d' "$file" 2>/dev/null || true + fi + fi +done + +echo "5. Удаление всего блока 'Помощь' (Help section)..." + +docker exec "${CONTAINER_NAME}" find /app/web -type f \( -name "*.svelte" -o -name "*.html" -o -name "*.js" -o -name "*.ts" \) \ + ! -path "*/node_modules/*" ! -path "*/.next/*" \ + 2>/dev/null | while read file; do + if docker exec "${CONTAINER_NAME}" file "$file" 2>/dev/null | grep -q "text\|JSON\|ASCII"; then + if docker exec "${CONTAINER_NAME}" grep -qE "Помощь|Узнайте, как использовать|обратитесь за поддержкой|Документация" "$file" 2>/dev/null; then + echo " Удаление блока помощи из: $file" + # Удаляем строки из блока помощи + docker exec "${CONTAINER_NAME}" sed -i '/Помощь/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/Узнайте, как использовать/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/обратитесь за поддержкой/d' "$file" 2>/dev/null || true + docker exec "${CONTAINER_NAME}" sed -i '/к сообществу/d' "$file" 2>/dev/null || true + fi + fi +done + +echo "" +echo "6. Перезапуск контейнера..." + +docker restart "${CONTAINER_NAME}" >/dev/null 2>&1 || { + echo "Предупреждение: Перезапустите вручную: docker restart ${CONTAINER_NAME}" +} + +echo "" +echo "=== Удаление завершено! ===" +echo "" +echo "Проверьте:" +echo " 1. Откройте https://odo.iieasy.ru" +echo " 2. В футере не должно быть ссылок на Discord, Twitter/X, GitHub" +echo " 3. Не должно быть строки про лицензию" +echo "" +echo "ВАЖНО: Очистите кеш браузера (Ctrl+Shift+Delete)" diff --git a/scripts/test_direct_vision.sh b/scripts/test_direct_vision.sh new file mode 100755 index 0000000..05cae3e --- /dev/null +++ b/scripts/test_direct_vision.sh @@ -0,0 +1,68 @@ +#!/bin/bash +# Прямой тест vision через Ollama API (минуя Open WebUI) + +set -e + +CONTAINER_OLLAMA="ollama" +MODEL="gemma3n:e4b-it-fp16" +IMAGE_FILE="/home/its/iiEasyWeb/test_images/test_image.jpg" + +echo "=== Прямой тест Vision через Ollama API ===" +echo "" + +# Проверка файла изображения +if [ ! -f "$IMAGE_FILE" ]; then + echo "✗ Файл изображения не найден: $IMAGE_FILE" + echo "Скачайте изображение сначала" + exit 1 +fi + +echo "1. Кодирование изображения в base64..." +IMAGE_B64=$(base64 -w 0 "$IMAGE_FILE" 2>/dev/null || base64 "$IMAGE_FILE" | tr -d '\n') +echo " ✓ Изображение закодировано (${#IMAGE_B64} символов)" + +echo "" +echo "2. Отправка запроса к Ollama API..." +echo " Модель: $MODEL" +echo " Промпт: Опиши это изображение на русском языке. Что ты видишь?" +echo "" + +# Определяем команду docker +DOCKER_CMD="docker" +if ! docker ps >/dev/null 2>&1; then + DOCKER_CMD="sudo docker" +fi + +# Отправляем запрос +RESPONSE=$($DOCKER_CMD exec $CONTAINER_OLLAMA curl -s --max-time 120 -X POST http://localhost:11434/api/generate \ + -H 'Content-Type: application/json' \ + -d "{ + \"model\": \"$MODEL\", + \"prompt\": \"Опиши это изображение на русском языке. Что ты видишь на картинке?\", + \"images\": [\"$IMAGE_B64\"], + \"stream\": false + }" 2>&1) + +echo "" +if echo "$RESPONSE" | grep -q '"response"'; then + echo "✓ Успешно! Ответ модели:" + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('response', ''))" 2>/dev/null || \ + echo "$RESPONSE" | grep -o '"response":"[^"]*"' | sed 's/"response":"\(.*\)"/\1/' | sed 's/\\n/\n/g' | sed 's/\\"/"/g' + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "=== ✓ Vision работает напрямую через Ollama API! ===" + echo "" + echo "Если это работает, значит проблема в Open WebUI, а не в Ollama." + echo "Возможные причины:" + echo " 1. Open WebUI v0.8.3 не поддерживает vision для gemma3n" + echo " 2. Модель не помечена как vision-модель в настройках" + echo " 3. Нужно обновить Open WebUI до более новой версии" +elif echo "$RESPONSE" | grep -q "error\|Error"; then + echo "✗ Ошибка:" + echo "$RESPONSE" | grep -i error || echo "$RESPONSE" +else + echo "⚠ Неожиданный ответ:" + echo "$RESPONSE" | head -20 +fi diff --git a/scripts/test_vision.sh b/scripts/test_vision.sh new file mode 100755 index 0000000..83c75c0 --- /dev/null +++ b/scripts/test_vision.sh @@ -0,0 +1,142 @@ +#!/bin/bash +# Скрипт для тестирования vision capabilities модели gemma3n:e4b-it-fp16 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +TEST_DIR="$PROJECT_DIR/test_images" +IMAGE_URL="https://yandex-images.clstorage.net/PU5vN2154/532297ZKm/lCuSfyMn0DdbJqcFVeFiB9Ti31Te2dZ1EepiRw3Cs0Qw8cQ1ND5OQRKC1yH4LnhdRtloQ4aXHng5ZSLNmXHy_8k293YSMsBWKnOvYAXBbhPcl6pYmqi-ZGWDazZo2pYkJNHpkJHrg5yiO0bEOIeEICe5cFqrojYgyNQ6mHj4e5IUb_Lri3uxo9fmXv0dMf7f1NvH9J5YVsyhRvvmtD9eTc1QfVxV42d8OotKrLTDfHx7jfDqjpIqHAyt9ngIIHsSjLtOJ7_jTYdHLn4hJWlCj_jj69gGlApIBYaTfoxPrtHFDGlYUd-PjHI3hijAm6hIb-lUFlpOehQAJTbwEGwjcqjjVns323WrR2q6CalVyg9kZ8s0jkBm8OVGr26UH86NHaD1rKnPH6zinwJIKBv8VEdF_FrGLt6QFOVqFOQY776w47YDNy8Biwfefg0lxVaL2E9L5NbAztwh8vuCcMNSEfG8aYQFBxe0Eu9CMPhz1Axz1dgqmkpSYCAR8nSA_GMSKKPa9-tzIUc7ooKthV1ykwSjrzhORKq0eRrvBlTbajF5yAUoVX-7XM7HouyU51xMaxFMBl7atixQFW4o1BznMoi_chuL8_mTO_IKce21CosEQ6Owkkhi9EGSz5pIE56NjURBxKn7pwTKt6K4FB_YYHexZG4qlsIg8GVaiJyIB0p0bx5PF4_FIy-SItWZId5f8M9_RIqg2hRVCqP2hDvSfWGAYUjJd6c8lr9mgGj7kPQbVQBCUgIm1NB9lsxQrIO-wO92fytbOQsjjiqBqa0CK9Qjy6wi6CoIfX5vZjw_pj1BKGm0UVOXuArPFngIN2AUgz3w4mZSTvz8IQYo_IhPVlQ7PiOjhyWD6wZSVZ3NDrvsC-8w4lxmzE32T1qMuy5JOTRxaNHHH0QCp4IkgMv8LH8JeJaOUj4sVOGCBBgQi5KMX74TQzOBW9cGTu35UZr3qAfj5O6kMogxjqNanMOs" +IMAGE_FILE="$TEST_DIR/test_image.jpg" +MODEL="gemma3n:e4b-it-fp16" + +echo "=== Тестирование Vision Capabilities модели $MODEL ===" +echo "" + +# Определяем команду docker (с sudo или без) +DOCKER_CMD="docker" +if ! docker ps >/dev/null 2>&1; then + DOCKER_CMD="sudo docker" +fi + +# Проверка контейнера Ollama +echo "1. Проверка контейнера Ollama..." +if ! $DOCKER_CMD ps --format '{{.Names}}' 2>/dev/null | grep -q "^ollama$"; then + echo " ✗ Ошибка: Контейнер ollama не запущен" + echo " Запустите: docker compose up -d ollama" + exit 1 +fi +echo " ✓ Контейнер ollama запущен" + +# Проверка модели +echo "" +echo "2. Проверка загруженных моделей..." +MODELS=$($DOCKER_CMD exec ollama ollama list 2>/dev/null) +if echo "$MODELS" | grep -q "$MODEL"; then + echo " ✓ Модель $MODEL найдена" + echo "$MODELS" | grep "$MODEL" +else + echo " ✗ Модель $MODEL не найдена" + echo " Загрузите модель: docker exec ollama ollama pull $MODEL" + exit 1 +fi + +# Создаем директорию для тестовых изображений +mkdir -p "$TEST_DIR" +cd "$TEST_DIR" + +# Скачиваем изображение +echo "" +echo "3. Подготовка тестового изображения..." +if [ ! -f "$IMAGE_FILE" ]; then + echo " Скачиваю изображение..." + curl -L -o "$IMAGE_FILE" "$IMAGE_URL" || { + echo " ✗ Ошибка: не удалось скачать изображение" + exit 1 + } + echo " ✓ Изображение скачано: $IMAGE_FILE" +else + echo " ✓ Изображение уже существует: $IMAGE_FILE" +fi + +# Проверяем размер файла +if [ -f "$IMAGE_FILE" ]; then + FILE_SIZE=$(stat -c%s "$IMAGE_FILE" 2>/dev/null || stat -f%z "$IMAGE_FILE" 2>/dev/null) + echo " Размер файла: $(numfmt --to=iec-i --suffix=B $FILE_SIZE 2>/dev/null || echo "${FILE_SIZE} bytes")" + + # Проверяем тип файла + FILE_TYPE=$(file "$IMAGE_FILE" 2>/dev/null | cut -d: -f2) + echo " Тип файла: $FILE_TYPE" +fi + +# Кодируем изображение в base64 +echo "" +echo "4. Кодирование изображения в base64..." +IMAGE_B64=$(base64 -w 0 "$IMAGE_FILE" 2>/dev/null || base64 "$IMAGE_FILE" | tr -d '\n') +B64_LENGTH=${#IMAGE_B64} +echo " ✓ Изображение закодировано (длина: $B64_LENGTH символов)" + +# Отправляем запрос к Ollama API +echo "" +echo "5. Отправка запроса к Ollama API..." +echo " Модель: $MODEL" +echo " Промпт: Опиши это изображение на русском языке. Что ты видишь на картинке?" +echo "" + +# Отправляем запрос через Docker exec к Ollama API +# Используем временный файл для передачи большого base64 +echo " Отправка запроса (это может занять некоторое время)..." +TEMP_FILE=$(mktemp) +cat > "$TEMP_FILE" </dev/null 2>&1 +RESPONSE=$($DOCKER_CMD exec ollama sh -c "curl -s --max-time 120 -X POST http://localhost:11434/api/generate \ + -H 'Content-Type: application/json' \ + -d @/tmp/request.json" 2>&1) + +# Удаляем временный файл +rm -f "$TEMP_FILE" + +# Проверяем ответ +if echo "$RESPONSE" | grep -q '"response"'; then + echo " ✓ Успешно! Ответ модели:" + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + # Извлекаем ответ более надежным способом + echo "$RESPONSE" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('response', ''))" 2>/dev/null || \ + echo "$RESPONSE" | grep -o '"response":"[^"]*"' | sed 's/"response":"\(.*\)"/\1/' | sed 's/\\n/\n/g' | sed 's/\\"/"/g' | head -20 + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "=== ✓ Тест завершен успешно! Vision работает! ===" +elif echo "$RESPONSE" | grep -q "error\|Error\|ERROR"; then + echo " ✗ Ошибка при запросе к API:" + echo "$RESPONSE" | grep -i error || echo "$RESPONSE" + echo "" + echo "Проверьте:" + echo " 1. Логи Ollama: $DOCKER_CMD logs ollama --tail 30" + echo " 2. Поддерживает ли модель vision: $DOCKER_CMD exec ollama ollama show $MODEL" +else + echo " ⚠ Неожиданный ответ от API:" + echo "$RESPONSE" + echo "" + echo "Попробуйте проверить через веб-интерфейс Open WebUI" +fi + +echo "" +echo "6. Инструкции для тестирования через веб-интерфейс:" +echo "" +echo " Шаг 1: Откройте Open WebUI: https://odo.iieasy.ru" +echo " Шаг 2: Выберите модель: $MODEL" +echo " Шаг 3: Найдите кнопку загрузки изображения в поле ввода (📎 или 📷)" +echo " Шаг 4: Загрузите изображение: $IMAGE_FILE" +echo " Шаг 5: Задайте вопрос: \"Опиши это изображение на русском языке. Что ты видишь?\"" +echo "" +echo "Файл изображения: $IMAGE_FILE" diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100755 index 0000000..b4aa706 --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,50 @@ +#!/bin/bash +# Скрипт для обновления Open WebUI до последней версии + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONTAINER_NAME="open-webui" + +echo "=== Обновление Open WebUI ===" +echo "" + +cd "$PROJECT_DIR" + +# Проверка текущей версии в docker-compose.yml +CURRENT_VERSION=$(grep "image: ghcr.io/open-webui/open-webui:" docker-compose.yml | sed 's/.*:\(.*\)/\1/') +echo "Текущая версия в docker-compose.yml: $CURRENT_VERSION" + +echo "" +echo "1. Получение новых образов..." +sudo docker compose pull + +echo "" +echo "2. Остановка контейнеров..." +sudo docker compose stop + +echo "" +echo "3. Пересоздание контейнеров с новыми образами..." +sudo docker compose up -d + +echo "" +echo "4. Ожидание запуска контейнеров (30 секунд)..." +sleep 30 + +echo "" +echo "5. Применение ребрендинга..." +if [ -f "$SCRIPT_DIR/rebrand.sh" ]; then + "$SCRIPT_DIR/rebrand.sh" +else + echo "⚠ Скрипт rebrand.sh не найден, пропускаем ребрендинг" +fi + +echo "" +echo "=== Обновление завершено! ===" +echo "" +echo "Проверьте статус:" +echo " sudo docker compose ps" +echo "" +echo "Проверьте логи:" +echo " sudo docker compose logs open-webui --tail 50" diff --git a/searxng/settings.yml b/searxng/settings.yml new file mode 100644 index 0000000..2937d39 --- /dev/null +++ b/searxng/settings.yml @@ -0,0 +1,22 @@ +# SearXNG Settings для работы с Open WebUI +# Этот файл включает поддержку JSON формата для API запросов + +use_default_settings: true + +server: + secret_key: "CHANGE_ME_SECRET_KEY" + bind_address: "0.0.0.0" + port: 8080 + limiter: false + method: "GET" + +search: + safe_search: 0 + autocomplete: "google" + formats: + - html + - json + +general: + instance_name: "SearXNG" + debug: false diff --git a/ssl-setup-guide.md b/ssl-setup-guide.md new file mode 100644 index 0000000..450f620 --- /dev/null +++ b/ssl-setup-guide.md @@ -0,0 +1,121 @@ +# Быстрая настройка SSL для odo.iieasy.ru + +## Проблема: ERR_SSL_UNRECOGNIZED_NAME_ALERT + +Эта ошибка означает, что SSL сертификат не настроен или не соответствует домену. + +## Решение: Настройка Let's Encrypt сертификата + +### Шаг 1: Проверка DNS + +Убедитесь, что домен `odo.iieasy.ru` указывает на ваш сервер: + +```bash +# Проверьте DNS запись +dig odo.iieasy.ru +short +# или +nslookup odo.iieasy.ru +``` + +Должен вернуться IP адрес вашего сервера. + +### Шаг 2: Создание SSL сертификата в Nginx Proxy Manager + +1. Войдите в Nginx Proxy Manager (обычно `http://your-server-ip:81`) + +2. Перейдите в **SSL Certificates** (в левом меню) + +3. Нажмите **Add SSL Certificate** + +4. Выберите **Let's Encrypt** + +5. Заполните форму: + ``` + Domain Names: odo.iieasy.ru + Email Address: ваш@email.com + Agree to Let's Encrypt Terms: ✓ (включено) + Use a DNS Challenge: ✗ (выключено, если порт 80 доступен) + ``` + +6. Нажмите **Save** + +7. Дождитесь создания сертификата (1-2 минуты). Статус должен стать зеленым. + +### Шаг 3: Применение сертификата к Proxy Host + +1. Перейдите в **Proxy Hosts** + +2. Найдите или создайте Proxy Host для `odo.iieasy.ru` + +3. Откройте его для редактирования + +4. Перейдите на вкладку **SSL** + +5. Выберите созданный сертификат в поле **SSL Certificate** + +6. Включите опции: + - ✓ **Force SSL** (перенаправляет HTTP → HTTPS) + - ✓ **HTTP/2 Support** + - ✓ **HSTS Enabled** (опционально) + +7. Нажмите **Save** + +### Шаг 4: Проверка + +1. Подождите 1-2 минуты для применения изменений + +2. Откройте `https://odo.iieasy.ru` в браузере + +3. Проверьте, что SSL работает (замочек в адресной строке) + +## Альтернатива: Использование существующего сертификата + +Если у вас уже есть SSL сертификат для домена: + +1. **SSL Certificates** → **Add SSL Certificate** → **Custom** + +2. Вставьте: + - **Certificate**: содержимое файла `.crt` или `.pem` + - **Private Key**: содержимое файла `.key` + +3. Нажмите **Save** + +4. Примените к Proxy Host как описано выше + +## Временное решение (только для тестирования) + +Если нужно быстро проверить работу без SSL: + +1. В Proxy Host отключите SSL (вкладка SSL → SSL Certificate = None) + +2. Используйте HTTP: `http://odo.iieasy.ru` + +3. После настройки SSL включите обратно + +## Проверка работы + +```bash +# Проверка SSL сертификата +openssl s_client -connect odo.iieasy.ru:443 -servername odo.iieasy.ru + +# Проверка доступности +curl -I https://odo.iieasy.ru +``` + +## Частые проблемы + +### Сертификат не создается + +- Проверьте, что порт 80 доступен из интернета +- Убедитесь, что DNS запись правильная +- Проверьте логи Nginx Proxy Manager + +### Сертификат создан, но ошибка остается + +- Убедитесь, что сертификат применен к Proxy Host +- Проверьте, что домен в сертификате совпадает с доменом в Proxy Host +- Очистите кеш браузера + +### Порты 80/443 заняты + +Если порты заняты другими сервисами, настройте перенаправление или используйте DNS challenge в Let's Encrypt. diff --git a/test.jpg b/test.jpg new file mode 100644 index 0000000..501b0eb --- /dev/null +++ b/test.jpg @@ -0,0 +1 @@ +Geoblocked \ No newline at end of file diff --git a/test_images/test_image.jpg b/test_images/test_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..da7630e69c863985ebdf97f898c077dde4f3ca0e GIT binary patch literal 23550 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 XiJ#Lw7t5G?@nS0Mf0>W7{F(bVCZ4~H literal 0 HcmV?d00001 diff --git a/worker/.env.example b/worker/.env.example new file mode 100644 index 0000000..5d09a73 --- /dev/null +++ b/worker/.env.example @@ -0,0 +1,34 @@ +# === NEXTCLOUD === +DOMAIN_NEXTCLOUD=https://next.iieasy.ru +NC_USER=your_nextcloud_username +NC_APP_PASSWORD=your_app_password + +# Пути для сканирования (разделенные запятыми) +# {username} будет заменен на имя пользователя Nextcloud +NC_SCAN_PATHS=/home/{username}/Documents,/home/{username}/Files,/Shared/Documents + +# === OPEN WEBUI API === +DOMAIN_OPENWEBUI=https://odo.iieasy.ru +OPENWEBUI_API_KEY=your_api_key_here +OPENWEBUI_TIMEOUT=300 + +# === AUTHENTIK (опционально, для маппинга пользователей) === +DOMAIN_AUTHENTIK=https://auth.iieasy.ru +AUTHENTIK_API_TOKEN=your_authentik_api_token + +# Маппинг пользователей Nextcloud -> Authentik (опционально) +# Формат: NC_USER_{nextcloud_username}={authentik_user_id} +# NC_USER_john=user.12345678-1234-1234-1234-123456789012 + +# === НАСТРОЙКИ ВОРКЕРА === +# Интервал синхронизации в секундах (по умолчанию 300 = 5 минут) +SYNC_INTERVAL=300 + +# Максимальный размер файла для обработки в байтах (по умолчанию 100MB) +MAX_FILE_SIZE=104857600 + +# Путь к БД состояния синхронизации +DB_PATH=sync_state.db + +# Уровень логирования (DEBUG, INFO, WARNING, ERROR) +LOG_LEVEL=INFO diff --git a/worker/config.py b/worker/config.py new file mode 100644 index 0000000..2c947fa --- /dev/null +++ b/worker/config.py @@ -0,0 +1,132 @@ +""" +Конфигурация для воркера синхронизации Nextcloud -> Qdrant +Типизация и валидация настроек +""" +import os +from pathlib import Path +from dataclasses import dataclass +from typing import List, Optional +from dotenv import load_dotenv + +# Загрузка переменных окружения: сначала worker/.env, затем корневой .env +load_dotenv() +root_env = Path(__file__).resolve().parent.parent / ".env" +load_dotenv(root_env) + + +@dataclass +class NextcloudConfig: + """Конфигурация подключения к Nextcloud""" + url: str + username: str + password: str + scan_paths: List[str] # Пути для сканирования + + +@dataclass +class OpenWebUIConfig: + """Конфигурация подключения к Open WebUI API""" + api_url: str + api_key: str + timeout: int = 300 # Таймаут для больших файлов (секунды) + + +@dataclass +class AuthentikConfig: + """Конфигурация для маппинга пользователей Authentik""" + api_url: Optional[str] = None + api_token: Optional[str] = None + + +@dataclass +class WorkerConfig: + """Основная конфигурация воркера""" + nextcloud: NextcloudConfig + openwebui: OpenWebUIConfig + authentik: AuthentikConfig + sync_interval: int = 300 # Интервал синхронизации (секунды) + max_file_size: int = 100 * 1024 * 1024 # Максимальный размер файла (100MB) + db_path: str = "sync_state.db" # Путь к SQLite БД для отслеживания состояния + log_level: str = "INFO" + + +def load_config() -> WorkerConfig: + """ + Загрузка и валидация конфигурации из переменных окружения + + Returns: + WorkerConfig: Валидированная конфигурация + + Raises: + ValueError: Если обязательные переменные не установлены + """ + # Nextcloud конфигурация + nc_url = os.getenv("DOMAIN_NEXTCLOUD", "").rstrip("/") + if not nc_url: + raise ValueError("DOMAIN_NEXTCLOUD не установлен в .env") + + nc_user = os.getenv("NC_USER") + if not nc_user: + raise ValueError("NC_USER не установлен в .env") + + nc_password = os.getenv("NC_APP_PASSWORD") + if not nc_password: + raise ValueError("NC_APP_PASSWORD не установлен в .env") + + # Пути для сканирования (можно настроить через переменные окружения) + scan_paths = os.getenv( + "NC_SCAN_PATHS", + "/home/{username}/Documents,/home/{username}/Files,/Shared/Documents" + ).split(",") + + nextcloud_config = NextcloudConfig( + url=nc_url, + username=nc_user, + password=nc_password, + scan_paths=[path.strip() for path in scan_paths] + ) + + # Open WebUI конфигурация + webui_url = os.getenv("DOMAIN_OPENWEBUI", "").rstrip("/") + if not webui_url: + raise ValueError("DOMAIN_OPENWEBUI не установлен в .env") + + webui_api_key = os.getenv("OPENWEBUI_API_KEY") + if not webui_api_key or webui_api_key == "твой_api_ключ_от_openwebui": + raise ValueError( + "OPENWEBUI_API_KEY не установлен или имеет значение по умолчанию. " + "Создайте API ключ в Open WebUI -> Settings -> Account -> API Keys" + ) + + timeout = int(os.getenv("OPENWEBUI_TIMEOUT", "300")) + + openwebui_config = OpenWebUIConfig( + api_url=f"{webui_url}/api/v1", + api_key=webui_api_key, + timeout=timeout + ) + + # Authentik конфигурация (опционально) + authentik_url = os.getenv("DOMAIN_AUTHENTIK", "").rstrip("/") + authentik_token = os.getenv("AUTHENTIK_API_TOKEN") + + authentik_config = AuthentikConfig( + api_url=authentik_url if authentik_url else None, + api_token=authentik_token + ) + + # Общие настройки воркера + sync_interval = int(os.getenv("SYNC_INTERVAL", "300")) + max_file_size = int(os.getenv("MAX_FILE_SIZE", str(100 * 1024 * 1024))) + db_path = os.getenv("DB_PATH", "sync_state.db") + log_level = os.getenv("LOG_LEVEL", "INFO") + + return WorkerConfig( + nextcloud=nextcloud_config, + openwebui=openwebui_config, + authentik=authentik_config, + sync_interval=sync_interval, + max_file_size=max_file_size, + db_path=db_path, + log_level=log_level + ) diff --git a/worker/document_processor.py b/worker/document_processor.py new file mode 100644 index 0000000..1630845 --- /dev/null +++ b/worker/document_processor.py @@ -0,0 +1,243 @@ +""" +Обработка документов различных форматов +Поддержка PDF, DOCX, текстовых файлов с обработкой больших файлов (>100MB) +""" +import logging +import io +from typing import Optional, Tuple +from pathlib import Path + +logger = logging.getLogger(__name__) + + +class DocumentProcessor: + """Обработчик документов различных форматов""" + + def __init__(self, max_file_size: int = 100 * 1024 * 1024): + """ + Инициализация процессора документов + + Args: + max_file_size: Максимальный размер файла для прямой обработки (байты) + """ + self.max_file_size = max_file_size + + def process_file( + self, + file_content: bytes, + filename: str, + mime_type: Optional[str] = None + ) -> Tuple[str, bool]: + """ + Обработка файла и извлечение текста + + Args: + file_content: Содержимое файла + filename: Имя файла (для определения типа) + mime_type: MIME тип файла (опционально) + + Returns: + Кортеж (текст, is_large_file) где is_large_file указывает, + что файл был обработан потоково из-за большого размера + """ + file_size = len(file_content) + is_large_file = file_size > self.max_file_size + + # Определение типа файла + file_ext = Path(filename).suffix.lower() + + try: + if file_ext == '.pdf' or mime_type == 'application/pdf': + return self._process_pdf(file_content, is_large_file), is_large_file + + elif file_ext in ['.docx', '.doc'] or mime_type in ['application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'application/msword']: + return self._process_docx(file_content), False + + elif file_ext in ['.txt', '.md', '.markdown'] or mime_type in ['text/plain', 'text/markdown']: + return self._process_text(file_content), False + + elif file_ext == '.csv' or mime_type == 'text/csv': + return self._process_csv(file_content), False + + else: + logger.warning(f"Неподдерживаемый формат файла: {filename} (тип: {mime_type})") + # Пытаемся обработать как текст + try: + return self._process_text(file_content), False + except Exception: + raise ValueError(f"Не удалось обработать файл {filename}: неподдерживаемый формат") + + except Exception as e: + logger.error(f"Ошибка при обработке файла {filename}: {e}") + raise + + def _process_pdf(self, content: bytes, is_large: bool) -> str: + """ + Обработка PDF файла + + Args: + content: Содержимое PDF + is_large: Флаг большого файла (для потоковой обработки) + + Returns: + Извлеченный текст + """ + try: + import pypdf + + pdf_file = io.BytesIO(content) + pdf_reader = pypdf.PdfReader(pdf_file) + + text_parts = [] + total_pages = len(pdf_reader.pages) + + logger.info(f"Обработка PDF: {total_pages} страниц") + + # Для больших файлов обрабатываем страницы порциями + if is_large: + # Ограничиваем количество страниц для очень больших файлов + max_pages = min(total_pages, 1000) # Максимум 1000 страниц + logger.warning(f"Большой PDF файл. Обрабатываются первые {max_pages} из {total_pages} страниц") + else: + max_pages = total_pages + + for page_num in range(max_pages): + try: + page = pdf_reader.pages[page_num] + text = page.extract_text() + if text.strip(): + text_parts.append(f"--- Страница {page_num + 1} ---\n{text}") + except Exception as e: + logger.warning(f"Ошибка при обработке страницы {page_num + 1}: {e}") + continue + + result = "\n\n".join(text_parts) + + if not result.strip(): + raise ValueError("Не удалось извлечь текст из PDF") + + return result + + except ImportError: + raise ImportError( + "Библиотека pypdf не установлена. Установите: pip install pypdf" + ) + except Exception as e: + logger.error(f"Ошибка при обработке PDF: {e}") + raise + + def _process_docx(self, content: bytes) -> str: + """ + Обработка DOCX файла + + Args: + content: Содержимое DOCX + + Returns: + Извлеченный текст + """ + try: + import docx + + doc_file = io.BytesIO(content) + doc = docx.Document(doc_file) + + text_parts = [] + + # Извлечение текста из параграфов + for paragraph in doc.paragraphs: + if paragraph.text.strip(): + text_parts.append(paragraph.text) + + # Извлечение текста из таблиц + for table in doc.tables: + for row in table.rows: + row_text = " | ".join(cell.text.strip() for cell in row.cells) + if row_text.strip(): + text_parts.append(row_text) + + result = "\n\n".join(text_parts) + + if not result.strip(): + raise ValueError("Не удалось извлечь текст из DOCX") + + return result + + except ImportError: + raise ImportError( + "Библиотека python-docx не установлена. Установите: pip install python-docx" + ) + except Exception as e: + logger.error(f"Ошибка при обработке DOCX: {e}") + raise + + def _process_text(self, content: bytes) -> str: + """ + Обработка текстового файла + + Args: + content: Содержимое файла + + Returns: + Текст с правильной кодировкой + """ + # Попытка различных кодировок + encodings = ['utf-8', 'utf-8-sig', 'cp1251', 'latin-1'] + + for encoding in encodings: + try: + return content.decode(encoding) + except UnicodeDecodeError: + continue + + # Если ничего не помогло, используем errors='replace' + return content.decode('utf-8', errors='replace') + + def _process_csv(self, content: bytes) -> str: + """ + Обработка CSV файла (конвертация в читаемый текст) + + Args: + content: Содержимое CSV + + Returns: + Текстовая версия CSV + """ + import csv + + text_content = self._process_text(content) + csv_reader = csv.reader(io.StringIO(text_content)) + + rows = [] + for row_num, row in enumerate(csv_reader, 1): + rows.append(f"Строка {row_num}: {' | '.join(row)}") + + return "\n".join(rows) + + def is_supported_format(self, filename: str, mime_type: Optional[str] = None) -> bool: + """ + Проверка поддержки формата файла + + Args: + filename: Имя файла + mime_type: MIME тип + + Returns: + True если формат поддерживается + """ + file_ext = Path(filename).suffix.lower() + supported_extensions = {'.pdf', '.docx', '.doc', '.txt', '.md', '.markdown', '.csv'} + + if file_ext in supported_extensions: + return True + + supported_mimes = { + 'application/pdf', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'application/msword', + 'text/plain', + 'text/markdown', + 'text/csv' + } + + return mime_type in supported_mimes if mime_type else False diff --git a/worker/nextcloud_client.py b/worker/nextcloud_client.py new file mode 100644 index 0000000..78f7b6d --- /dev/null +++ b/worker/nextcloud_client.py @@ -0,0 +1,249 @@ +""" +WebDAV клиент для работы с Nextcloud +Сканирование директорий, загрузка файлов, получение метаданных +""" +import os +import logging +from datetime import datetime +from typing import List, Dict, Optional, Tuple +from pathlib import Path +import requests +from requests.auth import HTTPBasicAuth +from requests.exceptions import RequestException, Timeout + +logger = logging.getLogger(__name__) + + +class NextcloudClient: + """Клиент для работы с Nextcloud через WebDAV API""" + + def __init__(self, url: str, username: str, password: str): + """ + Инициализация WebDAV клиента + + Args: + url: URL Nextcloud (например, https://next.iieasy.ru) + username: Имя пользователя Nextcloud + password: App Password (не основной пароль!) + """ + self.base_url = url.rstrip("/") + self.webdav_url = f"{self.base_url}/remote.php/dav/files/{username}" + self.auth = HTTPBasicAuth(username, password) + self.session = requests.Session() + self.session.auth = self.auth + self.session.headers.update({ + "User-Agent": "iiEasy-Nextcloud-Sync/1.0" + }) + + def list_directory(self, path: str, depth: int = 1) -> List[Dict]: + """ + Получение списка файлов и директорий через PROPFIND + + Args: + path: Путь к директории (относительно WebDAV root) + depth: Глубина рекурсии (0 - только указанный ресурс, 1 - + дочерние) + + Returns: + Список словарей с информацией о файлах/директориях + + Raises: + RequestException: При ошибках сетевого запроса + """ + url = f"{self.webdav_url}/{path.lstrip('/')}" + + try: + # PROPFIND запрос для получения списка ресурсов + headers = { + "Depth": str(depth), + "Content-Type": "application/xml" + } + + body = """ + + + + + + + + +""" + + response = self.session.request( + "PROPFIND", + url, + headers=headers, + data=body, + timeout=30 + ) + response.raise_for_status() + + # Парсинг XML ответа (упрощенная версия) + # В production лучше использовать xml.etree.ElementTree + files = [] + # Здесь должен быть парсинг XML, но для простоты используем альтернативный метод + return self._parse_propfind_response(response.text) + + except Timeout: + logger.error(f"Таймаут при получении списка файлов: {path}") + raise + except RequestException as e: + logger.error(f"Ошибка при получении списка файлов {path}: {e}") + raise + + def _parse_propfind_response(self, xml_content: str) -> List[Dict]: + """ + Упрощенный парсинг PROPFIND ответа + В production лучше использовать xml.etree.ElementTree или lxml + """ + import re + files = [] + + # Простой regex парсинг (для production использовать XML парсер) + # Ищем href и getlastmodified + href_pattern = r'(.*?)' + modified_pattern = r'(.*?)' + size_pattern = r'(.*?)' + type_pattern = r'(.*?)' + + hrefs = re.findall(href_pattern, xml_content) + modifieds = re.findall(modified_pattern, xml_content) + sizes = re.findall(size_pattern, xml_content) + types = re.findall(type_pattern, xml_content) + + for i, href in enumerate(hrefs): + # Убираем префикс /remote.php/dav/files/username + clean_href = href.replace(f"/remote.php/dav/files/{self.auth.username}", "") + if clean_href == "": + continue + + files.append({ + "path": clean_href, + "modified": modifieds[i] if i < len(modifieds) else None, + "size": int(sizes[i]) if i < len(sizes) and sizes[i] else 0, + "type": types[i] if i < len(types) else "application/octet-stream", + "is_directory": i < len(types) and types[i] == "httpd/unix-directory" + }) + + return files + + def download_file(self, path: str) -> bytes: + """ + Загрузка файла из Nextcloud + + Args: + path: Путь к файлу (относительно WebDAV root) + + Returns: + Содержимое файла в виде bytes + + Raises: + RequestException: При ошибках загрузки + """ + url = f"{self.webdav_url}/{path.lstrip('/')}" + + try: + response = self.session.get(url, timeout=300, stream=True) + response.raise_for_status() + return response.content + + except Timeout: + logger.error(f"Таймаут при загрузке файла: {path}") + raise + except RequestException as e: + logger.error(f"Ошибка при загрузке файла {path}: {e}") + raise + + def get_file_metadata(self, path: str) -> Dict: + """ + Получение метаданных файла (размер, дата изменения) + + Args: + path: Путь к файлу + + Returns: + Словарь с метаданными + """ + try: + url = f"{self.webdav_url}/{path.lstrip('/')}" + response = self.session.head(url, timeout=30) + response.raise_for_status() + + return { + "size": int(response.headers.get("Content-Length", 0)), + "modified": response.headers.get("Last-Modified"), + "etag": response.headers.get("ETag", "").strip('"') + } + except RequestException as e: + logger.warning(f"Не удалось получить метаданные для {path}: {e}") + return { + "size": 0, + "modified": None, + "etag": "" + } + + def scan_directory_recursive(self, base_path: str, max_depth: int = 10) -> List[Dict]: + """ + Рекурсивное сканирование директории + + Args: + base_path: Базовый путь для сканирования + max_depth: Максимальная глубина рекурсии + + Returns: + Список всех файлов с их метаданными + """ + all_files = [] + + def scan_recursive(current_path: str, depth: int = 0): + if depth > max_depth: + return + + try: + items = self.list_directory(current_path, depth=1) + + for item in items: + item_path = item["path"] + + # Пропускаем сам каталог + if item_path == current_path: + continue + + if item.get("is_directory"): + # Рекурсивный обход поддиректорий + scan_recursive(item_path, depth + 1) + else: + # Добавляем файл в список + all_files.append({ + "path": item_path, + "size": item.get("size", 0), + "modified": item.get("modified"), + "type": item.get("type", "application/octet-stream") + }) + + except Exception as e: + logger.error(f"Ошибка при сканировании {current_path}: {e}") + + scan_recursive(base_path) + return all_files + + def extract_username_from_path(self, path: str) -> Optional[str]: + """ + Извлечение имени пользователя из пути Nextcloud + Например: /home/username/Documents -> username + + Args: + path: Путь к файлу/директории + + Returns: + Имя пользователя или None + """ + # Формат путей: /home/{username}/... + parts = path.strip("/").split("/") + if len(parts) >= 2 and parts[0] == "home": + return parts[1] + return None + + def close(self): + """Закрытие сессии""" + self.session.close() diff --git a/worker/nextcloud_sync.py b/worker/nextcloud_sync.py new file mode 100755 index 0000000..3031b5b --- /dev/null +++ b/worker/nextcloud_sync.py @@ -0,0 +1,448 @@ +#!/usr/bin/env python3 +""" +Воркер синхронизации Nextcloud -> Qdrant через Open WebUI API +Background процесс для автоматической синхронизации документов +""" +import sys +import time +import logging +import sqlite3 +import hashlib +from pathlib import Path +from typing import Dict, Optional, List +from datetime import datetime + +# Импорт локальных модулей +from config import load_config, WorkerConfig +from nextcloud_client import NextcloudClient +from openwebui_client import OpenWebUIClient +from document_processor import DocumentProcessor + +# Настройка логирования +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler('sync.log'), + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + + +class SyncStateDB: + """Управление состоянием синхронизации через SQLite""" + + def __init__(self, db_path: str): + """ + Инициализация БД состояния + + Args: + db_path: Путь к файлу БД + """ + self.db_path = db_path + self._init_db() + + def _init_db(self): + """Инициализация схемы БД""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + CREATE TABLE IF NOT EXISTS synced_files ( + file_path TEXT PRIMARY KEY, + file_hash TEXT NOT NULL, + file_size INTEGER NOT NULL, + last_modified TEXT, + synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + user_id TEXT, + document_id TEXT + ) + """) + + cursor.execute(""" + CREATE INDEX IF NOT EXISTS idx_file_hash ON synced_files(file_hash) + """) + + conn.commit() + conn.close() + + def get_file_hash(self, file_path: str, file_content: bytes) -> str: + """Вычисление хеша файла""" + return hashlib.sha256(file_content).hexdigest() + + def is_file_synced(self, file_path: str, file_hash: str) -> bool: + """ + Проверка, синхронизирован ли файл + + Args: + file_path: Путь к файлу + file_hash: Хеш содержимого файла + + Returns: + True если файл уже синхронизирован с таким хешем + """ + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + SELECT file_hash FROM synced_files + WHERE file_path = ? AND file_hash = ? + """, (file_path, file_hash)) + + result = cursor.fetchone() + conn.close() + + return result is not None + + def mark_file_synced( + self, + file_path: str, + file_hash: str, + file_size: int, + last_modified: Optional[str], + user_id: Optional[str], + document_id: Optional[str] + ): + """Отметка файла как синхронизированного""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute(""" + INSERT OR REPLACE INTO synced_files + (file_path, file_hash, file_size, last_modified, synced_at, user_id, document_id) + VALUES (?, ?, ?, ?, ?, ?, ?) + """, ( + file_path, + file_hash, + file_size, + last_modified, + datetime.now().isoformat(), + user_id, + document_id + )) + + conn.commit() + conn.close() + + def get_sync_stats(self) -> Dict: + """Получение статистики синхронизации""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + cursor.execute("SELECT COUNT(*) FROM synced_files") + total_files = cursor.fetchone()[0] + + cursor.execute("SELECT SUM(file_size) FROM synced_files") + total_size = cursor.fetchone()[0] or 0 + + conn.close() + + return { + "total_files": total_files, + "total_size": total_size + } + + +class UserMapper: + """Маппинг пользователей Nextcloud -> Authentik""" + + def __init__(self, config: WorkerConfig): + """ + Инициализация маппера пользователей + + Args: + config: Конфигурация воркера + """ + self.config = config + # Кеш маппинга (в production можно использовать БД или Authentik API) + self._cache: Dict[str, str] = {} + + def map_nextcloud_user_to_authentik(self, nc_username: str) -> Optional[str]: + """ + Маппинг имени пользователя Nextcloud в user_id Authentik + + Args: + nc_username: Имя пользователя в Nextcloud + + Returns: + user_id Authentik или None если маппинг не найден + """ + # Проверка кеша + if nc_username in self._cache: + return self._cache[nc_username] + + # В production здесь должен быть запрос к Authentik API + # или использование общей БД пользователей + # Пока используем простое предположение: username совпадает + # или можно настроить через переменные окружения + + # Попытка получить из переменных окружения (формат: NC_USER_username=AUTHENTIK_USER_ID) + env_key = f"NC_USER_{nc_username}" + authentik_user_id = None + + import os + if env_key in os.environ: + authentik_user_id = os.environ[env_key] + else: + # По умолчанию предполагаем, что username совпадает + # В production это должно быть через Authentik API + authentik_user_id = nc_username + + self._cache[nc_username] = authentik_user_id + return authentik_user_id + + def get_user_groups(self, nc_username: str) -> List[str]: + """ + Получение списка групп доступа для пользователя + + Args: + nc_username: Имя пользователя Nextcloud + + Returns: + Список групп доступа + """ + # В production получать из Authentik API или Nextcloud групп + # Пока возвращаем пустой список (только личные файлы) + return [] + + +class NextcloudSyncWorker: + """Основной класс воркера синхронизации""" + + def __init__(self, config: WorkerConfig): + """ + Инициализация воркера + + Args: + config: Конфигурация воркера + """ + self.config = config + self.nc_client = NextcloudClient( + config.nextcloud.url, + config.nextcloud.username, + config.nextcloud.password + ) + self.webui_client = OpenWebUIClient( + config.openwebui.api_url, + config.openwebui.api_key, + config.openwebui.timeout + ) + self.processor = DocumentProcessor(config.max_file_size) + self.state_db = SyncStateDB(config.db_path) + self.user_mapper = UserMapper(config) + + def sync_path(self, path_template: str) -> int: + """ + Синхронизация указанного пути + + Args: + path_template: Шаблон пути (может содержать {username}) + + Returns: + Количество синхронизированных файлов + """ + synced_count = 0 + + # Замена {username} в шаблоне пути + if "{username}" in path_template: + # Сканируем все домашние директории + # В production можно получить список пользователей из Nextcloud API + username = self.config.nextcloud.username + path = path_template.replace("{username}", username) + else: + path = path_template + + try: + logger.info(f"Сканирование пути: {path}") + files = self.nc_client.scan_directory_recursive(path) + logger.info(f"Найдено файлов: {len(files)}") + + for file_info in files: + try: + if self._sync_file(file_info, path_template): + synced_count += 1 + except Exception as e: + logger.error(f"Ошибка при синхронизации файла {file_info['path']}: {e}") + continue + + except Exception as e: + logger.error(f"Ошибка при сканировании пути {path}: {e}") + + return synced_count + + def _sync_file(self, file_info: Dict, path_template: str) -> bool: + """ + Синхронизация одного файла + + Args: + file_info: Информация о файле + path_template: Шаблон пути (для определения владельца) + + Returns: + True если файл был синхронизирован + """ + file_path = file_info["path"] + file_size = file_info.get("size", 0) + + # Пропускаем слишком большие файлы + if file_size > self.config.max_file_size * 2: # Двойной лимит для безопасности + logger.warning(f"Файл {file_path} слишком большой ({file_size} bytes), пропуск") + return False + + # Проверка поддержки формата + if not self.processor.is_supported_format(file_path, file_info.get("type")): + logger.debug(f"Неподдерживаемый формат: {file_path}") + return False + + try: + # Загрузка файла + file_content = self.nc_client.download_file(file_path) + file_hash = self.state_db.get_file_hash(file_path, file_content) + + # Проверка, не синхронизирован ли уже файл + if self.state_db.is_file_synced(file_path, file_hash): + logger.debug(f"Файл уже синхронизирован: {file_path}") + return False + + # Обработка файла + text_content, is_large = self.processor.process_file( + file_content, + Path(file_path).name, + file_info.get("type") + ) + + # Определение владельца файла + username = self.nc_client.extract_username_from_path(file_path) + if not username: + # Для общих папок используем None или специальную группу + username = None + + user_id = self.user_mapper.map_nextcloud_user_to_authentik(username) if username else None + access_groups = self.user_mapper.get_user_groups(username) if username else [] + + # Метаданные для API + metadata = { + "source": "nextcloud", + "path": file_path, + "original_size": file_size, + "is_large_file": is_large + } + + # Загрузка в Open WebUI + if is_large or len(text_content.encode('utf-8')) > self.config.max_file_size: + # Для больших файлов загружаем как текст + result = self.webui_client.upload_document_with_text( + text_content=text_content, + filename=Path(file_path).name, + user_id=user_id, + access_groups=access_groups, + metadata=metadata + ) + else: + # Для обычных файлов загружаем оригинал + result = self.webui_client.upload_document( + file_content=file_content, + filename=Path(file_path).name, + user_id=user_id, + access_groups=access_groups, + metadata=metadata + ) + + # Сохранение состояния + document_id = result.get("id") if isinstance(result, dict) else None + self.state_db.mark_file_synced( + file_path=file_path, + file_hash=file_hash, + file_size=file_size, + last_modified=file_info.get("modified"), + user_id=user_id, + document_id=document_id + ) + + logger.info(f"Файл синхронизирован: {file_path} -> документ ID: {document_id}") + return True + + except Exception as e: + logger.error(f"Ошибка при синхронизации файла {file_path}: {e}") + return False + + def run_once(self): + """Однократный запуск синхронизации""" + logger.info("=== Начало синхронизации ===") + + total_synced = 0 + + for path_template in self.config.nextcloud.scan_paths: + try: + synced = self.sync_path(path_template) + total_synced += synced + logger.info(f"Синхронизировано файлов из {path_template}: {synced}") + except Exception as e: + logger.error(f"Ошибка при синхронизации пути {path_template}: {e}") + + stats = self.state_db.get_sync_stats() + logger.info(f"=== Синхронизация завершена ===") + logger.info(f"Всего синхронизировано в этом цикле: {total_synced}") + logger.info(f"Всего файлов в БД: {stats['total_files']}") + logger.info(f"Общий размер: {stats['total_size'] / 1024 / 1024:.2f} MB") + + def run_daemon(self): + """Запуск воркера в режиме daemon (бесконечный цикл)""" + logger.info("Запуск воркера в режиме daemon") + logger.info(f"Интервал синхронизации: {self.config.sync_interval} секунд") + + try: + while True: + self.run_once() + logger.info(f"Ожидание {self.config.sync_interval} секунд до следующей синхронизации...") + time.sleep(self.config.sync_interval) + except KeyboardInterrupt: + logger.info("Получен сигнал остановки, завершение работы...") + finally: + self.cleanup() + + def cleanup(self): + """Очистка ресурсов""" + logger.info("Очистка ресурсов...") + self.nc_client.close() + self.webui_client.close() + + +def main(): + """Точка входа""" + import argparse + + parser = argparse.ArgumentParser(description="Воркер синхронизации Nextcloud -> Qdrant") + parser.add_argument( + "--once", + action="store_true", + help="Запустить синхронизацию один раз и выйти" + ) + parser.add_argument( + "--daemon", + action="store_true", + help="Запустить в режиме daemon (бесконечный цикл)" + ) + + args = parser.parse_args() + + try: + config = load_config() + worker = NextcloudSyncWorker(config) + + if args.once: + worker.run_once() + elif args.daemon: + worker.run_daemon() + else: + # По умолчанию запускаем один раз + worker.run_once() + + except Exception as e: + logger.error(f"Критическая ошибка: {e}", exc_info=True) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/worker/openwebui_client.py b/worker/openwebui_client.py new file mode 100644 index 0000000..0ec544e --- /dev/null +++ b/worker/openwebui_client.py @@ -0,0 +1,179 @@ +""" +HTTP клиент для загрузки документов в Open WebUI API +Поддержка метаданных user_id и access_groups для изоляции доступа +""" +import logging +from typing import Dict, Optional, List +import requests +from requests.exceptions import RequestException, Timeout + +logger = logging.getLogger(__name__) + + +class OpenWebUIClient: + """Клиент для работы с Open WebUI API""" + + def __init__(self, api_url: str, api_key: str, timeout: int = 300): + """ + Инициализация клиента Open WebUI API + + Args: + api_url: Базовый URL API (например, https://odo.iieasy.ru/api/v1) + api_key: API ключ из Open WebUI Settings -> Account -> API Keys + timeout: Таймаут для запросов (секунды) + """ + self.api_url = api_url.rstrip("/") + self.api_key = api_key + self.timeout = timeout + self.session = requests.Session() + self.session.headers.update({ + "Authorization": f"Bearer {api_key}", + "User-Agent": "iiEasy-Nextcloud-Sync/1.0" + }) + + def upload_document( + self, + file_content: bytes, + filename: str, + user_id: Optional[str] = None, + access_groups: Optional[List[str]] = None, + metadata: Optional[Dict] = None + ) -> Dict: + """ + Загрузка документа в Open WebUI через API + + Args: + file_content: Содержимое файла в виде bytes + filename: Имя файла + user_id: ID пользователя Authentik для изоляции доступа + access_groups: Список групп доступа (для совместного доступа) + metadata: Дополнительные метаданные (source, path и т.д.) + + Returns: + Ответ API в виде словаря + + Raises: + RequestException: При ошибках загрузки + """ + url = f"{self.api_url}/documents" + + # Подготовка данных для multipart/form-data + files = { + "file": (filename, file_content) + } + + # Метаданные передаются через form-data или JSON + data = {} + + if user_id: + data["user_id"] = user_id + + if access_groups: + # Если API поддерживает список групп, передаем как JSON строку или отдельные поля + data["access_groups"] = ",".join(access_groups) if isinstance(access_groups, list) else access_groups + + if metadata: + # Добавляем дополнительные метаданные + for key, value in metadata.items(): + if key not in data: + data[key] = str(value) + + try: + logger.info(f"Загрузка документа {filename} (размер: {len(file_content)} bytes)") + + response = self.session.post( + url, + files=files, + data=data, + timeout=self.timeout + ) + + response.raise_for_status() + + result = response.json() if response.content else {} + logger.info(f"Документ {filename} успешно загружен. ID: {result.get('id', 'unknown')}") + + return result + + except Timeout: + logger.error(f"Таймаут при загрузке документа {filename}") + raise + except RequestException as e: + logger.error(f"Ошибка при загрузке документа {filename}: {e}") + if hasattr(e.response, 'text'): + logger.error(f"Ответ сервера: {e.response.text}") + raise + + def upload_document_with_text( + self, + text_content: str, + filename: str, + user_id: Optional[str] = None, + access_groups: Optional[List[str]] = None, + metadata: Optional[Dict] = None + ) -> Dict: + """ + Загрузка текстового документа (например, извлеченного из PDF) + + Args: + text_content: Текст документа + filename: Имя файла (будет изменено на .txt если нужно) + user_id: ID пользователя Authentik + access_groups: Список групп доступа + metadata: Дополнительные метаданные + + Returns: + Ответ API + """ + # Убеждаемся, что файл имеет расширение .txt + if not filename.endswith('.txt'): + filename = filename.rsplit('.', 1)[0] + '.txt' + + file_content = text_content.encode('utf-8') + return self.upload_document( + file_content=file_content, + filename=filename, + user_id=user_id, + access_groups=access_groups, + metadata=metadata + ) + + def check_document_exists(self, document_id: str) -> bool: + """ + Проверка существования документа по ID + + Args: + document_id: ID документа + + Returns: + True если документ существует + """ + try: + url = f"{self.api_url}/documents/{document_id}" + response = self.session.get(url, timeout=30) + return response.status_code == 200 + except RequestException: + return False + + def delete_document(self, document_id: str) -> bool: + """ + Удаление документа по ID + + Args: + document_id: ID документа + + Returns: + True если удаление успешно + """ + try: + url = f"{self.api_url}/documents/{document_id}" + response = self.session.delete(url, timeout=30) + response.raise_for_status() + return True + except RequestException as e: + logger.error(f"Ошибка при удалении документа {document_id}: {e}") + return False + + def close(self): + """Закрытие сессии""" + self.session.close() diff --git a/worker/requirements.txt b/worker/requirements.txt new file mode 100644 index 0000000..3cd69ef --- /dev/null +++ b/worker/requirements.txt @@ -0,0 +1,17 @@ +# Зависимости для воркера синхронизации Nextcloud -> Qdrant + +# HTTP клиенты +requests>=2.31.0 + +# Обработка переменных окружения +python-dotenv>=1.0.0 + +# Обработка PDF +pypdf>=3.17.0 + +# Обработка DOCX +python-docx>=1.1.0 + +# WebDAV клиент (альтернатива requests для WebDAV) +# Используем requests напрямую, но можно добавить: +# easywebdav>=1.2.0 # Опционально, если нужны специфичные WebDAV функции