Compare commits

..

81 Commits

Author SHA1 Message Date
Fedor
2bb56342f4 Add OnlyOfficeTemplates module 2026-02-16 09:27:19 +03:00
Fedor
fd54177ada docs: авторизация ej.sudrf.ru через ЕСИА (Browserless + n8n)
- docs/BROWSERLESS_EJ_SUDRF_ESIA_LOGIN.md: использование browserless_login_esia.js
- Вход/выход, типы ошибок, шаг 2 (SMS), технические детали
2026-02-04 10:54:03 +03:00
Fedor
fd2e7cfb07 feat(n8n): авторизация ej.sudrf.ru через ЕСИА (Browserless)
- Скрипт browserless_login_esia.js для n8n: ГАС Правосудие → ЕСИА → до экрана SMS
- Галочка согласия (#iAgree), ожидание активации кнопки Войти, клик по button.esiaLogin
- Заполнение логина/пароля на ЕСИА: видимый input, keyboard.type + blur/change
- Куки через page.cookies() (совместимость с browserless /function)
- Вход: login/pass из body или fallback из n8n (JSON.stringify для спецсимволов)
- Возврат: status waiting_for_sms + cookies для второго шага (ввод SMS)
2026-02-04 10:52:19 +03:00
Fedor
d7982931cd feat: добавлен telegram_replay.php для публикации ответов поддержки в CRM
Новый endpoint для записи ответов поддержки как комментариев в CRM:
- Принимает JSON с полями: answer, contact_id, project_id (опц.), support_user_id (опц.), channel (опц.)
- Использует прямые INSERT запросы в vtiger_crmentity, vtiger_modcomments, vtiger_modcommentscf
- Обязательно создаёт запись в vtiger_modcommentscf (иначе комментарий не отображается)
- Устанавливает deleted=0 (иначе фильтруется при выборке)
- Полная проверка ошибок БД с детальным логированием
- Логи: logs/tg_replay_inbound.log

Исправлены проблемы:
- vtws_create падал без выброса исключения — заменён на прямой SQL
- Убраны несуществующие колонки (from_mailconverter, customer_email, from_mailroom)
- Добавлена обязательная запись в vtiger_modcommentscf
2026-02-03 14:02:12 +03:00
Fedor
ea0edafba5 Добавлена обработка бинарных данных и форматирование email для n8n workflow
- Добавлен рабочий код для подготовки бинарных данных и вложений для узла Send email
- Реализовано форматирование form_data в читаемый текст с разделами:
  * Личные данные (с автоматическим расчетом возраста, код документа, серия и номер)
  * Контактная информация
  * Страховщик (с телефоном страховой компании)
  * Банковские реквизиты (с получателем платежа)
  * Информация о страховом случае
  * Описание ситуации
- Добавлена документация по использованию и устранению неполадок
- Созданы альтернативные версии кода (простая версия, Function Node версия)
2025-12-26 14:51:58 +03:00
Fedor
1fdb244fd4 fix: исправлены ошибки clipboard и добавлена передача ticket_number в webhook поддержки
- Исправлена ошибка 'Cannot read properties of undefined (reading writeText)' в Step1Phone, StepClaimConfirmation, Step3Payment
  - Добавлена проверка на существование navigator.clipboard
  - Добавлен fallback для старых браузеров (document.execCommand)
  - Добавлена обработка ошибок с try/catch

- Добавлена передача ticket_number и ticket_id в webhook поддержки
  - При обработке события out_of_scope сохраняются claim_id, ticket_number, ticket_id из payload
  - Эти поля теперь передаются в webhook при отправке в поддержку

- Обновлён n8n код для обработки ошибок и out_of_scope
  - Добавлено явное сохранение ticket_number в data объекта события
  - ticket_number теперь гарантированно попадает в событие для Redis
2025-12-05 12:39:08 +03:00
Fedor
ab54530500 feat: добавлено детальное логирование статусов заявлений для диагностики
- Добавлено логирование всех заявлений с их статусами, если запрос вернул 0 строк
- Помогает понять, почему заявление не возвращается
2025-12-04 10:33:29 +03:00
Fedor
ffe30033da fix: временно убран фильтр is_confirmed для диагностики
- Убран фильтр is_confirmed для выяснения, почему заявление не возвращается
- После диагностики нужно вернуть фильтры с правильной логикой
2025-12-04 10:31:46 +03:00
Fedor
35ca11552d fix: убран фильтр по status_code='approved' - теперь approved заявления тоже возвращаются
- Заявления со статусом 'approved' теперь показываются в списке
- Исключаются только заявления с is_confirmed = true
- Исправляет проблему, когда заявление не возвращалось из-за статуса 'approved'
2025-12-04 10:30:36 +03:00
Fedor
b28bb4b636 fix: временно убраны фильтры для диагностики заявлений
- Убраны фильтры is_confirmed и status_code для диагностики
- Нужно выяснить, почему заявление не возвращается даже без фильтров
2025-12-04 10:27:35 +03:00
Fedor
37472d612a feat: добавлена диагностика статусов заявлений в debug
- Добавлены test_count_approved и test_count_confirmed для диагностики
- Показывает, сколько заявлений со статусом approved и is_confirmed = true
- Помогает понять, почему заявления не возвращаются в списке
2025-12-04 10:23:02 +03:00
Fedor
e0097a164d fix: улучшен поиск заявлений по телефону - поддержка разных форматов
- Поиск теперь работает по разным форматам телефона:
  * 71234543212 (оригинальный)
  * +71234543212 (с плюсом)
  * 81234543212 (с 8 вместо 7)
- Убрано условие unified_id IS NULL - теперь ищет в любом случае
- Поиск в clpr_user_accounts и напрямую в payload->>'phone'

Исправляет проблему поиска заявлений для контакта 71234543212
2025-12-04 10:18:04 +03:00
Fedor
6a783deba1 fix: улучшен поиск заявлений по телефону - добавлен fallback на прямой поиск по payload
- Теперь поиск работает двумя способами:
  1. Через unified_id (если есть запись в clpr_user_accounts)
  2. Напрямую по телефону в payload (если unified_id не установлен)

Исправляет проблему, когда заявления не находятся для контакта 71234543212
2025-12-04 10:15:41 +03:00
Fedor
920eb53660 fix: исправлена вставка номера телефона через копирование
- Установка значения напрямую в input элемент
- Триггер события input для синхронизации с формой
- Использование requestAnimationFrame вместо setTimeout для более надёжной работы

Теперь можно вставлять номер телефона через Ctrl+V / Cmd+V
2025-12-04 10:10:19 +03:00
Fedor
1f88e156b7 fix: исправлена проблема с label for атрибутом в createCheckbox
- Добавлена генерация безопасного id (убираются специальные символы)
- Добавлено экранирование id при использовании в атрибуте for
- Убеждаемся, что id и for всегда совпадают

Исправляет предупреждение: 'Incorrect use of <label for=FORM_ELEMENT>'
2025-12-04 09:45:40 +03:00
Fedor
003210dcfc fix: исправлены проблемы с формами и циклическими ссылками
- Исправлена проблема с циклическими ссылками в Step1Phone.tsx при onPaste (добавлен setTimeout)
- Добавлены name атрибуты во все поля формы для правильной работы форм и автозаполнения
- Исправлены проблемы с label/input связями (все label имеют правильные for/id)
- Все поля теперь имеют id и name атрибуты (исправляет предупреждения в консоли)

Исправленные функции:
- createField: добавлен name атрибут
- createReadonlyField: добавлен name атрибут
- createDateField: добавлен name атрибут
- createMoneyField: добавлен name атрибут
- createTextarea: добавлен name атрибут
- createBankSelect: добавлен name атрибут для основного поля и скрытого bank_id
- createCheckbox: добавлен name атрибут, проверена связь label/input
2025-12-04 09:21:12 +03:00
Fedor
346d9a77d2 feat: добавлена визуальная подсветка обязательных полей
- Добавлены звёздочки (*) рядом с обязательными полями
- Незаполненные обязательные поля подсвечиваются жёлтой рамкой
- Добавлен блок с предупреждением о незаполненных полях перед кнопкой отправки
- Улучшена валидация с визуальной обратной связью
- Пользователю теперь понятно, какие поля нужно заполнить
2025-12-03 18:59:58 +03:00
Fedor
38457394c1 fix: исправлена ошибка setSelectionRange для email полей и добавлено логирование
- Исправлена ошибка 'setSelectionRange not supported for email field' - добавлен try-catch и проверка типа поля
- Добавлено логирование наличия contact_data_confirmed, cf_2624, bank_id, bank_name в body запроса
- Это поможет отследить, передаются ли поля из frontend в backend
2025-12-03 18:51:33 +03:00
Fedor
e7915df634 fix: исправлено поле email для контрагента - добавлен type='email'
- Добавлен type='email' и inputmode='email' для полей email
- Это исправляет проблему, когда в поле email контрагента можно было вводить только цифры
- Обработчики событий уже были настроены правильно, но не хватало правильного типа поля
2025-12-03 18:47:08 +03:00
Fedor
b2433f38d8 feat: добавлено логирование для отладки передачи cf_2624 и bank_name
- Добавлено логирование извлечения contact_data_confirmed, cf_2624, bank_id, bank_name из body
- Добавлено логирование наличия этих полей в event_data перед отправкой в Redis
- Поможет отследить, передаются ли поля из frontend в backend
2025-12-03 18:44:07 +03:00
Fedor
8e116c76a4 feat: после отправки заявления сразу показывается сообщение об успехе
- Добавлен callback onSubmitted в StepClaimConfirmation
- После успешной отправки (SMS-верификации) сразу показывается сообщение об успехе
- Убрана форма редактирования после отправки
- Пользователь видит только сообщение: 'Поздравляем! Ваше обращение направлено в Клиентправ...'
2025-12-03 18:41:18 +03:00
Fedor
bf3fb5fef0 feat: добавлены cf_2624 и bank_name в payload при отправке заявления
Frontend (StepClaimConfirmation):
- Добавлен contact_data_confirmed в payload для Redis канала
- Добавлен cf_2624 (значение для CRM: 1 или 0)
- Добавлены bank_id и bank_name в payload
- bank_name сохраняется в state при выборе банка

Backend (claims.py):
- Добавлены contact_data_confirmed и cf_2624 в event_data
- Добавлены bank_id и bank_name в event_data для Redis канала clientright:webform:approve

Обновлён статус заявления 226564ce-d7cf-48ee-a820-690e8f5ec8e5 на draft_claim_ready для тестирования
2025-12-03 18:35:02 +03:00
Fedor
1fc64c035e feat: обновлён текст успешной отправки заявления
- Заменён текст на главной странице после отправки:
  'Поздравляем! Ваше обращение направлено в Клиентправ.'
  'В ближайшее время на указанную Вами электронную почту поступит письмо, подтверждающее регистрацию вашего обращения.'

- Обновлены toast-уведомления при отправке заявления
- Убрано старое сообщение 'Мы изучаем ваш вопрос и документы'
2025-12-03 18:29:36 +03:00
Fedor
b93bb9e8ad feat: выбор банка для СБП выплат теперь обязательное поле
- Добавлен bank_id в список обязательных полей в validateAllFields()
- Обновлён placeholder: "Начните вводить название банка (обязательно)"
- Обновлён заголовок: "Банк для получения выплаты (обязательно)"
- При отправке формы проверяется заполнение банка

Теперь форма не отправится без выбора банка для получения выплаты по СБП.
2025-12-03 18:18:53 +03:00
Fedor
35adcb3043 feat: добавлены ИНН и E-mail как обязательные поля для контрагентов
- ИНН организации теперь обязательное поле
- E-mail организации теперь обязательное поле
- Обновлены placeholder'ы с указанием (обязательно)
- Обновлена валидация validateAllFields()

Обязательные поля для контрагентов:
1. Наименование (accountname)
2. ИНН (inn)
3. Адрес (address)
4. E-mail (email)

Необязательные:
- Телефон (phone)
- Сайт (website)
2025-12-03 18:13:58 +03:00
Fedor
3d9669dd8e fix: настройка подключения MySQL CRM из Docker контейнера
- Изменён docker-compose.yml: добавлен network_mode: host для доступа к MySQL на хосте
- Обновлён config.py: mysql_crm_host = localhost (работает в режиме host)
- MySQL CRM теперь успешно подключается из контейнера
2025-12-03 18:08:05 +03:00
Fedor
6a9f8b5465 docs: обновлён лог сессии 2025-12-03
- Получение cf_2624 из MySQL при загрузке черновика
- Исправление подключения MySQL CRM из Docker контейнера
- Блокировка редактирования полей при contact_data_confirmed=true
- Выбор банка для СБП выплат
- Все проблемы решены, система работает корректно
2025-12-03 18:07:53 +03:00
Fedor
a86120dd53 fix: передача contact_data_confirmed в StepClaimConfirmation для блокировки полей
- Добавлен prop contact_data_confirmed в StepClaimConfirmation
- Передача флага из formData.contact_data_confirmed в компонент
- Исправлена логика получения флага (приоритет: props > claimPlanData > false)
- Поля должны блокироваться при contact_data_confirmed=true
2025-12-03 18:04:08 +03:00
Fedor
e114231541 feat: Получение cf_2624 из MySQL при загрузке черновика
- Добавлен сервис CrmMySQLService для подключения к MySQL БД vtiger CRM
- Обновлён get_draft() для прямого SQL запроса к MySQL вместо webservice API
- Получение cf_2624 и всех данных контакта из MySQL
- Обновлена документация и SQL файлы для n8n
- Добавлено логирование для отладки

Преимущества:
- Проще: один SQL запрос вместо цепочки HTTP запросов
- Быстрее: прямой запрос к БД
- Надёжнее: не зависит от webservice API
- Актуальнее: всегда свежие данные из БД
2025-12-03 16:04:25 +03:00
Fedor
b7197e0da5 feat(forms): автоподстановка банков и улучшенная обработка телефона
- Step1Phone: добавлена обработка вставки телефона с автоматической очисткой от +7 и обрезкой до 10 цифр
- Step3Payment: заменён Select на AutoComplete для выбора банка с автоподстановкой
- generateConfirmationFormHTML: заменён select на input с datalist для автоподстановки банков в форме подтверждения
- Добавлены скрытые поля bank_id для сохранения ID банка отдельно от названия
- Добавлены файлы для проверки заявки 226564ce

Улучшения UX:
- Пользователь может вводить название банка вместо прокрутки длинного списка
- Автоматическая фильтрация списка банков при вводе
- Предупреждение при обрезке номера телефона при вставке
2025-12-02 17:12:25 +03:00
Fedor
ee1c4af5c3 feat: добавлен выбор банка для СБП выплат
Frontend:
- Динамическая загрузка 226 банков из NSPK API
- Выбор банка добавлен в Step3Payment (новая заявка)
- Выбор банка добавлен в generateConfirmationFormHTML (редактирование)
- Поля: bank_id (ID из NSPK) и bank_name (название для отображения)

Backend:
- Добавлено поле bank_id в ClaimCreateRequest

API:
- http://212.193.27.93/api/payouts/dictionaries/nspk-banks

Изменения:
- ticket_form/frontend/src/components/form/Step3Payment.tsx
- ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts
- ticket_form/frontend/src/pages/ClaimForm.tsx
- ticket_form/backend/app/api/models.py
2025-12-02 11:06:15 +03:00
Fedor
834520a045 docs: add session log 2025-12-01 2025-12-01 22:19:31 +03:00
Fedor
da82100b60 feat: UI/UX improvements + CRM integration methods + documents_meta deduplication
Frontend:
- Changed main title to 'Подать обращение о защите прав потребителя'
- Changed browser title to 'Clientright — защита прав потребителей'
- Enhanced draft cards: show problem_description (250 chars), category tag, document progress bar
- Fixed 'Назад' button to always return to draft selection
- Added SSE connection for OCR status updates
- Renamed steps: Вход, Обращение, Документы, Заявление
- Skip 'Проверка полиса' and 'Тип события' steps for new claim flow

Backend:
- Fixed client IP extraction (X-Forwarded-For, X-Real-IP)
- Added problem_title, category, documents_required_list to draft list API
- Fixed documents_uploaded count to count unique field_labels

CRM Webservices:
- Added UpsertContact.php - create/update contacts with tgid support
- Added UpsertAccounts.php - batch upsert offenders by INN
- Added UpsertProject.php - create/update projects with offender mapping

Database:
- Fixed documents_meta duplicates in existing claims
- SQL query for deduplication by field_name provided
2025-12-01 22:18:21 +03:00
Fedor
81acd49fd9 feat(frontend): использование form_draft для формы подтверждения
- StepDraftSelection: кнопка 'Продолжить' активна для draft_docs_complete
- ClaimForm: проверка form_draft при загрузке черновика
- Если есть form_draft — преобразуем в propertyName и показываем форму подтверждения
- Поддержка статуса draft_docs_complete для перехода к подтверждению
2025-11-30 11:45:35 +03:00
Fedor
0f8631bf20 fix(backend): исправлен алгоритм hash для совместимости с JS
- Использован ctypes.c_int32 для эмуляции JS 32-битного сдвига
- Теперь Python hash совпадает с JS hash в n8n
- Кэширование form_draft работает корректно
2025-11-30 11:37:16 +03:00
Fedor
3801bc4949 feat(backend): кэширование form_draft в check-ocr-status
- Добавлена проверка наличия черновика в БД перед запуском RAG
- Если documents_hash совпадает — возвращаем черновик из кэша
- Если черновика нет или он устарел — запускаем RAG workflow
- Добавлен параметр force_refresh для принудительного обновления
- Импортирован db сервис для работы с PostgreSQL
2025-11-30 11:30:36 +03:00
Fedor
985ee23810 feat(n8n): RAG workflow для извлечения данных из документов
- Обновлён Code1: нормализация данных из give_data1 с поддержкой payload.applicant, ai_analysis, wizard_plan
- Обновлён Code6: генерация промптов для RAG (user, project, offenders)
- Добавлена документация по настройке n8n нод для OCR статуса
- Добавлен эндпоинт check-ocr-status в documents.py
- Добавлен лог сессии с полным описанием workflow

Workflow itX62h38faB51y9J успешно извлекает:
- Данные пользователя (ФИО, контакты, адрес)
- Данные проекта (сумма, предмет, даты договора)
- Несколько контрагентов с разными ролями (seller, service_provider)
2025-11-29 19:29:14 +03:00
Fedor
840acca51a feat(documents): дедупликация documents_meta и исправление field_label
- Исправлен N8N_CODE_PROCESS_UPLOADED_FILES_FIXED.js: использовать uploads_field_labels[0] вместо [grp]
- Создан SQL_CLAIMSAVE_FIXED_NEW_FLOW_DEDUP.sql с дедупликацией documents_meta
- Создан SQL_CLEANUP_DOCUMENTS_META_DUPLICATES.sql для очистки существующих дубликатов
- Создан полный уникальный индекс idx_document_texts_hash_unique на document_texts(file_hash)
- Добавлен SESSION_LOG_2025-11-28_documents_dedup.md с описанием всех изменений

Fixes:
- field_label теперь корректно отображает 'Переписка' вместо 'group-2'
- documents_meta не накапливает дубликаты при повторных сохранениях
- ON CONFLICT (file_hash) теперь работает для document_texts
2025-11-28 18:16:53 +03:00
Fedor
6c770f0a87 feat(ticket_form): Новая архитектура загрузки документов
- StepDocumentsNew.tsx: поэкранная загрузка документов
- StepWaitingClaim.tsx: ожидание формирования заявления с SSE
- StepDraftSelection.tsx: поддержка новых статусов черновиков
- documents.py: API для загрузки документов
- NEW_FLOW_ARCHITECTURE.md: документация новой архитектуры

Флоу: Description → Documents → Waiting → Claim Review → SMS
Статусы: draft_new, draft_docs_progress, draft_docs_complete, draft_claim_ready
2025-11-26 12:52:54 +03:00
Fedor
0868d37484 Сессия 26.11.2025: Исправления UI Wizard - блоки загрузки, retry аутентификации 2025-11-25 23:42:32 +03:00
Fedor
55c1402d99 Исправлен React.useRef → useRef 2025-11-25 23:38:24 +03:00
Fedor
18fcdecae8 Исправлено дублирование блоков документов (ref для отслеживания созданных) 2025-11-25 23:36:32 +03:00
Fedor
f058ca91ad Убран дублирующий useEffect для создания блоков документов 2025-11-25 23:33:59 +03:00
Fedor
796316d969 Убраны лишние поля для предустановленных документов (description, кнопка Удалить) 2025-11-25 23:31:40 +03:00
Fedor
f3b5771c09 Блоки загрузки документов сразу развёрнуты при загрузке плана 2025-11-25 23:28:36 +03:00
Fedor
2ce0c585ff Чекбокс 'нет документа' перенесён под блок загрузки 2025-11-25 23:26:20 +03:00
Fedor
6cc07b0ba6 Исправлен JSX Fragment для блоков загрузки документов 2025-11-25 23:22:34 +03:00
Fedor
b5478c143f Заменены чекбоксы docs_exist на блоки загрузки файлов 2025-11-25 23:20:33 +03:00
Fedor
be1ac2ed49 Добавлен retry механизм для webservice аутентификации (race condition fix) 2025-11-25 23:02:52 +03:00
Fedor
2b1dca9e92 Автогенерация уникального описания для дополнительных блоков документов 2025-11-25 21:22:11 +03:00
Fedor
99ef902a31 Показываем поле описания для всех блоков если блоков > 1 2025-11-25 21:20:48 +03:00
Fedor
8626c9aff4 Автоматическая нумерация дубликатов field_label при отправке визарда 2025-11-25 21:12:30 +03:00
Fedor
444e5d2b91 Добавлен лог отправляемых метаданных документов (field_label) 2025-11-25 20:53:19 +03:00
Fedor
8f4cff55e9 Добавлен полный лог диалога за 22 ноября 2025 2025-11-25 20:46:59 +03:00
Fedor
52fe013375 feat(ticket_form): unified_id/contact_id передача, исправлен мерж сессии, новая сессия для жалобы
- Добавлены unified_id и contact_id в TicketFormDescriptionRequest
- Исправлен CODE_MERGE_PROJECT_TO_SESSION.js - теперь сохраняются ВСЕ данные из body.other
- Добавлен fallback на получение other из Webhook напрямую
- Генерация новой session_id при создании новой жалобы (сохраняя авторизацию)
- Добавлен SQL_SELECT_CONTACT_WITH_CUSTOM_FIELDS.sql для CRM контактов
- Создан SESSION_LOG_2025-11-25.md с документацией сессии
2025-11-25 20:02:21 +03:00
Fedor
a20a4d0e09 Добавлен лог сессии 2025-11-22 2025-11-22 09:40:14 +03:00
Fedor
486f3619ff Добавлен field_label в результат переименования файлов, исправлена загрузка черновиков, обновлен формат пути S3 с project_name 2025-11-22 09:38:38 +03:00
Fedor
d3ba054027 Исправление архивации проектов: поддержка S3 файлов и документов из связанных сущностей
- Добавлен метод getRelatedDocs() для получения документов из связанных сущностей (контакты, контрагенты)
- Добавлен метод downloadS3File() для скачивания файлов из S3 во временную папку
- Добавлен метод cleanupTempFiles() для очистки временных файлов
- Исправлен getPaths() для корректной обработки S3 файлов (всегда запрашивает s3_bucket/s3_key из БД)
- Исправлен getArchive() для проектов: собирает документы из основной записи и связанных сущностей
- Исправлен путь к vendor/autoload.php (поиск по нескольким путям)
- Исправлено имя временного файла (короткое имя вместо полного пути для избежания 'File name too long')

Результат: архив успешно создается с документами из проекта и связанных сущностей (25 документов для проекта 396447)
2025-11-21 10:23:52 +03:00
Fedor
30a0df9c64 docs: добавлен лог сессии 19.11.2025 2025-11-20 00:39:04 +03:00
Fedor
d2f37faa7b fix: убран claim_id, используется только session_id на ранних этапах
- Убрана проверка claim_id из StepDescription.tsx
- Заменен claim_id на session_id в StepWizardPlan.tsx для SSE подключения
- Убран claim_id из запросов к API и сохранения в Step1Phone
- Обновлен backend для работы с опциональным claim_id
- Добавлена документация по исправлению узла claimsave для первичного черновика
- Добавлены SQL запросы и примеры кода для n8n workflow
2025-11-20 00:38:33 +03:00
Fedor
de011efba9 fix: исправлен конфликт имён переменных в loadDraft (claimId -> finalClaimId)
- Исправлена ошибка ReferenceError при загрузке черновиков
- Переименована локальная переменная claimId в finalClaimId для избежания конфликта с параметром функции
- Обновлена логика извлечения claim_id из разных источников (claim.claim_id, payload.claim_id, body.claim_id, claim.id)
- Добавлен fallback на параметр claimId функции для надёжности
2025-11-19 23:33:52 +03:00
Fedor
cd90b0d58a feat: Добавлен инструмент генерации документов для AI Ассистента
- Создан API create_document_with_text.php для создания DOCX/XLSX/PPTX с текстом от AI
- Поддержка Markdown форматирования (заголовки, жирный, курсив, списки, код)
- Установлен PHPWord для красивого форматирования документов
- Исправлены пути сохранения (crm2/CRM_Active_Files/... без /crm/ в начале)
- Замена пробелов на подчеркивания в именах папок
- Создана документация для AI и разработчиков
- Добавлены API для работы с шаблонами Nextcloud
2025-11-12 19:46:06 +03:00
Fedor
75912e5cfb Добавлена возможность изменения ширины AI Drawer
- Добавлен resize handle (полоска для перетаскивания) слева от drawer
- Реализовано изменение ширины перетаскиванием (от 300px до 50% экрана)
- Сохранение ширины в localStorage
- Автоматическое обновление margin для main-container
- Обработка изменения размера окна браузера
- Скрытие resize handle на мобильных устройствах
2025-11-11 15:24:27 +03:00
Fedor
43e760aad6 Добавлен лог сессии реализации SSE + Redis для AI Drawer 2025-11-11 15:17:06 +03:00
Fedor
1a4653298d Реализован SSE + Redis Pub/Sub для AI Drawer
- Добавлен SSE endpoint (aiassist/ai_sse.php) для real-time получения ответов от n8n
- Обновлен n8n_proxy.php: убран callback, добавлена передача Redis параметров в n8n
- Обновлен ai-drawer-simple.js: переход с polling на SSE с fallback через Redis
- Добавлен check_redis_response.php для прямого чтения из Redis кэша
- Добавлена документация: N8N_REDIS_SETUP.md, N8N_REDIS_FIX.md, AI_DRAWER_REDIS_SSE.md
- Поддержка plain text ответов от n8n (автоматическое определение формата)
- Кэширование ответов в Redis для надежности (TTL 5 минут)
2025-11-11 15:16:27 +03:00
Fedor
f770bd0e43 Fix AI Drawer SSE Redis caching 2025-11-10 22:58:35 +03:00
Fedor
546ce83763 feat: Прямой PHP эндпоинт для привязки документов
Создан api_attach_documents.php:
 Прямой эндпоинт без backend proxy
 URL: https://crm.clientright.ru/api_attach_documents.php
 Принимает массив документов из n8n
 Умная обработка S3 путей (добавляет хост если нужно)
 Поддержка file/file_url, filename/file_name
 Привязка к HelpDesk или Project (зависит от ticket_id)
 Проксирует к upload_documents_to_crm.php
 Полное логирование в logs/api_attach_documents.log

Готово к использованию в n8n!
2025-11-02 19:25:04 +03:00
Fedor
5f4f99245d feat: Добавлена поддержка привязки документов к HelpDesk
Изменения в upload_documents_to_crm.php:
 Добавлена логика привязки к HelpDesk (заявке) если указан ticket_id
 Если ticket_id не указан - привязываем к Project (старая логика)
 Обновлена функция normalizeInputData для передачи ticket_id
 Добавлено логирование привязки к HelpDesk/Project

Использование:
- Передать ticket_id в payload → документ привязывается к HelpDesk
- Не передавать ticket_id → документ привязывается к Project
2025-11-02 19:05:46 +03:00
Fedor
c60d00f563 feat: Создана операция CreateWebClaim для заявок через webservice
-  CreateWebClaim.php: создание заявок (HelpDesk) в vTiger
-  Обязательные поля: title, contact_id, project_id, event_type
-  Опциональные: description, incident_date, transport_number
-  Маппинг event_type на русские категории
-  Возврат: {ticket_id, ticket_number, title, category, status}
-  Зарегистрирована в БД: operationid=52
-  webservice.php: ob_get_clean + ob_start для очистки BOM
-  Логирование в logs/CreateWebClaim.log
2025-11-02 00:09:22 +03:00
Fedor
746367606e fix: CreateWebProject возвращает чистый JSON без BOM
-  CreateWebProject.php: возвращает массив (не json_encode)
-  CreateWebProject.php: ob_start/ob_end_clean для глушения warnings
-  Удалён closing tag ?> и лишние переносы
-  webservice.php: ob_clean() после includes (удаляет BOM)
-  Результат: чистый JSON без BOM и переносов
- 🎯 N8N теперь корректно парсит ответ

Связанные изменения:
- erv_platform: claim_id генерируется только в n8n
2025-11-01 16:53:20 +03:00
Fedor
901259f5b8 chore: Обновлён submodule erv_platform (лог сессии 01.11) 2025-11-01 13:39:23 +03:00
Fedor
f720c14ed1 chore: Обновлён submodule erv_platform 2025-11-01 13:37:03 +03:00
Fedor
c34f7c9bd9 docs: Документация для CreateWebProject 2025-11-01 12:52:41 +03:00
Fedor
af80214990 feat: Добавлена операция CreateWebProject для vTiger webservice
- Упрощённая операция создания проекта: обязательно policy_number + contact_id
- Логика: если проект с таким полисом существует - возвращает ID БЕЗ обновления
- Если не существует - создаёт новый проект
- Поля:
  * projectname: 'ERV {полис} цифровой адвокат'
  * projectstatus: 'модерация'
  * projecttype: 'ерв урегулирование'
  * linktoaccountscontacts: привязка к контакту
  * cf_1994: Заявитель (контрагент 67458)
  * cf_1885: номер полиса
  * cf_1887, cf_1889: периоды страхования (опционально)
- Возвращает JSON: {"project_id": "123", "is_new": true/false}
- Зарегистрировано в БД:
  * vtiger_ws_operation (id: 51)
  * vtiger_ws_operation_parameters (policy_number, contact_id, period_start, period_end)
- Логи: logs/CreateWebProject.log
- Протестировано: проект 396865 создан и повторно найден без дублирования
2025-11-01 12:51:54 +03:00
Fedor
6bedfbc429 📋 Добавлен лог сессии 01.11.2025 2025-11-01 12:23:47 +03:00
Fedor
7e3f0dcede Исправление путей к папкам проектов в Nextcloud + создание файлов из CRM
🔧 Исправления:
- Исправлены пути к папкам проектов: теперь /Documents/Project/{Name}_{Id}
- Исправлена функция openProjectFolder() во всех JS файлах
- Добавлены кнопки создания Word/Excel/PowerPoint из CRM (10 модулей)
- Создание файлов напрямую в S3 с автоиндексацией через Redis
- Исправлена ошибка 'Class Redis not found' (использован Predis)

📁 Изменённые файлы:
- layouts/v7/lib/nextcloud-editor.js
- crm_extensions/nextcloud_editor/js/nextcloud-editor.js
- layouts/v7/lib/nextcloud-editor-v3.js
- crm_extensions/file_storage/api/create_nextcloud_file.php
- layouts/v7/modules/*/DetailViewHeaderTitle.tpl (10 модулей)
- layouts/v7/modules/Documents/*.tpl (кнопки редактирования)

🎯 Результат:
- Кнопка 'Папка в Nextcloud' открывает правильную папку
- Создание файлов работает молниеносно (прямо в S3)
- Redis события публикуются корректно
- OnlyOffice открывается для редактирования

Проект 391552 теперь открывается по правильному пути!
2025-11-01 12:22:12 +03:00
Fedor
3a1635ec4d feat: OnlyOffice Document Server integration with auto-save
- Добавлен s3Path в callbackUrl для сохранения в оригинальный файл
- Исправлено сохранение: теперь файлы обновляются в S3 автоматически
- Отключена проверка SSL в OnlyOffice (rejectUnauthorized: false)
- Разрешены callback на приватные IP адреса
- Добавлено логирование callback в onlyoffice_callback.log
- Восстановлены оптимальные настройки индексации Nextcloud
- filesystem_check_changes = 0 для S3 External Storage
- Redis event system работает для автоматической индексации

Документация:
- ИСПРАВЛЕНИЕ_СОХРАНЕНИЯ_ONLYOFFICE_01_11_2025.md
- ИСПРАВЛЕНИЕ_SSL_ONLYOFFICE_01_11_2025.md
- ВОССТАНОВЛЕНИЕ_ОПТИМИЗАЦИИ_01_11_2025.md
- ONLYOFFICE_НАСТРОЙКИ.md
- ТЕСТИРОВАНИЕ_СОХРАНЕНИЯ_ONLYOFFICE.md
2025-11-01 10:32:51 +03:00
Fedor
3e8c3968a0 docs: Добавлена документация по OnlyOffice Standalone setup 2025-11-01 01:04:08 +03:00
Fedor
269c7ea216 feat: OnlyOffice Standalone integration with S3 direct URLs
 ЧТО СДЕЛАНО:
- Поднят новый standalone OnlyOffice Document Server (порт 8083)
- Настроен Nginx для доступа через office.clientright.ru:9443
- Создан open_file_v3_standalone.php для работы с новым OnlyOffice
- Реализована поддержка прямых S3 URL (bucket публичный)
- Добавлен s3_proxy.php с поддержкой Range requests
- Создан onlyoffice_callback.php для сохранения (базовая версия)
- Файлы успешно открываются и загружаются!

⚠️ TODO (на завтра):
- Доработать onlyoffice_callback.php для сохранения обратно в ОРИГИНАЛЬНЫЙ путь в S3
- Добавить Redis маппинг documentKey → S3 path
- Обновить CRM JS для использования open_file_v3_standalone.php
- Протестировать сохранение файлов
- Удалить тестовые файлы

📊 РЕЗУЛЬТАТ:
- OnlyOffice Standalone РАБОТАЕТ! 
- Файлы открываются напрямую из S3 
- Редактор загружается БЫСТРО 
- Автосохранение настроено  (но нужна доработка callback)
2025-11-01 01:02:03 +03:00
Fedor
d7941ac862 feat: CreateWebContact возвращает is_new флаг
- Return: {"contact_id": "123", "is_new": true/false}
- is_new = true: контакт создан сейчас
- is_new = false: контакт уже существовал
- Логируется в CreateWebContact.log
- Протестировано:
  * Новый 79194927999 → {contact_id: 396636, is_new: true}
  * Существующий 79001234567 → {contact_id: 396625, is_new: false}
2025-10-30 19:49:42 +03:00
Fedor
09c1fbd1db feat: Добавлена операция CreateWebContact для vTiger webservice
- Упрощённая операция создания контакта: обязательно только mobile
- Логика: если контакт существует - возвращает ID БЕЗ обновления
- Если не существует - создаёт с дефолтами (Клиент ERV_XXXX)
- Формат телефона: 79001234567 (без +)
- Зарегистрировано в БД:
  * vtiger_ws_operation (id: 50)
  * vtiger_ws_operation_parameters (mobile, firstname, lastname, email)
- Логи: logs/CreateWebContact.log
- Протестировано: контакт 396625 найден и возвращён без изменений
2025-10-30 19:21:34 +03:00
5094 changed files with 374039 additions and 2785 deletions

View File

@@ -1,10 +1,81 @@
{
"mcpServers": {
"context7": {
"url": "https://mcp.context7.com/mcp",
"headers": {
"CONTEXT7_API_KEY": "ctx7sk-541e7992-c38f-442f-8902-ae99645f2477"
}
}
"context7": {
"url": "https://mcp.context7.com/mcp",
"headers": {
"CONTEXT7_API_KEY": "ctx7sk-541e7992-c38f-442f-8902-ae99645f2477"
}
},
"shadcn": {
"command": "/usr/bin/docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-v", "/var/www/fastuser/data/www/crm.clientright.ru:/workspace",
"-w", "/workspace",
"node:20-alpine",
"npx",
"-y",
"shadcn@latest",
"mcp"
],
"env": {}
},
"antd-components": {
"command": "/usr/bin/docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-v", "/var/www/fastuser/data/www/crm.clientright.ru:/workspace",
"-w", "/workspace",
"node:20-alpine",
"npx",
"-y",
"@jzone-mcp/antd-components-mcp"
],
"env": {}
},
"n8n-mcp": {
"command": "docker",
"args": [
"run",
"-i",
"--rm",
"--init",
"-e", "MCP_MODE=stdio",
"-e", "LOG_LEVEL=error",
"-e", "DISABLE_CONSOLE_OUTPUT=true",
"-e", "N8N_API_URL=https://n8n.clientright.pro/",
"-e", "N8N_API_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5MzMwYWVjZC1hYjExLTQxODEtOWIyYy1iMDZhZWEzMTNmNzQiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzUzNjg3NDM4fQ.XJjyYXXOoO4eUGAfkSVRMJzLYvi25hczsp2F7j4UV7Y",
"ghcr.io/czlonkowski/n8n-mcp:latest"
]
},
"memory": {
"url": "http://185.197.75.249:9000/sse"
}
,
"n8n-mcp2": {
"command": "npx",
"args": [
"-y",
"supergateway",
"--streamableHttp",
"https://n8n.clientright.pro/mcp-server/http",
"--header",
"authorization:Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5MzMwYWVjZC1hYjExLTQxODEtOWIyYy1iMDZhZWEzMTNmNzQiLCJpc3MiOiJuOG4iLCJhdWQiOiJtY3Atc2VydmVyLWFwaSIsImp0aSI6Ijc5MTQzNDU5LTM0NzMtNDQ2Mi05MzU1LTZmMTAzZTdlMzAxNCIsImlhdCI6MTc2NDU3OTI0N30.g-WQpgWMjdFWVpUMaRP023MCQqfk_e3ollLyPpcE_Io"
]
}
}
}
}

96
AI_DRAWER_DEBUG.md Normal file
View File

@@ -0,0 +1,96 @@
# 🔍 Диагностика проблемы AI Drawer
## Проблема
Ошибка: "Ошибка при получении ответа. Попробуйте еще раз."
## Что проверить
### 1. Проверить формат сообщения от n8n
n8n может публиковать сообщение в двух форматах:
**Формат 1 (просто текст):**
```
"Текст ответа от AI"
```
**Формат 2 (JSON объект):**
```json
{
"task_id": "task-123",
"response": "Текст ответа",
"status": "completed"
}
```
### 2. Проверить Redis ключ
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
GET "ai:response:cache:task-691209e225894-1762789858"
```
Если ключ есть → ответ сохранен, но SSE не получил
Если ключа нет → n8n не сохраняет в ключ (нужно настроить)
### 3. Проверить логи SSE
В консоли браузера должны быть логи:
- `AI Drawer: SSE connection opened`
- `AI Drawer: Received response via SSE`
Если их нет → SSE не подключается
### 4. Проверить публикацию в канал
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
MONITOR
```
Затем отправьте сообщение в AI Drawer - должны видеть PUBLISH команду
## Решение
### Если n8n публикует только в канал (без ключа):
1. **Добавьте Redis SET ноду в n8n** перед PUBLISH:
- Operation: `Set`
- Key: `ai:response:cache:{{ $json.taskId }}`
- Value: JSON с ответом
- TTL: 300 секунд
2. **Или** используйте текущий код - SSE endpoint сам сохранит в ключ когда получит из канала
### Если SSE не подключается:
1. Проверьте что `/aiassist/ai_sse.php` доступен
2. Проверьте логи PHP на ошибки
3. Проверьте консоль браузера на ошибки CORS/сети
## Текущая архитектура
```
n8n → Redis PUBLISH (канал) → SSE endpoint получает → сохраняет в ключ → отправляет браузеру
Если SSE не получил → fallback проверяет ключ
```
## Что исправлено
✅ SSE endpoint теперь:
- Принимает и JSON и простой текст
- Сохраняет ответ в Redis ключ при получении
- Проверяет ключ при подключении (на случай если ответ уже есть)
✅ JavaScript теперь:
- Не вызывает fallback если уже получил ответ
- Проверяет Redis ключ периодически если SSE не работает
- Логирует все действия для отладки
## Следующие шаги
1. Проверьте что n8n сохраняет в ключ `ai:response:cache:{taskId}` ПЕРЕД публикацией
2. Проверьте логи в консоли браузера
3. Проверьте логи PHP (error_log)

130
AI_DRAWER_REDIS_SSE.md Normal file
View File

@@ -0,0 +1,130 @@
# AI Drawer: Redis Pub/Sub вместо Polling
## ✅ Что сделано
### Проблема
Раньше использовался polling - браузер каждые 2 секунды спрашивал сервер "готово ли?". Это создавало:
- Лишние запросы к серверу
- Задержку до 2 секунд перед получением ответа
- Нагрузку на БД
- Дублирование истории (БД + n8n)
### Решение
Теперь используется **Redis Pub/Sub + SSE (Server-Sent Events)** с прямой публикацией из n8n:
- ✅ Мгновенная доставка ответов (без задержек)
- ✅ Нет лишних запросов (одно SSE соединение)
- ✅ Меньше нагрузка на сервер
- ✅ Нет дублирования - история только в n8n
- ✅ Упрощенная архитектура - без БД и callback
- ✅ Fallback на polling если SSE не работает
## 📋 Архитектура
```
1. AI Drawer → n8n_proxy.php → возвращает task_id
2. n8n обрабатывает → публикует ответ НАПРЯМУЮ в Redis
3. Redis PUBLISH → канал "ai:response:{task_id}"
4. ai_sse.php → подписан на Redis → отправляет через SSE в браузер
5. Браузер → EventSource → получает ответ мгновенно! ⚡
```
**История диалога:** Сохраняется в n8n автоматически (не дублируется в БД)
## 📁 Измененные файлы
### 1. `/aiassist/ai_sse.php` (новый)
SSE endpoint для подписки на Redis события
### 2. `/aiassist/n8n_proxy.php` (упрощен)
- ❌ Убрано сохранение в БД
- ❌ Убран callback URL
- ✅ Добавлены параметры Redis для n8n
### 3. `/callback_ai_response.php` (больше не используется)
Можно удалить - n8n публикует напрямую в Redis
### 4. `/layouts/v7/resources/js/ai-drawer-simple.js` (обновлен)
- Заменен `startPolling()` на `startSSEListener()`
- Добавлен fallback на polling если SSE не работает
- Добавлено поле `currentEventSource` для управления SSE соединением
## 🔧 Как работает
### Отправка запроса:
```javascript
// Пользователь отправляет сообщение
sendToN8N(message) получает task_id startSSEListener(task_id)
```
### Получение ответа:
```javascript
// SSE соединение открывается один раз
EventSource('/aiassist/ai_sse.php?task_id=123')
// n8n обрабатывает и публикует НАПРЯМУЮ в Redis:
Redis PUBLISH "ai:response:123" {
task_id: "123",
response: "...",
status: "completed"
}
// SSE endpoint получает событие и отправляет в браузер
// Браузер получает ответ мгновенно!
```
### Настройка n8n:
См. подробную инструкцию: `N8N_REDIS_SETUP.md`
## 🛡️ Fallback механизм
Если SSE не работает (старые браузеры, проблемы с сетью):
1. Через 5 секунд автоматически переключается на polling
2. Использует старый метод `startPollingFallback()`
3. Проверяет БД каждые 2 секунды
## ⚙️ Настройки Redis
- **Host**: `crm.clientright.ru`
- **Port**: `6379`
- **Password**: `CRM_Redis_Pass_2025_Secure!`
- **Канал**: `ai:response:{task_id}`
## 🧪 Тестирование
1. Откройте AI Drawer в CRM
2. Отправьте сообщение
3. Проверьте консоль браузера:
- `AI Drawer: SSE connection opened`
- `AI Drawer: Received response via SSE`
4. Ответ должен прийти мгновенно после обработки n8n
## 📊 Преимущества
| Параметр | Polling (старое) | Redis Pub/Sub (новое) |
|----------|------------------|----------------------|
| Скорость | До 2 сек задержки | Мгновенно ⚡ |
| Запросы | Каждые 2 сек | Одно соединение |
| Нагрузка | Высокая | Низкая |
| Надежность | ✅ | ✅ + fallback |
## 🔍 Отладка
### Проверить Redis публикацию:
```bash
redis-cli -h crm.clientright.ru -a 'CRM_Redis_Pass_2025_Secure!' \
PUBLISH "ai:response:test-task" '{"task_id":"test-task","response":"test"}'
```
### Проверить SSE endpoint:
```bash
curl -N "https://crm.clientright.ru/aiassist/ai_sse.php?task_id=test-task"
```
### Логи:
- PHP error_log: `/var/log/php/error.log`
- Ищите: `[AI SSE]` и `[Callback]`
## ✅ Результат
Теперь AI Drawer получает ответы **мгновенно** через Redis Pub/Sub вместо ожидания polling каждые 2 секунды!

View File

@@ -0,0 +1,105 @@
# Лог сессии: Реализация SSE + Redis Pub/Sub для AI Drawer
**Дата:** 11 ноября 2025
**Задача:** Переход с polling на real-time коммуникацию через SSE и Redis Pub/Sub
## Проблема
Изначально использовался механизм polling:
- n8n отправлял ответ в `callback_ai_response.php`
- Ответ сохранялся в БД таблицу `ai_responses`
- Frontend периодически опрашивал `get_ai_result.php`
- Неэффективно и не real-time
## Решение
Реализована архитектура на основе:
- **Server-Sent Events (SSE)** для real-time доставки ответов
- **Redis Pub/Sub** для передачи сообщений от n8n
- **Redis кэш** для fallback механизма
## Реализованные компоненты
### 1. SSE Endpoint (`aiassist/ai_sse.php`)
- Подписывается на Redis канал `ai:response:{taskId}`
- Отправляет события через SSE
- Поддерживает plain text и JSON форматы от n8n
- Кэширует ответы в Redis для надежности
- Логирование в `/logs/ai_sse_debug.log`
### 2. Обновлен `n8n_proxy.php`
- Убран callback URL
- Добавлена передача Redis параметров в n8n:
- `redisChannel`
- `redisHost`
- `redisPort`
- `redisPassword`
### 3. Обновлен `ai-drawer-simple.js`
- Заменен `startPolling()` на `startSSEListener()`
- Использует `EventSource` API для SSE
- Fallback через `checkRedisDirectly()` если SSE не работает
- Обработка событий: `connected`, `response`, `error`, `heartbeat`
### 4. Fallback endpoint (`aiassist/check_redis_response.php`)
- Прямое чтение из Redis кэша
- Используется если SSE не подключился или потерял соединение
## Настройка n8n
### Redis Node Configuration:
- **Operation:** Publish
- **Channel:** `{{ $json.body.redisChannel }}` или `ai:response:{{ $json.body.taskId }}`
- **Data:** `{{ $json.output }}` (можно plain text или JSON)
### Рекомендуется:
1. Сначала выполнить `Redis SET` в ключ `ai:response:cache:{taskId}` (TTL 300 сек)
2. Затем выполнить `Redis PUBLISH` в канал `ai:response:{taskId}`
## Тестирование
### Успешные тесты:
- ✅ Получение plain text ответов от n8n
- ✅ Кэширование в Redis
- ✅ SSE доставка ответов
- ✅ Fallback механизм работает
### Логи:
- Файл: `/logs/ai_sse_debug.log`
- Последние успешные запросы:
- `task-691326da57708-1762862810` (15:07:07)
- `task-6913281c9ff7e-1762863132` (15:12:36)
## Преимущества новой архитектуры
1. **Real-time:** Ответы приходят мгновенно через SSE
2. **Надежность:** Fallback через Redis кэш
3. **Гибкость:** Поддержка plain text и JSON форматов
4. **Производительность:** Нет постоянного polling
5. **Масштабируемость:** Redis Pub/Sub поддерживает множество подписчиков
## Файлы в репозитории
### Новые файлы:
- `aiassist/ai_sse.php`
- `aiassist/check_redis_response.php`
- `AI_DRAWER_REDIS_SSE.md`
- `N8N_REDIS_SETUP.md`
- `N8N_REDIS_FIX.md`
### Измененные файлы:
- `aiassist/n8n_proxy.php`
- `layouts/v7/resources/js/ai-drawer-simple.js`
## Git коммит
```
1a465329 Реализован SSE + Redis Pub/Sub для AI Drawer
```
## Статус
**Работает в продакшене**
**Все изменения запушены в Git**
**Документация обновлена**

90
API_ATTACH_DOCS_README.md Normal file
View File

@@ -0,0 +1,90 @@
# 📎 API для привязки документов
## ✅ Прямой эндпоинт (готов к использованию!)
```
POST https://crm.clientright.ru/api_attach_documents.php
```
---
## 🚀 Формат запроса (из n8n)
```json
[
{
"contact_id": "320096",
"project_id": "396868",
"ticket_id": "396936",
"filename": "boarding_pass.pdf",
"file_type": "flight_delay_boarding_or_ticket",
"file": "/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/crm2/CRM_Active_Files/Documents/HelpDesk/ЗАЯВКА_827_396936/flight_delay_boarding_or_ticket.pdf"
}
]
```
**Важно:**
- ✅ Массив `[...]` (даже для одного документа)
-`file` без хоста → автоматически добавится `https://s3.twcstorage.ru`
-`ticket_id` опционально (если есть → HelpDesk, иначе → Project)
---
## 📊 Формат ответа
```json
{
"success": true,
"total_processed": 1,
"successful": 1,
"failed": 0,
"results": [
{
"document_id": "15x396941",
"document_numeric_id": "396941",
"attached_to": "ticket",
"attached_to_id": "396936",
"file_name": "boarding_pass.pdf",
"file_type": "flight_delay_boarding_or_ticket",
"s3_bucket": "f9825c87-...",
"s3_key": "crm2/CRM_Active_Files/...",
"file_size": 85320,
"message": "Документ создан и привязан..."
}
],
"errors": null
}
```
---
## 🧪 Тест
```bash
curl -X POST "https://crm.clientright.ru/api_attach_documents.php" \
-H "Content-Type: application/json" \
-d '[
{
"contact_id": "320096",
"project_id": "396868",
"ticket_id": "396936",
"filename": "test.pdf",
"file_type": "flight_delay_boarding_or_ticket",
"file": "/bucket/path/to/file.pdf"
}
]'
```
---
## 🔍 Логи
```bash
tail -f /var/www/fastuser/data/www/crm.clientright.ru/logs/api_attach_documents.log
```
---
## 🎯 Готово к использованию в n8n!

238
CREATE_WEB_PROJECT_DOCS.md Normal file
View File

@@ -0,0 +1,238 @@
# CreateWebProject - Документация
**Дата создания:** 01.11.2025
**Автор:** Фёдор
**Статус:** ✅ Работает и протестировано
---
## 📋 Назначение
Операция vTiger webservice для создания проекта из web-формы ERV.
**Логика работы:**
- Если проект с таким номером полиса **существует** → возвращает ID **БЕЗ обновления**
- Если проект **не существует** → создаёт новый
По аналогии с `CreateWebContact`.
---
## 🔧 Параметры
### Обязательные:
1. **policy_number** (String) - Номер полиса ERV (cf_1885)
- Пример: `E1000-123456789`
2. **contact_id** (String) - ID контакта для привязки
- Пример: `396625`
- Получается из `CreateWebContact` на шаге 1
### Опциональные:
3. **period_start** (String) - Дата начала страхования (cf_1887)
- Формат: `DD-MM-YYYY`
- Пример: `01-01-2025`
4. **period_end** (String) - Дата окончания страхования (cf_1889)
- Формат: `DD-MM-YYYY`
- Пример: `31-12-2025`
---
## 📥 Пример запроса
```bash
curl -X POST "https://crm.clientright.ru/webservice.php" \
-d "operation=CreateWebProject" \
-d "sessionName={token от login}" \
-d "policy_number=E1000-123456789" \
-d "contact_id=396625" \
-d "period_start=01-01-2025" \
-d "period_end=31-12-2025"
```
---
## 📤 Пример ответа
### Новый проект (создан):
```json
{
"success": true,
"result": "{\"project_id\":\"396865\",\"is_new\":true}"
}
```
### Существующий проект (найден):
```json
{
"success": true,
"result": "{\"project_id\":\"396865\",\"is_new\":false}"
}
```
**Важно:** `result` - это JSON-строка, требует `JSON.parse()`!
---
## 🏗️ Создаваемые поля проекта
При создании нового проекта заполняются:
| Поле | Значение | Примечание |
|------|----------|------------|
| `projectname` | `ERV {полис} цифровой адвокат` | Например: "ERV E1000-123456789 цифровой адвокат" |
| `projectstatus` | `модерация` | Статус |
| `projecttype` | `ерв урегулирование` | Тип проекта |
| `linktoaccountscontacts` | `12x{contact_id}` | Привязка к контакту |
| `cf_1994` | `11x67458` | Заявитель (контрагент) |
| `cf_1885` | `{policy_number}` | Номер полиса ⭐ |
| `cf_1887` | `{period_start}` | Период начала (опционально) |
| `cf_1889` | `{period_end}` | Период конца (опционально) |
| `assigned_user_id` | Текущий API user | Ответственный |
---
## 🔍 Логика поиска
Проект ищется в БД по запросу:
```sql
SELECT p.projectid
FROM vtiger_project p
INNER JOIN vtiger_projectcf pcf ON p.projectid = pcf.projectid
LEFT JOIN vtiger_crmentity e ON e.crmid = p.projectid
WHERE e.deleted = 0 AND pcf.cf_1885 = '{policy_number}'
LIMIT 1
```
**Поиск по:** `cf_1885` (номер полиса)
---
## 📁 Файлы
- **Handler:** `include/Webservices/CreateWebProject.php`
- **Handler Method:** `vtws_createwebproject`
- **Логи:** `logs/CreateWebProject.log`
---
## 🗄️ Регистрация в БД
### vtiger_ws_operation:
```
operationid: 51
name: CreateWebProject
handler_path: include/Webservices/CreateWebProject.php
handler_method: vtws_createwebproject
type: POST
prelogin: 0
```
### vtiger_ws_operation_parameters:
```
1. policy_number (String)
2. contact_id (String)
3. period_start (String)
4. period_end (String)
```
---
## ✅ Тестирование
### Тест 1: Создание нового проекта
```
Полис: E1000-TEST-1761990646
Contact: 396625
Результат: {"project_id":"396865","is_new":true}
Статус: ✅ Создан
```
### Тест 2: Повторный вызов с тем же полисом
```
Полис: E1000-TEST-1761990646
Contact: 396625
Результат: {"project_id":"396865","is_new":false}
Статус: ✅ Найден существующий (НЕ создан дубликат!)
```
---
## 🔗 Интеграция с n8n
### Workflow флоу (планируется):
```
1. Step1Phone → CreateWebContact
↓ contact_id
2. Step2Policy → Проверка полиса
↓ policy_number, period_start, period_end
3. n8n → CreateWebProject
POST https://crm.clientright.ru/webservice.php
{
operation: CreateWebProject,
sessionName: {token},
policy_number: "E1000-123456789",
contact_id: "396625",
period_start: "01-01-2025",
period_end: "31-12-2025"
}
4. Response → {"project_id": "396865", "is_new": false}
5. Redis session update:
claim:{claim_id} += {
project_id: "396865",
is_new_project: false
}
```
---
## 📊 Связь с CreateWebContact
| Операция | Ищет по | Создаёт если | Возвращает |
|----------|---------|--------------|------------|
| `CreateWebContact` | `mobile` (телефон) | Не найден контакт | `{contact_id, is_new}` |
| `CreateWebProject` | `cf_1885` (полис) | Не найден проект | `{project_id, is_new}` |
**Флоу:** Телефон → Контакт → Полис → Проект → Тип события → Документы → Тикет
---
## 🐛 Troubleshooting
### Ошибка: "Unknown operation requested"
**Причина:** Операция не зарегистрирована в БД.
**Решение:** Запустить `register_CreateWebProject_fixed.php` (уже выполнено).
### Ошибка: BOM символ в ответе
**Причина:** Файл CreateWebProject.php сохранён с UTF-8 BOM.
**Решение:** `sed -i '1s/^\xEF\xBB\xBF//' include/Webservices/CreateWebProject.php` (уже выполнено).
### Проект создаётся дубликатом
**Причина:** Номер полиса отличается (пробелы, регистр).
**Решение:** В коде есть `trim()`, но нет приведения к верхнему регистру. Если нужно - добавить.
---
## 📝 История изменений
**01.11.2025:**
- ✅ Создан файл CreateWebProject.php
- ✅ Зарегистрирован в БД (operationid: 51)
- ✅ Протестирован (создание + поиск)
- ✅ Закоммичен в master (af802149)
---
**Готово к использованию!** 🎉

67
FINAL_STEPS.sh Normal file
View File

@@ -0,0 +1,67 @@
#!/bin/bash
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "🚀 ФИНАЛЬНЫЕ ШАГИ - Сохранение и перезапуск"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# 1. Git Commit
echo "📦 Шаг 1: Git Commit..."
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform
git add -A
git commit -m "fix: OCR endpoint исправлен /process → /analyze-file
Проблема:
❌ POST http://147.45.146.17:8001/process → 404 Not Found
❌ OCR не работал
❌ Gemini Vision не получал данные
Решение:
✅ Endpoint изменен на /analyze-file (правильный)
✅ Исправлено в 3 местах
✅ Добавлено детальное логирование
Теперь OCR работает!"
git push origin main
echo "✅ Git push выполнен!"
echo ""
# 2. Перезапуск Backend
echo "🔄 Шаг 2: Перезапуск Backend..."
pkill -9 -f "uvicorn app.main"
sleep 2
cd /var/www/fastuser/data/www/crm.clientright.ru/erv_platform/backend
source venv/bin/activate
nohup python -m uvicorn app.main:app --host 0.0.0.0 --port 8100 > ../../erv_platform_backend.log 2>&1 &
sleep 3
echo "✅ Backend перезапущен!"
echo ""
# 3. Проверка
echo "🔍 Шаг 3: Проверка сервисов..."
curl -s http://localhost:8100/health | python3 -m json.tool
echo ""
docker ps | grep frontend
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "✅ ВСЁ ГОТОВО!"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
echo "🌐 Открывай и тестируй:"
echo " http://147.45.146.17:5173"
echo ""
echo "🔍 Смотри логи:"
echo " tail -f /var/www/fastuser/data/www/crm.clientright.ru/erv_platform_backend.log"
echo ""
echo "При загрузке файла увидишь:"
echo " 🔍 Starting OCR for: filename"
echo " 📄 OCR completed: XXX chars"
echo " 🤖 Starting AI analysis"
echo " ✅ AI: policy, confidence: 95%"
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

@@ -0,0 +1,219 @@
# 📝 Лог сессии: Реализация генерации документов для AI Ассистента
**Дата:** 2025-01-12
**Участники:** Фёдор, AI Assistant
**Тема:** Создание инструмента для генерации документов из шаблонов и с текстом от AI
---
## 🎯 Цель сессии
Реализовать функционал генерации документов (претензий, исков, жалоб, ходатайств) для AI Ассистента с возможностью использования шаблонов Nextcloud и форматирования Markdown.
---
## 📋 Обсужденные вопросы
### 1. Шаблонизация документов в Nextcloud
**Вопрос:** Как настроить шаблоны в Nextcloud? Есть ли понятный механизм шаблонизирования?
**Анализ:**
- Проверен API Nextcloud Direct Editing - endpoint `/templates` не существует
- Найдена папка `/Templates/` в корне пользователя admin
- ONLYOFFICE хранит "Общие шаблоны" отдельно от обычной папки Templates
- Шаблоны доступны через WebDAV PROPFIND
**Решение:**
- Использовать WebDAV для получения списка шаблонов
- Создать API для работы с шаблонами через WebDAV
- Поддержать заполнение переменных через PHPWord
### 2. Процесс создания документов AI Ассистентом
**Вопрос:** Как AI Ассистент будет создавать документы?
**Решение:**
1. Пользователь просит создать документ
2. AI Drawer отправляет запрос в n8n
3. n8n → GPT-4 анализирует запрос и генерирует текст
4. n8n вызывает API создания документа
5. API создает DOCX с текстом (поддержка Markdown форматирования)
6. Документ сохраняется в S3 в папку проекта
7. Возвращается ссылка на редактирование в OnlyOffice
### 3. Форматирование документов
**Вопрос:** Можно ли сделать красивое форматирование документов?
**Решение:** ✅ Да! Реализована поддержка Markdown:
- Заголовки: `# H1`, `## H2`, `### H3`
- Жирный: `**текст**` или `__текст__`
- Курсив: `*текст*` или `_текст_`
- Код: `` `текст` ``
- Маркированные списки: `- пункт` или `* пункт`
- Нумерованные списки: `1. пункт`
---
## 🔧 Реализованные компоненты
### 1. API создания документов с текстом
**Файл:** `/crm_extensions/file_storage/api/create_document_with_text.php`
**Функционал:**
- Создает DOCX/XLSX/PPTX с текстом от AI
- Поддержка Markdown форматирования
- Сохранение в S3 в правильную папку проекта
- Возврат ссылки на редактирование в OnlyOffice
**Особенности:**
- Поддержка JSON POST запросов
- Fallback на простой DOCX если PHPWord недоступен
- Правильная обработка пробелов (замена на подчеркивания)
- Правильный путь: `crm2/CRM_Active_Files/Documents/...` (без `/crm/` в начале)
### 2. API создания документов из шаблонов
**Файл:** `/crm_extensions/file_storage/api/create_from_template.php`
**Функционал:**
- Скачивает шаблон из Nextcloud через WebDAV
- Заполняет переменные через PHPWord
- Сохраняет готовый документ в папку проекта
### 3. API получения списка шаблонов
**Файл:** `/crm_extensions/file_storage/api/list_templates.php`
**Функционал:**
- Получает список шаблонов из Nextcloud через WebDAV PROPFIND
- Фильтрует только Office файлы
- Возвращает JSON с метаданными
### 4. Установка PHPWord
**Команда:**
```bash
composer require phpoffice/phpword
```
**Результат:**
- ✅ PHPWord 1.4.0 установлен
- ✅ Поддержка форматирования Markdown
- ✅ Красивое оформление документов
---
## 📝 Исправленные проблемы
### Проблема 1: PHPWord не установлен
- **Решение:** Установлен через composer
- **Дополнительно:** Добавлен fallback на простой DOCX через ZIP
### Проблема 2: JSON POST не обрабатывался
- **Решение:** Добавлена проверка Content-Type и парсинг JSON из php://input
### Проблема 3: Неправильный путь к файлам
- **Было:** `/crm/crm2/CRM_Active_Files/...`
- **Стало:** `crm2/CRM_Active_Files/...`
- **Решение:** Исправлен путь в `create_document_with_text.php`
### Проблема 4: Пробелы в именах папок
- **Было:** `Крылова ГБУ ЖИЛИЩНИК...`
- **Стало:** `Крылова_ГБУ_ЖИЛИЩНИК...`
- **Решение:** Добавлена замена пробелов на подчеркивания в `recordName`
---
## 📚 Созданная документация
1. **AI_DOCUMENT_TOOL_INSTRUCTION.md** - Инструкция для AI Ассистента
2. **AI_DOCUMENT_GENERATION_FLOW.md** - Описание процесса создания документов
3. **MARKDOWN_FORMATTING.md** - Справочник по Markdown форматированию
4. **NEXTCLOUD_TEMPLATES.md** - Работа с шаблонами Nextcloud
5. **NEXTCLOUD_TEMPLATES_API_ANALYSIS.md** - Анализ API шаблонов
6. **ONLYOFFICE_TEMPLATES_ANALYSIS.md** - Анализ шаблонов ONLYOFFICE
7. **N8N_HTTP_REQUEST_CURL.md** - cURL команды для n8n
---
## 🎯 Результаты
### ✅ Реализовано:
1. **API создания документов** - работает, протестирован
2. **Поддержка Markdown** - заголовки, жирный, курсив, списки, код
3. **Правильные пути** - документы сохраняются в правильную структуру
4. **Обработка пробелов** - автоматическая замена на подчеркивания
5. **PHPWord установлен** - красивое форматирование документов
6. **Документация** - полная инструкция для AI и разработчиков
### 📊 Статистика:
- **Создано файлов:** 7+ (API, документация)
- **Установлено библиотек:** PHPWord 1.4.0
- **Исправлено проблем:** 4
- **Поддерживаемых форматов:** DOCX, XLSX, PPTX
- **Поддерживаемых элементов Markdown:** 6 типов
---
## 🚀 Следующие шаги
1. **Настроить в n8n:**
- Добавить HTTP Request ноду для создания документов
- Подключить к AI workflow
- Протестировать создание документов
2. **Улучшения (опционально):**
- Добавить поддержку шаблонов с переменными
- Расширенное форматирование (таблицы, изображения)
- Автоматическое определение типа документа
3. **Интеграция с AI:**
- Добавить инструмент в список доступных для AI
- Протестировать генерацию документов через AI Drawer
---
## 📁 Измененные файлы
### Новые файлы:
- `/crm_extensions/file_storage/api/create_document_with_text.php`
- `/crm_extensions/file_storage/api/create_from_template.php`
- `/crm_extensions/file_storage/api/list_templates.php`
- `/crm_extensions/file_storage/docs/AI_DOCUMENT_TOOL_INSTRUCTION.md`
- `/crm_extensions/file_storage/docs/AI_DOCUMENT_GENERATION_FLOW.md`
- `/crm_extensions/file_storage/docs/MARKDOWN_FORMATTING.md`
- `/crm_extensions/file_storage/docs/NEXTCLOUD_TEMPLATES.md`
- `/crm_extensions/file_storage/docs/NEXTCLOUD_TEMPLATES_API_ANALYSIS.md`
- `/crm_extensions/file_storage/docs/ONLYOFFICE_TEMPLATES_ANALYSIS.md`
- `/crm_extensions/file_storage/docs/N8N_HTTP_REQUEST_CURL.md`
### Обновленные файлы:
- `composer.json` - добавлен phpoffice/phpword
- `composer.lock` - обновлен после установки PHPWord
---
## 💡 Ключевые решения
1. **Использование Markdown** - стандартный синтаксис, понятный AI
2. **WebDAV вместо API** - надежнее, работает всегда
3. **Fallback механизм** - работает даже без PHPWord
4. **Правильная структура путей** - соответствует существующей системе
---
## ✅ Статус: Готово к использованию
Все компоненты реализованы, протестированы и готовы к использованию в n8n workflow.

155
N8N_REDIS_FIX.md Normal file
View File

@@ -0,0 +1,155 @@
# 🔧 Исправление конфигурации n8n для Redis публикации
## ❌ Проблема в текущей конфигурации
```json
{
"channel": "=ai:response:{{ $('Edit Fields').item.json.taskId }}",
"messageData": "={{ JSON.stringify($json.output) }}"
}
```
**Проблемы:**
1. ❌ Канал использует `$('Edit Fields').item.json.taskId` - неправильный путь
2.`messageData` содержит `$json.output` - неправильный формат
3. ❌ Нет сохранения в Redis ключ для fallback
## ✅ Правильная конфигурация
### Вариант 1: Если taskId в корне webhook body
**Channel:**
```
ai:response:{{ $json.taskId }}
```
**Message (JSON объект):**
```json
{
"task_id": "{{ $json.taskId }}",
"response": "{{ $json.output }}",
"status": "completed"
}
```
### Вариант 2: Если taskId в webhook.body
**Channel:**
```
ai:response:{{ $json.webhook.body.taskId }}
```
**Message (JSON объект):**
```json
{
"task_id": "{{ $json.webhook.body.taskId }}",
"response": "{{ $json.output }}",
"status": "completed"
}
```
### Вариант 3: Если ответ в другой ноде (например, AI Chat)
**Channel:**
```
{{ $json.webhook.body.redisChannel }}
```
**Message (JSON объект):**
```json
{
"task_id": "{{ $json.webhook.body.taskId }}",
"response": "{{ $json['AI Chat'].json.response }}",
"status": "completed"
}
```
## 📋 Полная настройка n8n workflow
### Шаг 1: Redis SET (сохранить в ключ для fallback)
**Operation:** `Set`
**Key:** `ai:response:cache:{{ $json.webhook.body.taskId }}`
**Value:**
```json
{
"task_id": "{{ $json.webhook.body.taskId }}",
"response": "{{ $json['AI Chat'].json.response }}",
"status": "completed",
"timestamp": "{{ $now.toISO() }}"
}
```
**TTL:** `300` секунд
### Шаг 2: Redis PUBLISH (опубликовать в канал для SSE)
**Operation:** `Publish`
**Channel:** `{{ $json.webhook.body.redisChannel }}`
**Message:**
```json
{
"task_id": "{{ $json.webhook.body.taskId }}",
"response": "{{ $json['AI Chat'].json.response }}",
"status": "completed"
}
```
## 🔍 Как найти правильный путь к данным
1. **Добавьте ноду "Set" перед Redis:**
- Сохраните все данные из предыдущих нод
- Посмотрите структуру данных в n8n
2. **Используйте Expression Editor в n8n:**
- Нажмите на поле "Channel" или "Message"
- Выберите "Expression"
- Начните вводить `$json.` - увидите доступные поля
3. **Проверьте webhook body:**
- В ноде Webhook посмотрите что приходит
- `taskId` и `redisChannel` должны быть в `$json.webhook.body`
## ✅ Проверка
После настройки проверьте:
1. **В n8n:**
- Запустите workflow
- Проверьте что Redis ноды выполнились успешно
- Посмотрите что именно публикуется в канал
2. **В Redis:**
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
MONITOR
```
Должны видеть:
- `SET ai:response:cache:task-xxx ...`
- `PUBLISH ai:response:task-xxx ...`
3. **В браузере:**
- Откройте консоль (F12)
- Должны видеть: `AI Drawer: SSE connection opened`
- Должны видеть: `AI Drawer: Received response via SSE`
## 🐛 Отладка
Если не работает:
1. **Проверьте формат сообщения:**
- Должен быть валидный JSON
- Должно быть поле `response` или `task_id`
2. **Проверьте канал:**
- Должен совпадать с `redisChannel` из `n8n_proxy.php`
- Формат: `ai:response:task-xxx`
3. **Проверьте логи PHP:**
```bash
tail -f /var/log/apache2/error.log | grep "AI SSE"
```
4. **Проверьте что SSE endpoint доступен:**
- Откройте в браузере: `/aiassist/ai_sse.php?task_id=test-123`
- Должен открыться поток SSE (не ошибка)

185
N8N_REDIS_SETUP.md Normal file
View File

@@ -0,0 +1,185 @@
# Настройка n8n для прямой публикации в Redis
## 🎯 Цель
Настроить n8n workflow так, чтобы после обработки AI ответа он публиковал результат **напрямую в Redis**, без промежуточного callback.
## 📋 Архитектура
```
AI Drawer → n8n_proxy.php → n8n webhook
[Обработка AI]
Redis PUBLISH → ai:response:{taskId}
SSE → браузер получает мгновенно! ⚡
```
## 🔧 Настройка n8n Workflow
### Шаг 1: Добавить Redis ноду после обработки AI
В вашем n8n workflow после ноды обработки AI добавьте **Redis ноду**:
1. **Тип ноды**: `Redis`
2. **Operation**: `Publish`
### Шаг 2: Настройки Redis ноды
**Connection:**
```
Host: crm.clientright.ru
Port: 6379
Password: CRM_Redis_Pass_2025_Secure!
Database: 0
```
**Operation Settings:**
```
Operation: Publish
Channel: {{ $json.redisChannel }}
```
**Message (вариант 1 - JSON объект, рекомендуется):**
```json
{
"task_id": "{{ $json.taskId }}",
"status": "completed",
"response": "{{ $json.aiResponse }}",
"timestamp": "{{ $now.toISO() }}"
}
```
**Message (вариант 2 - просто текст, тоже работает):**
```
{{ $json.aiResponse }}
```
⚠️ **Важно:** SSE endpoint поддерживает оба формата:
- JSON объект с полем `response` - предпочтительно
- Просто текст ответа - тоже работает (автоматически обрабатывается)
### Шаг 2.5: Сохранение в Redis ключ (ВАЖНО для fallback)
⚠️ **КРИТИЧНО:** Сохраняйте ответ в Redis ключ **ПЕРЕД** публикацией в канал!
**Порядок действий в n8n:**
1. Обработка AI → получен ответ
2. **Сначала:** Redis SET → сохранить в ключ `ai:response:cache:{taskId}` (TTL 300 сек)
3. **Потом:** Redis PUBLISH → опубликовать в канал `ai:response:{taskId}`
**Добавьте Redis ноду для SET (перед PUBLISH):**
**Operation:** `Set`
**Key:** `ai:response:cache:{{ $json.taskId }}`
**Value:**
```json
{
"task_id": "{{ $json.taskId }}",
"response": "{{ $json.aiResponse }}",
"status": "completed",
"timestamp": "{{ $now.toISO() }}"
}
```
**TTL:** `300` секунд (5 минут)
**Зачем это нужно:**
- Если SSE не подписался вовремя → fallback найдет ответ в ключе
- Если браузер перезагрузился → ответ все еще доступен
- Надежность: двойное сохранение (канал + ключ)
### Шаг 3: Канал Redis
Канал формируется автоматически из `taskId`:
```
ai:response:{{ $json.taskId }}
```
Или используйте значение из входящего запроса:
```
{{ $json.redisChannel }}
```
## 📝 Пример workflow
```
[Webhook] → [AI обработка] → [Redis SET] → [Redis PUBLISH] → [End]
↓ ↓
[Сохранить историю в n8n] [Ответ в ключе + канале]
```
**Порядок:**
1. SET в ключ `ai:response:cache:{taskId}` (для fallback)
2. PUBLISH в канал `ai:response:{taskId}` (для SSE)
3. Сохранение истории в n8n
### Детали Redis ноды:
**Input:**
- `taskId` - из входящего webhook запроса
- `aiResponse` - результат обработки AI
- `redisChannel` - канал из входящего запроса (`ai:response:{taskId}`)
**Output:**
- Публикация в Redis канал
- Браузер получает через SSE мгновенно
## ✅ Проверка
### Тест публикации из командной строки:
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
PUBLISH "ai:response:test-task" '{"task_id":"test-task","response":"Тест","status":"completed"}'
```
### Проверка в n8n:
1. Запустите workflow с тестовым запросом
2. Проверьте логи Redis ноды - должна быть успешная публикация
3. В браузере откройте AI Drawer и отправьте сообщение
4. Ответ должен прийти мгновенно через SSE
## 🔍 Отладка
### Если ответ не приходит:
1. **Проверьте канал Redis:**
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
MONITOR
```
Должны видеть PUBLISH команды
2. **Проверьте формат сообщения:**
Сообщение должно быть валидным JSON:
```json
{
"task_id": "task-123",
"response": "Ответ от AI",
"status": "completed"
}
```
3. **Проверьте SSE endpoint:**
```bash
curl -N "https://crm.clientright.ru/aiassist/ai_sse.php?task_id=test-task"
```
## 📊 Преимущества новой архитектуры
✅ **Проще** - нет промежуточного callback
✅ **Быстрее** - прямая публикация в Redis
✅ **Надежнее** - меньше точек отказа
✅ **Меньше кода** - убрали БД и callback
## 🚨 Важно
- История диалога сохраняется в n8n автоматически (не нужно дублировать в БД)
- Если Redis недоступен, браузер автоматически переключится на fallback (polling)
- Канал Redis уникален для каждого запроса: `ai:response:{taskId}`
## 📁 Связанные файлы
- `/aiassist/n8n_proxy.php` - отправляет запрос в n8n с параметрами Redis
- `/aiassist/ai_sse.php` - SSE endpoint для получения ответов из Redis
- `/layouts/v7/resources/js/ai-drawer-simple.js` - JavaScript клиент с SSE

View File

@@ -0,0 +1,89 @@
# 🐛 FIX: Кнопка "Через Nextcloud" → Ошибка подключения к API
**Дата:** 2 ноября 2025
**Проблема:** Кнопка "📚 Через Nextcloud" показывала ошибку "Ошибка подключения к API"
## 🔍 Диагностика
### Симптомы:
1.`nextcloud_open.php` **работал в CLI** (возвращал правильный JSON)
2. ❌ Через веб (curl/браузер) возвращал **HTTP 500 (пустой ответ)**
3. ❌ JavaScript fetch() получал пустой ответ → показывал ошибку
### Причина:
**Redis PHP extension** был установлен только для PHP 7.2, а Apache использовал **PHP 7.3**!
```bash
# PHP CLI (работало):
php -v # PHP 7.2 (имеет redis extension)
# Apache (не работало):
phpinfo() # PHP 7.3 (НЕТ redis extension!)
```
## ✅ Решение
### 1. Обнаружили модуль .so:
```bash
find /opt/php73 -name "redis.so"
# /opt/php73/lib/php/extensions/no-debug-non-zts-20180731/redis.so
```
### 2. Создали конфиг:
```bash
echo "extension=redis.so" > /opt/php73/mods-available/redis.ini
ln -s /opt/php73/mods-available/redis.ini /opt/php73/conf.d/redis.ini
```
### 3. Перезапустили Apache:
```bash
systemctl restart apache2
```
### 4. Проверка:
```bash
curl https://crm.clientright.ru/crm_extensions/file_storage/api/nextcloud_open.php?recordId=395695
# Ответ:
{
"success": true,
"fileId": 115163,
"redirectUrl": "https://office.clientright.ru:8443/apps/files/files/115163?...",
"source": "redis"
}
```
## 🎯 Результат
✅ Кнопка "📚 Через Nextcloud" **РАБОТАЕТ**
✅ FileID получается из **Redis** (быстро!)
✅ Файлы открываются в **OnlyOffice через Nextcloud**
## 📂 Изменённые файлы
- `/opt/php73/mods-available/redis.ini` (создан)
- `/opt/php73/conf.d/redis.ini` (symlink создан)
- `nextcloud_open.php` (оптимизирован, убрана PROPFIND fallback логика)
## 🧪 Проверка других кнопок
| Кнопка | Статус | Примечание |
|--------|--------|------------|
| ⚡ **Быстро** | ✅ Работает | S3 → OnlyOffice Standalone |
| 📚 **Через Nextcloud** | ✅ Работает | Redis → Nextcloud → OnlyOffice |
| 📁 **Папка в Nextcloud** | ✅ Работает | Открывает папку проекта |
| 📄 **Скачать** | ✅ Работает | Прямая ссылка S3 |
## 🔧 Для проверки в будущем:
```bash
# Проверка модулей PHP 7.3:
curl -s "https://crm.clientright.ru/crm_extensions/file_storage/api/test_modules.php"
# Должно показать: {"mysqli":true,"redis":true,"json":true}
```
---
**Автор:** AI Assistant (Claude Sonnet 4.5)
**Время исправления:** ~2 часа (большая часть на диагностику)
**Сложность:** ⭐⭐⭐ (3/5) - нетривиальная проблема с разными версиями PHP

View File

@@ -0,0 +1,138 @@
# 🔧 ИСПРАВЛЕНИЕ: Кнопки "Папка в Nextcloud" не работали
## ❌ ПРОБЛЕМА:
В модулях CRM кнопка **"Папка в Nextcloud"** была неактивна и не реагировала на клики.
## 🔍 ПРИЧИНА:
В JavaScript файле `crm_extensions/nextcloud_editor/js/nextcloud-editor.js` **отсутствовали функции**, которые вызывались из шаблонов:
**Было в JS:**
-`openProjectFolder()` — ТОЛЬКО ДЛЯ Project
**Вызывалось из шаблонов, но НЕ СУЩЕСТВОВАЛО:**
-`openRecordFolder()` — для HelpDesk, Invoice, SalesOrder, PurchaseOrder, Leads, Quotes, Potentials
-`openAccountFolder()` — для Accounts
-`openContactFolder()` — для Contacts
## ✅ РЕШЕНИЕ:
### 1. Добавлена универсальная функция `openRecordFolder()`
```javascript
function openRecordFolder(moduleName, recordId, recordName) {
// Нормализация имени (убираем кавычки, заменяем пробелы)
if (recordName) {
recordName = recordName.replace(/"/g, '_').replace(/\s+/g, '_');
}
// Формируем путь к папке
const folderName = recordName ? `${recordName}_${recordId}` : `${moduleName.toLowerCase()}_${recordId}`;
const encodedFolderName = encodeURIComponent(folderName);
const nextcloudUrl = 'https://office.clientright.ru:8443';
const folderUrl = `${nextcloudUrl}/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/${moduleName}/${encodedFolderName}`;
// Открываем в новом окне
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
}
```
**Используется в модулях:**
- HelpDesk (тикеты)
- Invoice (счета)
- SalesOrder (заказы)
- PurchaseOrder (закупки)
- Leads (лиды)
- Quotes (предложения)
- Potentials (сделки)
### 2. Добавлена специализированная функция `openAccountFolder()`
```javascript
function openAccountFolder(accountId, accountName) {
// Нормализация имени контрагента
if (accountName) {
accountName = accountName.replace(/"/g, '_').replace(/\s+/g, '_');
}
const folderName = accountName ? `${accountName}_${accountId}` : `account_${accountId}`;
const encodedFolderName = encodeURIComponent(folderName);
const folderUrl = `https://office.clientright.ru:8443/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/Accounts/${encodedFolderName}`;
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
}
```
**Используется в модуле:**
- Accounts (контрагенты)
### 3. Добавлена специализированная функция `openContactFolder()`
```javascript
function openContactFolder(contactId, firstName, lastName) {
// Составление имени из firstName и lastName
let contactName = '';
if (firstName || lastName) {
contactName = `${firstName || ''}_${lastName || ''}`.replace(/^_+|_+$/g, '');
contactName = contactName.replace(/"/g, '_').replace(/\s+/g, '_');
}
const folderName = contactName ? `${contactName}_${contactId}` : `contact_${contactId}`;
const encodedFolderName = encodeURIComponent(folderName);
const folderUrl = `https://office.clientright.ru:8443/apps/files/?dir=/crm/crm2/CRM_Active_Files/Documents/Contacts/${encodedFolderName}`;
window.open(folderUrl, 'nextcloud_folder', 'width=1200,height=800,scrollbars=yes,resizable=yes');
}
```
**Используется в модуле:**
- Contacts (контакты)
### 4. Очищен кеш Smarty
```bash
rm -rf test/templates_c/v7/*.php
```
## 📋 ПРОВЕРКА:
### Функции в JS:
```bash
grep -n "^function open.*Folder" crm_extensions/nextcloud_editor/js/nextcloud-editor.js
```
**Результат:**
```
9:function openRecordFolder(moduleName, recordId, recordName) {
37:function openProjectFolder(projectId, projectName) {
65:function openAccountFolder(accountId, accountName) {
87:function openContactFolder(contactId, firstName, lastName) {
```
**ВСЕ 4 ФУНКЦИИ НА МЕСТЕ!**
### Где используются:
| Модуль | Функция | Параметры |
|--------|---------|-----------|
| HelpDesk | `openRecordFolder()` | 'HelpDesk', recordId, ticket_no |
| Invoice | `openRecordFolder()` | 'Invoice', recordId, invoice_no |
| SalesOrder | `openRecordFolder()` | 'SalesOrder', recordId, salesorder_no |
| PurchaseOrder | `openRecordFolder()` | 'PurchaseOrder', recordId, purchaseorder_no |
| Leads | `openRecordFolder()` | 'Leads', recordId, firstname_lastname |
| Quotes | `openRecordFolder()` | 'Quotes', recordId, quote_no |
| Potentials | `openRecordFolder()` | 'Potentials', recordId, potentialname |
| Accounts | `openAccountFolder()` | accountId, accountname |
| Contacts | `openContactFolder()` | contactId, firstname, lastname |
| Project | `openProjectFolder()` | projectId, projectname |
## 🎯 РЕЗУЛЬТАТ:
**КНОПКИ "Папка в Nextcloud" ТЕПЕРЬ РАБОТАЮТ ВО ВСЕХ МОДУЛЯХ!**
При клике на кнопку открывается **новое окно** с Nextcloud, где отображается папка соответствующей записи CRM.
## 📁 ИЗМЕНЕННЫЕ ФАЙЛЫ:
- `crm_extensions/nextcloud_editor/js/nextcloud-editor.js` — добавлены 3 недостающие функции
- `test/templates_c/v7/*.php` — очищен кеш (автоматически пересоздастся)
## 📅 ДАТА ИСПРАВЛЕНИЯ:
02.11.2025

View File

@@ -0,0 +1,223 @@
# ✅ NEXTCLOUD - ИТОГОВОЕ РЕШЕНИЕ МЕДЛЕННОЙ РАБОТЫ
**Дата:** 31 октября 2025
**Проблема:** Медленное открытие файлов для редактирования (25 секунд)
**Решение:** Найдено и применено
---
## 🔍 НАЙДЕННАЯ ПРОБЛЕМА
### **Корневая причина:**
При запросе **одного файла** через PROPFIND, Nextcloud с настройкой `filesystem_check_changes=1`:
1. Проверяет кеш для файла
2. **НЕ находит актуальные данные**
3. Решает обновить кеш
4. **СКАНИРУЕТ ВСЮ РОДИТЕЛЬСКУЮ ПАПКУ** `Documents/Project/`
5. Вызывает `getDirectoryContent()` для S3
6. **Делает LIST запрос к S3 для ~700 подпапок проектов**
7. Каждая подпапка - отдельный HTTP запрос
8. **700 запросов × 30ms latency = 21+ секунда**
### **Почему так происходило:**
```php
// AmazonS3.php line 648
function getDirectoryMetaData($path) {
// Сканирует ВСЮ папку, а не только запрошенный файл!
return $this->getDirectoryContent($path);
}
```
При `filesystem_check_changes=1` Nextcloud хотел убедиться что файл актуален, но вместо проверки одного файла - сканировал всю папку с сотнями проектов!
---
## ✅ РЕШЕНИЕ
### **Отключить filesystem_check для S3 External Storage:**
```bash
docker exec -u www-data nextcloud-fresh php occ files_external:option 1 filesystem_check_changes 0
```
### **Результаты:**
| Показатель | До | После | Улучшение |
|------------|-----|--------|-----------|
| **PROPFIND время** | 24-26 сек | 1.3-1.7 сек | **-94%** 🚀 |
| **Ускорение** | - | - | **x18** |
| **Стабильность** | Скачет | Стабильно | ✅ |
### **Побочные эффекты:**
⚠️ **Новые файлы НЕ видны автоматически**
**Решение:** Периодическое сканирование через cron:
```bash
# Каждые 30 минут - сканируем только новые файлы
*/30 * * * * docker exec -u www-data nextcloud-fresh php occ files:scan --path=/admin/files/crm/crm2/CRM_Active_Files/Documents/Project --shallow
```
---
## 📊 ТЕСТЫ ПРОИЗВОДИТЕЛЬНОСТИ
### Серия из 5 тестов:
```
Тест 1: 1.722s
Тест 2: 1.496s
Тест 3: 1.339s ← лучший
Тест 4: 1.447s
Тест 5: 1.452s
Среднее: 1.49 секунды
```
**Стабильная скорость:** ~1.5 секунды
---
## 🎯 ДОПОЛНИТЕЛЬНЫЕ ОПТИМИЗАЦИИ
### Что уже сделано:
1.**Redis Distributed Cache** - кеш между запросами
2.**PHP Memory 1024MB** - больше памяти
3.**OPcache оптимизирован** - меньше компиляций
4.**MySQL max_connections=300** - больше соединений
5.**Шифрование отключено** - меньше CPU
6.**filesystem_check_changes=0** - без сканирования S3
### CPU/Memory после всех оптимизаций:
```
Nextcloud: 0.05-5% CPU (было 50%)
Memory: 643-746 MB (было 1005 MB)
Deadlocks: минимум (было много)
```
---
## 💡 ДАЛЬНЕЙШИЕ УЛУЧШЕНИЯ (опционально)
### Если нужно ещё быстрее (< 1 секунды):
#### **#1: Nginx Reverse Proxy с кешем метаданных**
```nginx
# Кешировать PROPFIND ответы
location ~ ^/remote.php/dav/files/ {
proxy_cache_path /var/cache/nginx/propfind levels=1:2 keys_zone=PROPFIND:10m;
proxy_cache PROPFIND;
proxy_cache_valid 200 5m;
proxy_cache_methods GET HEAD PROPFIND;
proxy_cache_key "$request_method$request_uri";
proxy_pass http://nextcloud-fresh;
}
```
**Эффект:** Метаданные из Nginx кеша → 0.01-0.05 секунды
---
#### **#2: Прямой WOPI endpoint в CRM (обход Nextcloud для метаданных)**
```php
// Collabora запрашивает метаданные напрямую из CRM
// CRM отдаёт из своей БД (мгновенно!)
// Collabora читает файл через presigned S3 URL
WOPISrc = https://crm.clientright.ru/wopi/files/{recordId}
```
**Эффект:** 0.1-0.2 секунды полное открытие
---
#### **#3: Кеш в CRM Redis**
```php
// Кешировать fileId в CRM Redis
$redis->set("nc_fileid:$recordId", $fileId, 3600);
// При открытии - сразу из кеша
$fileId = $redis->get("nc_fileid:$recordId");
if ($fileId) {
// Сразу открываем без PROPFIND
return "https://office.clientright.ru:8443/apps/richdocuments/index?fileId=$fileId";
}
```
**Эффект:** Без PROPFIND вообще → 0.05-0.1 секунды
---
## 🎯 ИТОГОВЫЕ РЕКОМЕНДАЦИИ
### **ДЛЯ ПРОДАКШЕНА (СЕЙЧАС):**
```bash
# 1. filesystem_check_changes=0 для S3 (УЖЕ ПРИМЕНЕНО)
docker exec -u www-data nextcloud-fresh php occ files_external:option 1 filesystem_check_changes 0
# 2. Cron сканирование каждые 30 минут
crontab -e
*/30 * * * * docker exec -u www-data nextcloud-fresh php occ files:scan --path=/admin/files/crm --shallow > /dev/null 2>&1
# 3. Мониторинг
docker stats nextcloud-fresh --no-stream
```
### **ЕСЛИ НУЖНО ЕЩЁ БЫСТРЕЕ:**
- Nginx cache для PROPFIND (→ 0.05s)
- Кеш fileId в CRM Redis (→ без PROPFIND)
- Прямой WOPI от CRM (→ полный обход Nextcloud для метаданных)
---
## ✅ ТЕКУЩИЙ СТАТУС
### Производительность:
-**Открытие для редактирования:** ~1.5 секунды (было 25с)
-**Просмотр:** ~0.2 секунды (прямые ссылки S3)
-**CPU Nextcloud:** 0.05-5% (было 50%)
-**Стабильность:** отлично
### Настройки:
```php
'memcache.distributed' => '\OC\Memcache\Redis', // ✅
'filesystem_check_changes' => 1, // глобально
```
```json
// External Storage ID 1 (/crm)
{
"filesystem_check_changes": 0, // ✅ ОТКЛЮЧЕНО
"encrypt": false, // ✅ ОТКЛЮЧЕНО
"previews": true
}
```
---
## 🎉 РЕЗУЛЬТАТ
**Редактирование документов теперь открывается в 18 раз быстрее!**
- Было: 25 секунд ❌
- Стало: 1.5 секунды ✅
**Это приемлемая скорость для редактирования!** 🚀
---
**Следующий шаг:** Настроить cron для периодического сканирования новых файлов
**Хочешь настрою cron?** 😊

View File

@@ -0,0 +1,560 @@
# 🚀 NEXTCLOUD - ЛАЙФХАКИ УСКОРЕНИЯ
## 🎯 ЦЕЛЬ: Работать как Google Drive
---
## 💡 ПРЕДЛОЖЕНИЯ ДЛЯ ОБСУЖДЕНИЯ
### 1⃣ **HTTP/2 Push и Preload** ⚡⚡⚡
**Что:** Предзагрузка ресурсов до того как браузер их запросит
**Реализация в Nginx:**
```nginx
location ~ \.php$ {
# Предзагрузка JS/CSS
add_header Link "</core/css/styles.css>; rel=preload; as=style";
add_header Link "</core/js/main.js>; rel=preload; as=script";
# HTTP/2 Push
http2_push /core/css/styles.css;
http2_push /core/js/main.js;
}
```
**Ожидаемый эффект:** +30-50% скорость загрузки интерфейса
---
### 2⃣ **Separating Data Directory на SSD** 💾⚡⚡⚡
**Что:** Переместить БД Nextcloud на SSD диск (если сейчас на HDD)
**Проверка текущего диска:**
```bash
docker exec nextcloud-db-fresh df -h /var/lib/mysql
lsblk
```
**Если на HDD:**
- Создать volume на SSD
- Перенести `/var/lib/mysql` туда
- **Ускорение:** 5-10x для случайных запросов
---
### 3⃣ **Query Cache для MySQL** 🗄️⚡⚡
**Что:** Кешировать результаты повторяющихся SQL запросов
**НО!** В MariaDB 10.6+ Query Cache УДАЛЁН (deprecated)
**Альтернатива - ProxySQL:**
```bash
docker run -d --name proxysql \
--link nextcloud-db-fresh:mysql \
-p 6033:6033 \
proxysql/proxysql
```
**Настройка:**
- ProxySQL кеширует запросы к БД
- Nextcloud подключается к ProxySQL вместо напрямую к MySQL
- **Ускорение:** 2-3x для повторяющихся запросов
---
### 4⃣ **Preview Pre-generation** 🖼️⚡⚡
**Что:** Генерировать превью заранее в фоне
```bash
# Установка приложения
docker exec -u www-data nextcloud-fresh php occ app:install previewgenerator
# Настройка размеров
docker exec -u www-data nextcloud-fresh php occ config:app:set previewgenerator squareSizes --value="32 256"
docker exec -u www-data nextcloud-fresh php occ config:app:set previewgenerator widthSizes --value="256 384"
docker exec -u www-data nextcloud-fresh php occ config:app:set previewgenerator heightSizes --value="256"
# Первичная генерация (один раз)
docker exec -u www-data nextcloud-fresh php occ preview:generate-all
# Cron каждые 10 минут
*/10 * * * * docker exec -u www-data nextcloud-fresh php occ preview:pre-generate
```
**Эффект:** Превью файлов грузятся мгновенно
---
### 5⃣ **Chunked Upload для больших файлов** 📤⚡⚡
**Что:** Загрузка кусками вместо одного большого файла
```bash
docker exec -u www-data nextcloud-fresh php occ config:app:set files max_chunk_size --value=10485760 # 10MB chunks
```
**Для S3 External Storage:**
```bash
# Multipart upload для файлов > 5GB
docker exec -u www-data nextcloud-fresh php occ config:system:set objectstore.s3.multipart_threshold --value=5368709120 --type=integer
```
**Эффект:** Большие файлы загружаются быстрее и надёжнее
---
### 6⃣ **MySQL InnoDB Optimization** 🗄️⚡⚡⚡
**Что:** Правильные индексы для oc_filecache
**Текущие индексы:**
```sql
-- Проверка
docker exec nextcloud-db-fresh mariadb -u root -proot_password nextcloud -e "SHOW INDEX FROM oc_filecache;" 2>&1 | grep -v insecure
```
**Добавление недостающих:**
```sql
-- Для быстрого поиска по storage+path
CREATE INDEX idx_storage_path_hash ON oc_filecache(storage, path_hash);
-- Для быстрого поиска дочерних файлов
CREATE INDEX idx_parent_name ON oc_filecache(parent, name(191));
-- Для быстрого поиска по mimetype
CREATE INDEX idx_storage_mimetype ON oc_filecache(storage, mimetype);
-- Для сортировки по времени
CREATE INDEX idx_storage_mtime ON oc_filecache(storage, mtime);
```
**Автоматически:**
```bash
docker exec -u www-data nextcloud-fresh php occ db:add-missing-indices
docker exec -u www-data nextcloud-fresh php occ db:convert-filecache-bigint
```
**Эффект:** +50-100% скорость запросов к файлам
---
### 7⃣ **Nginx FastCGI Cache** 🌐⚡⚡⚡
**Что:** Кеш статических ресурсов и некоторых динамических
**Конфигурация Nginx:**
```nginx
# В http блоке
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=NEXTCLOUD:100m inactive=60m;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
server {
location ~ ^\/(?:index|remote|public|cron|core\/ajax\/update|status|ocs\/v[12]|updater\/.+|oc[ms]-provider\/.+)\.php(?:$|\/) {
# FastCGI cache для избранных endpoints
fastcgi_cache NEXTCLOUD;
fastcgi_cache_valid 200 10m;
fastcgi_cache_bypass $http_pragma $http_authorization;
add_header X-Cache-Status $upstream_cache_status;
}
}
```
**Эффект:** Статика грузится мгновенно
---
### 8⃣ **Lazy Loading для больших папок** 📂⚡⚡
**Что:** Не грузить все 24K файлов сразу, а порциями
**В Nextcloud уже есть!** Проверяем настройки:
```bash
# Pagination включена по умолчанию
docker exec -u www-data nextcloud-fresh php occ config:app:get files pagination_limit
# Если нет - ставим
docker exec -u www-data nextcloud-fresh php occ config:app:set files pagination_limit --value=100
```
**Эффект:** Папки открываются быстрее
---
### 9⃣ **S3 Transfer Acceleration** 🌍⚡⚡⚡
**Что:** Использовать CDN endpoints TWC Storage
**Проверяем у провайдера TWC:**
- Есть ли CDN endpoint?
- Есть ли региональные endpoints ближе к серверу?
**Если есть:**
```php
// В config.php Nextcloud для S3
'objectstore' => [
'arguments' => [
'endpoint' => 'https://s3-accelerate.twcstorage.ru', // вместо обычного
]
]
```
**Эффект:** +50-200% скорость доступа к S3
---
### 🔟 **WebDAV Keepalive Connections** 🔗⚡⚡
**Что:** Переиспользовать HTTP соединения вместо создания новых
**В Nginx:**
```nginx
upstream nextcloud {
server nextcloud-fresh:80;
keepalive 32;
}
server {
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://nextcloud;
}
}
```
**Эффект:** Меньше латентности при множественных запросах
---
### 1⃣1⃣ **Collabora Caching** 📝⚡⚡⚡
**Что:** Кеш документов и шрифтов в Collabora
**Проверка docker-compose Collabora:**
```yaml
collabora:
environment:
- DONT_GEN_SSL_CERT=true
- extra_params=--o:ssl.enable=false --o:logging.level=warning --o:net.post_allow=crm\.clientright\.ru
# Добавляем кеширование
- extra_params=--o:admin_console.enable_pam=false --o:net.connection_timeout_secs=30 --o:per_document.idle_timeout_secs=3600
# Добавляем volume для кеша
volumes:
- /var/cache/collabora:/tmp/cache
```
**Создаём кеш папку:**
```bash
mkdir -p /var/cache/collabora
chmod 777 /var/cache/collabora
```
**Эффект:** Документы открываются в 2-3 раза быстрее
---
### 1⃣2⃣ **Browser Service Worker Cache** 🌐⚡⚡
**Что:** Кеш в браузере через Service Workers
**Nextcloud уже использует!** Проверить можно:
- F12 → Application → Service Workers
- Должен быть зарегистрирован
**Если нет - включаем PWA:**
```bash
docker exec -u www-data nextcloud-fresh php occ config:app:set core enable_pwa --value=yes
```
**Эффект:** Интерфейс грузится мгновенно после первого раза
---
### 1⃣3⃣ **Direct S3 Access (bypass Nextcloud)** 🔥⚡⚡⚡
**Что:** Открывать файлы напрямую из S3, минуя Nextcloud WebDAV
**Реализация в CRM:**
```php
// В nextcloud-editor.js
function openFileDirectFromS3(recordId) {
// Получаем s3_key из CRM
fetch(`/crm_extensions/nextcloud_api.php?record=${recordId}`)
.then(r => r.json())
.then(data => {
// Прямая ссылка на S3
const s3Url = `https://s3.twcstorage.ru/${bucket}/${data.s3_key}`;
// Для просмотра - прямо открываем
if (isViewMode) {
window.open(s3Url, '_blank');
}
// Для редактирования - через Nextcloud
if (isEditMode) {
openInCollabora(data.nc_path);
}
});
}
```
**Эффект:**
- Просмотр: мгновенно (прямо из S3)
- Редактирование: через Nextcloud (нужен Collabora)
---
### 1⃣4⃣ **MySQL Connection Pooling** 🔗⚡⚡
**Что:** Пул соединений к БД вместо открытия нового каждый раз
**ProxySQL (рекомендуется):**
```yaml
services:
proxysql:
image: proxysql/proxysql:latest
ports:
- "6033:6033"
volumes:
- ./proxysql.cnf:/etc/proxysql.cnf
```
**Или в самом MySQL:**
```sql
SET GLOBAL max_connections = 500; -- увеличиваем пул
SET GLOBAL thread_cache_size = 100; -- кеш потоков
SET GLOBAL table_open_cache = 4096; -- кеш таблиц
```
**Эффект:** Меньше overhead на создание соединений
---
### 1⃣5⃣ **Отключение Activity Log для CRM папки** 📊⚡⚡
**Что:** Не логировать каждое действие в папке с 24K файлов
```bash
# Отключаем activity для storage ID 4 (crm)
docker exec nextcloud-db-fresh mariadb -u root -proot_password nextcloud -e "
DELETE FROM oc_activity WHERE object_type = 'files' AND object_id IN (
SELECT fileid FROM oc_filecache WHERE storage = 4
);
"
# Настройка в Nextcloud
docker exec -u www-data nextcloud-fresh php occ config:app:set activity enable_activity_tracking --value=no
```
**Эффект:** Меньше записей в БД при каждом открытии файла
---
### 1⃣6⃣ **CDN для статики Nextcloud** 🌍⚡⚡⚡
**Что:** Статические файлы (JS/CSS/иконки) через CDN
**Cloudflare перед Nextcloud:**
```nginx
# В Nginx - доверяем Cloudflare IP
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
# ... остальные Cloudflare IP
real_ip_header CF-Connecting-IP;
```
**В Nextcloud config.php:**
```php
'overwriteprotocol' => 'https',
'trusted_proxies' => ['173.245.48.0/20', '103.21.244.0/22'],
```
**Эффект:**
- Статика грузится из ближайшего PoP
- Меньше нагрузки на сервер
- Быстрее для пользователей
---
### 1⃣7⃣ **Асинхронные операции через Jobs** ⏱️⚡⚡
**Что:** Тяжёлые операции (сканирование, превью) в фоне
```bash
# Cron вместо Ajax/Webcron
docker exec -u www-data nextcloud-fresh php occ background:cron
# В host cron
*/5 * * * * docker exec -u www-data nextcloud-fresh php occ cron:job:run
```
**Важно для нас:**
- Сканирование S3 - в фоне
- Генерация превью - в фоне
- Очистка кеша - в фоне
**Эффект:** UI не тормозит при открытии файлов
---
### 1⃣8⃣ **Partitioning таблицы oc_filecache** 🗂️⚡⚡⚡
**Что:** Разбить огромную таблицу по storage ID
```sql
-- Проверяем размер таблицы
SELECT
table_name AS `Table`,
ROUND(((data_length + index_length) / 1024 / 1024), 2) AS `Size (MB)`
FROM information_schema.TABLES
WHERE table_schema = "nextcloud" AND table_name = "oc_filecache";
-- Если > 100MB - делаем partitioning
ALTER TABLE oc_filecache
PARTITION BY RANGE (storage) (
PARTITION p_storage_1_3 VALUES LESS THAN (4),
PARTITION p_storage_4 VALUES LESS THAN (5), -- CRM папка отдельно!
PARTITION p_storage_other VALUES LESS THAN MAXVALUE
);
```
**Эффект:**
- Запросы к CRM папке НЕ сканируют всю таблицу
- +50-80% скорость для больших таблиц
---
### 1⃣9⃣ **Отдельный WOPI сервер для Collabora** 📝⚡⚡⚡
**Что:** Collabora на отдельном сервере/контейнере
**Сейчас у вас:**
- Nextcloud + Collabora на одном сервере
- Конкурируют за ресурсы
**Оптимально:**
```yaml
# На другом сервере или с лимитами
collabora:
cpus: '2.0'
mem_limit: 2g
mem_reservation: 1g
```
**Эффект:** Nextcloud и Collabora не мешают друг другу
---
### 2⃣0⃣ **Varnish Cache перед Nextcloud** 🚀⚡⚡⚡
**Что:** HTTP кеш для статики и некоторых API
```
Browser → Varnish (6081) → Nginx → Nextcloud
```
**VCL конфигурация:**
```vcl
# Кешируем статику
if (req.url ~ "^/core/.*\.(js|css|png|jpg|svg)$") {
return (hash); # кешируем
}
# НЕ кешируем WebDAV
if (req.url ~ "^/remote.php/dav/") {
return (pass); # не кешируем
}
```
**Эффект:** Статика грузится в 10-100 раз быстрее
---
## 🎯 РЕКОМЕНДОВАННЫЙ ПЛАН
### **БЫСТРЫЕ ПОБЕДЫ (сделать сегодня):**
1.**Добавить индексы в MySQL** (5 минут)
```bash
docker exec -u www-data nextcloud-fresh php occ db:add-missing-indices
```
2. ✅ **Preview Generator** (30 минут первый раз)
```bash
docker exec -u www-data nextcloud-fresh php occ app:install previewgenerator
docker exec -u www-data nextcloud-fresh php occ preview:generate-all &
```
3. ✅ **Отключить Activity для CRM папки** (2 минуты)
```sql
DELETE FROM oc_activity WHERE object_id IN (
SELECT fileid FROM oc_filecache WHERE storage = 4
);
```
4. ✅ **Chunked upload** (1 минута)
```bash
docker exec -u www-data nextcloud-fresh php occ config:app:set files max_chunk_size --value=10485760
```
**Ожидаемое ускорение:** +100-200%
---
### **СРЕДНЯЯ СЛОЖНОСТЬ (на неделе):**
5.**InnoDB Buffer Pool 1GB** - требует изменение docker-compose
6.**Partitioning oc_filecache** - разделение таблицы
7.**HTTP/2 в Nginx** - если ещё не включено
8.**Cron для фоновых задач** - вместо webcron
**Ожидаемое ускорение:** +50-100%
---
### **ПРОДВИНУТОЕ (если нужно ещё быстрее):**
9.**ProxySQL** - пул соединений к MySQL
10.**Varnish Cache** - HTTP кеш перед Nextcloud
11.**Отдельный сервер для Collabora** - разделение нагрузки
12.**CDN (Cloudflare)** - статика через CDN
**Ожидаемое ускорение:** +200-500%
---
### **ЭКСПЕРИМЕНТАЛЬНОЕ:**
13.**Direct S3 access для просмотра** - обход Nextcloud
14.**Nginx caching для WebDAV** - кеш метаданных
15.**S3 Transfer Acceleration** - если TWC поддерживает
---
## 💰 СООТНОШЕНИЕ УСИЛИЙ/РЕЗУЛЬТАТА
| Решение | Сложность | Эффект | Приоритет |
|---------|-----------|--------|-----------|
| **Индексы MySQL** | ⭐ | ⭐⭐⭐ | 🔥🔥🔥 |
| **Preview Generator** | ⭐ | ⭐⭐⭐ | 🔥🔥🔥 |
| **Отключить Activity** | ⭐ | ⭐⭐ | 🔥🔥 |
| **InnoDB Buffer 1GB** | ⭐⭐ | ⭐⭐⭐ | 🔥🔥🔥 |
| **Partitioning** | ⭐⭐⭐ | ⭐⭐⭐ | 🔥🔥 |
| **ProxySQL** | ⭐⭐⭐ | ⭐⭐ | 🔥 |
| **Varnish** | ⭐⭐⭐⭐ | ⭐⭐⭐ | 🔥 |
| **Direct S3** | ⭐⭐ | ⭐⭐⭐⭐ | 🔥🔥 |
---
## 🤔 МОИ РЕКОМЕНДАЦИИ
### **Начни с TOP-4:**
1. **Индексы MySQL** - 5 минут, +50% скорость
2. **Preview Generator** - 30 минут, мгновенные превью
3. **Отключить Activity для CRM** - 2 минуты, меньше записей в БД
4. **InnoDB Buffer 1GB** - 10 минут, +30% скорость БД
**Это даст примерно 2-3x ускорение БЕЗ риска!**
---
### **Потом, если нужно ещё:**
5. **Partitioning oc_filecache** - разделить таблицу (сложнее, но эффективно)
6. **Direct S3 access** - для просмотра файлов (очень быстро!)
---
## 🎯 ВОПРОСЫ ДЛЯ ОБСУЖДЕНИЯ:
1. **Хочешь начать с быстрых побед** (индексы, preview, activity)?
2. **Готов менять docker-compose** для InnoDB Buffer?
3. **Интересует Direct S3 access** для просмотра файлов (обход Nextcloud)?
4. **Рассматриваешь Varnish/ProxySQL** для серьёзного ускорения?
**Что выберем?** 😊

View File

@@ -0,0 +1,249 @@
# 🚀 ОПТИМИЗАЦИЯ NEXTCLOUD - 31 ОКТЯБРЯ 2025
## 🔍 ПРОБЛЕМА
### Симптомы:
- ⏱️ Медленное открытие документов из CRM
- 🔥 Nextcloud CPU: **50.43%** (постоянно)
- ❌ Database Deadlocks при работе с файлами
- 📊 24,000+ файлов в папке crm2
### Найденные проблемы:
#### 1. **Database Deadlocks** ❌
```
SQLSTATE[40001]: Serialization failure: 1213
Deadlock found when trying to get lock; try restarting transaction
```
- Nextcloud постоянно сканирует папку crm2 (24K файлов)
- Обновления таблицы `oc_filecache` создают блокировки
- 404 блокировки (Innodb_row_lock_waits)
#### 2. **Отсутствие Distributed Cache** ❌
- Был только `memcache.local` (APCu)
- Не было `memcache.distributed` (Redis)
- Кеш не переживал перезапуски
#### 3. **Автоматическое сканирование** ❌
- `filesystem_check_changes = 1` (по умолчанию)
- При каждом запросе Nextcloud сканировал все 24K файлов
- Огромная нагрузка на БД
#### 4. **Маленький InnoDB Buffer Pool** ❌
- Всего **128 MB** (должно быть минимум 512MB)
- Не хватает памяти для кеширования запросов
---
## ✅ РЕШЕНИЕ
### 1. Distributed Cache (Redis)
```bash
docker exec -u www-data nextcloud-fresh php occ config:system:set memcache.distributed --value='\OC\Memcache\Redis'
```
**Результат:**
- ✅ Кеш между запросами
- ✅ Меньше обращений к БД
- ✅ Быстрее открытие файлов
### 2. Отключение автосканирования
```bash
docker exec -u www-data nextcloud-fresh php occ config:system:set filesystem_check_changes --value=0 --type=integer
```
**Результат:**
- ✅ Nextcloud НЕ сканирует 24K файлов при каждом запросе
- ✅ Меньше Deadlocks
- ✅ Меньше нагрузки на БД
**Важно:** Файлы будут обновляться через:
- Redis события (`crm:file:events`)
- Ручное сканирование при необходимости
- WebDAV уведомления
### 3. Оптимизация PHP
```bash
# Увеличен memory_limit
echo 'memory_limit=1024M' >> /usr/local/etc/php/conf.d/nextcloud.ini
# Оптимизация OPcache
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.memory_consumption=256
opcache.revalidate_freq=60
```
**Результат:**
- ✅ Больше памяти для PHP скриптов
- ✅ Больше файлов в OPcache
- ✅ Меньше компиляций
### 4. Оптимизация MySQL
```sql
SET GLOBAL max_connections = 300; -- было 151
SET GLOBAL innodb_lock_wait_timeout = 120; -- было 50
```
**Примечание:** `innodb_buffer_pool_size` требует изменения в docker-compose и перезапуск.
---
## 📊 РЕЗУЛЬТАТ
### До оптимизации:
```
CPU: 50.43% ❌
Memory: 1005 MiB
Deadlocks: много
Скорость: медленно
```
### После оптимизации:
```
CPU: 5.29% ✅ (в 10 раз меньше!)
Memory: 645 MiB
Deadlocks: должны уменьшиться
Скорость: быстрее
```
---
## 🎯 ДОПОЛНИТЕЛЬНЫЕ РЕКОМЕНДАЦИИ
### 1. Увеличить InnoDB Buffer Pool (требует перезапуск)
Найти docker-compose файл Nextcloud и добавить:
```yaml
services:
db:
environment:
MYSQL_INNODB_BUFFER_POOL_SIZE: 1073741824 # 1GB
command:
- --innodb-buffer-pool-size=1073741824
- --innodb-log-file-size=268435456 # 256MB
- --innodb-flush-log-at-trx_commit=2
- --innodb-flush-method=O_DIRECT
```
**Ожидаемое ускорение:** +30-50%
### 2. Добавить индексы в oc_filecache
```sql
-- Проверить существующие индексы
SHOW INDEX FROM oc_filecache;
-- Добавить если нужно
CREATE INDEX idx_storage_path ON oc_filecache(storage, path(255));
CREATE INDEX idx_parent_name ON oc_filecache(parent, name(255));
```
**Ожидаемое ускорение:** +20-40%
### 3. Настроить Chunked Upload для больших файлов
```bash
docker exec -u www-data nextcloud-fresh php occ config:app:set files max_chunk_size --value=10485760 # 10MB
```
### 4. Включить Preview Generation в фоне
```bash
docker exec -u www-data nextcloud-fresh php occ config:app:set previewgenerator squareSizes --value="256 512"
docker exec -u www-data nextcloud-fresh php occ config:app:set previewgenerator widthSizes --value="256 512 1024"
docker exec -u www-data nextcloud-fresh php occ config:app:set previewgenerator heightSizes --value="256 512 1024"
```
---
## 📈 МОНИТОРИНГ
### Проверка производительности:
```bash
# CPU/Memory Nextcloud
docker stats nextcloud-fresh --no-stream
# Deadlocks MySQL
docker exec nextcloud-db-fresh mariadb -u root -proot_password -e "SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';" 2>&1 | grep -v insecure
# Redis статистика
redis-cli -h 127.0.0.1 -p 6380 -a 'Nextcloud_Redis_Pass_2025!' INFO stats 2>&1 | grep -v Warning | head -10
```
### Логи Nextcloud:
```bash
docker logs nextcloud-fresh --tail 50 | grep -i "error\|warning\|deadlock"
```
---
## ✅ ТЕКУЩИЕ НАСТРОЙКИ
### Nextcloud Config:
```php
'memcache.local' => '\OC\Memcache\APCu', // Локальный кеш
'memcache.distributed' => '\OC\Memcache\Redis', // Распределённый кеш ✅ НОВОЕ
'memcache.locking' => '\OC\Memcache\Redis', // Блокировки через Redis
'filesystem_check_changes' => 0, // Автоскан ОТКЛЮЧЕН ✅ НОВОЕ
```
### PHP:
```
memory_limit = 1024M (было 512M)
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.interned_strings_buffer = 16
```
### MySQL:
```
max_connections = 300 (было 151)
innodb_lock_wait_timeout = 120 (было 50)
innodb_buffer_pool_size = 128MB (⚠️ НУЖНО 1GB)
```
---
## 🚀 СЛЕДУЮЩИЕ ШАГИ
### Критично (сделать сегодня):
1. ✅ Distributed cache - **ГОТОВО**
2. ✅ Отключить автоскан - **ГОТОВО**
3. ✅ PHP оптимизация - **ГОТОВО**
4. ⏳ Проверить результат - **тестируем**
### Важно (сделать на неделе):
1. ⬜ Увеличить InnoDB buffer pool до 1GB
2. ⬜ Добавить индексы в oc_filecache
3. ⬜ Настроить chunked upload
4. ⬜ Preview generation в фоне
### Можно (если будет время):
1. ⬜ APCu увеличить (apc.shm_size=128M)
2. ⬜ Отключить ненужные приложения Nextcloud
3. ⬜ Настроить логи на rotation
4. ⬜ Мониторинг производительности
---
## 🎯 ОЖИДАЕМЫЙ РЕЗУЛЬТАТ
**После всех оптимизаций:**
- ⚡ Открытие документов: **в 3-5 раз быстрее**
- 📉 CPU Nextcloud: **< 10%** (было 50%)
- Deadlocks: **минимум** (было много)
- 💾 Меньше нагрузки на БД
---
**Статус:** Критичные оптимизации применены
**CPU до:** 50.43%
**CPU после:** 5.29%
**Ускорение:** **~10x** 🚀
**Дата:** 31 октября 2025, 10:04
**Автор:** Фёдор + AI Assistant

View File

@@ -0,0 +1,305 @@
# OnlyOffice + Nextcloud интеграция
**Дата:** 1 ноября 2025
**Цель:** Использовать OnlyOffice Document Server для редактирования файлов в Nextcloud
---
## ✅ Что сделано
### 1. Установлено приложение ONLYOFFICE
```bash
docker exec -u www-data nextcloud-fresh php occ app:install onlyoffice
# onlyoffice 9.11.0 installed ✅
```
### 2. Настроен Document Server
```bash
# Адрес OnlyOffice Document Server
DocumentServerUrl: https://office.clientright.ru:9443
# SSL проверка отключена
verify_peer_off: true
# JWT отключен (как в OnlyOffice)
jwt_secret: (пусто)
jwt_header: (пусто)
```
### 3. Отключен Collabora
```bash
# Чтобы не конфликтовал с OnlyOffice
docker exec -u www-data nextcloud-fresh php occ app:disable richdocuments
```
---
## 🎯 Теперь у тебя ДВА способа редактирования:
### Вариант 1: **Через CRM** (OnlyOffice напрямую)
```
CRM → open_file_v2.php → OnlyOffice (9443) → S3
```
**Преимущества:**
-**Молниеносно** (прямой доступ к S3)
- ✅ Автосохранение каждые 5 сек
- ✅ Файлы в CRM сразу обновляются
**Когда использовать:**
- Работаешь в CRM
- Нужна скорость
- Редактируешь файлы клиентов
---
### Вариант 2: **Через Nextcloud** (OnlyOffice через Nextcloud)
```
Nextcloud → OnlyOffice (9443) → WebDAV → S3
```
**Преимущества:**
-**Версионирование** файлов
- ✅ История изменений
- ✅ Права доступа через Nextcloud
- ✅ Интеграция с Nextcloud UI
**Когда использовать:**
- Работаешь в Nextcloud
- Нужна история версий
- Совместное редактирование
- Управление файлами
---
## 🚀 Как использовать в Nextcloud
### Шаг 1: Открой Nextcloud
```
https://office.clientright.ru:8443
Логин: admin
Пароль: office
```
### Шаг 2: Перейди к файлам
```
Файлы → crm/crm2/CRM_Active_Files/Documents
```
### Шаг 3: Открой файл для редактирования
```
Клик на файл .docx/.xlsx/.pptx
→ Автоматически откроется OnlyOffice!
```
---
## 📊 Сравнение OnlyOffice vs Collabora
| Характеристика | OnlyOffice | Collabora |
|----------------|------------|-----------|
| **Скорость** | ⚡⚡⚡ Молниеносно | 🐌 Медленнее |
| **Форматы** | .docx, .xlsx, .pptx | .odt, .ods, .odp + docx |
| **Совместимость** | MS Office | LibreOffice |
| **Интерфейс** | Современный | Классический |
| **Настройка** | Проще | Сложнее |
| **Автосохранение** | ✅ Есть | ✅ Есть |
| **Версионирование** | ✅ Через Nextcloud | ✅ Через Nextcloud |
---
## 🔧 Настройки OnlyOffice в Nextcloud
### Где найти:
```
Nextcloud → Настройки → Администрирование → ONLYOFFICE
```
### Текущие настройки:
```
Document Server Address: https://office.clientright.ru:9443
Secret key (JWT): (отключен)
Verify peer off: ✅ (SSL проверка отключена)
```
### Что можно настроить:
- Форматы файлов для открытия
- Права доступа (просмотр/редактирование)
- Автосохранение (интервал)
- Водяные знаки
- Шаблоны документов
---
## 🎨 Создание новых документов в Nextcloud
### Теперь можно создавать файлы прямо в Nextcloud!
**Как:**
1. Открой Nextcloud Files
2. Нажми **"+"** (New)
3. Выбери:
- 📄 **Document** (Word) → создаст .docx
- 📊 **Spreadsheet** (Excel) → создаст .xlsx
- 📽️ **Presentation** (PowerPoint) → создаст .pptx
4. Файл откроется в OnlyOffice для редактирования!
---
## ⚙️ Команды для управления
### Просмотр настроек:
```bash
# Адрес Document Server
docker exec -u www-data nextcloud-fresh php occ config:app:get onlyoffice DocumentServerUrl
# Проверка SSL
docker exec -u www-data nextcloud-fresh php occ config:app:get onlyoffice verify_peer_off
# JWT секрет
docker exec -u www-data nextcloud-fresh php occ config:app:get onlyoffice jwt_secret
```
### Изменение настроек:
```bash
# Изменить адрес Document Server
docker exec -u www-data nextcloud-fresh php occ config:app:set onlyoffice DocumentServerUrl --value="https://NEW_ADDRESS"
# Включить/отключить SSL проверку
docker exec -u www-data nextcloud-fresh php occ config:app:set onlyoffice verify_peer_off --value="true"
```
### Переключение между OnlyOffice и Collabora:
```bash
# Включить OnlyOffice
docker exec -u www-data nextcloud-fresh php occ app:enable onlyoffice
docker exec -u www-data nextcloud-fresh php occ app:disable richdocuments
# Включить Collabora
docker exec -u www-data nextcloud-fresh php occ app:disable onlyoffice
docker exec -u www-data nextcloud-fresh php occ app:enable richdocuments
```
---
## 🐛 Отладка
### Если файл не открывается:
**1. Проверь доступность OnlyOffice:**
```bash
curl -s https://office.clientright.ru:9443/healthcheck
# Должен вернуть: true
```
**2. Проверь настройки в Nextcloud:**
```bash
docker exec -u www-data nextcloud-fresh php occ config:app:get onlyoffice DocumentServerUrl
# Должно быть: https://office.clientright.ru:9443
```
**3. Проверь логи OnlyOffice:**
```bash
docker logs --tail 50 onlyoffice-standalone
```
**4. Проверь логи Nextcloud:**
```bash
docker exec -u www-data nextcloud-fresh php occ log:tail 50
```
**5. Проверь что приложение включено:**
```bash
docker exec -u www-data nextcloud-fresh php occ app:list | grep onlyoffice
# Должно быть в Enabled
```
---
## 🔒 Безопасность
### ⚠️ Сейчас JWT отключен!
**Что это значит:**
- Любой может использовать твой OnlyOffice сервер
- Не рекомендуется для продакшна
**Для продакшна:**
**1. Включи JWT в OnlyOffice:**
```bash
# Генерируй случайный секрет
SECRET=$(openssl rand -base64 32)
# Добавь в OnlyOffice config
docker exec onlyoffice-standalone bash -c "cat > /etc/onlyoffice/documentserver/local-jwt.json << EOF
{
\"services\": {
\"CoAuthoring\": {
\"token\": {
\"enable\": {
\"request\": {
\"inbox\": true,
\"outbox\": true
}
}
},
\"secret\": {
\"inbox\": {
\"string\": \"$SECRET\"
},
\"outbox\": {
\"string\": \"$SECRET\"
}
}
}
}
}
EOF
"
# Перезапусти OnlyOffice
docker restart onlyoffice-standalone
```
**2. Добавь секрет в Nextcloud:**
```bash
docker exec -u www-data nextcloud-fresh php occ config:app:set onlyoffice jwt_secret --value="$SECRET"
docker exec -u www-data nextcloud-fresh php occ config:app:set onlyoffice jwt_header --value="Authorization"
```
---
## 📈 Статистика
### Текущая конфигурация:
- ✅ OnlyOffice Document Server: **9.0.4**
- ✅ Nextcloud ONLYOFFICE app: **9.11.0**
- ✅ Nextcloud: **30.0.2**
- ✅ SSL: самоподписанный сертификат (проверка отключена)
- ⚠️ JWT: **отключен** (включи для продакшна!)
---
## 🎯 Итог
### Что работает сейчас:
1.**CRM → OnlyOffice** - прямой доступ (молниеносно)
2.**Nextcloud → OnlyOffice** - через интеграцию (с версионированием)
3.**Создание новых файлов** в Nextcloud
4.**Редактирование** docx, xlsx, pptx
5.**Автосохранение** в обоих случаях
### Что использовать:
- **Работа в CRM** → используй прямой OnlyOffice (быстрее)
- **Работа в Nextcloud** → используй интеграцию (больше функций)
- **Создание новых файлов** → Nextcloud (удобнее)
---
**Попробуй открыть файл в Nextcloud! Должно работать!** 🚀
**Теперь у тебя:**
- ⚡ Быстрое редактирование через CRM
- 📚 Версионирование через Nextcloud
- 🆕 Создание новых файлов
- ✅ Единый OnlyOffice для всего!

View File

@@ -0,0 +1,271 @@
# OnlyOffice Standalone — Настройка и Интеграция с S3
**Дата:** 31 октября 2025
**Статус:****РАБОТАЕТ! Файлы открываются!**
⚠️ **TODO:** Доработать сохранение обратно в оригинальный путь S3
---
## 🎯 ЧТО СДЕЛАНО
### 1. Поднят новый Standalone OnlyOffice Document Server
**Контейнер:** `onlyoffice-standalone`
**Порт:** `127.0.0.1:8083` → проксируется через Nginx на `office.clientright.ru:9443`
**Docker Compose:** `/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/docker-compose.onlyoffice-standalone.yml`
**Конфигурация:**
- JWT отключён (`JWT_ENABLED=false`)
- WOPI отключён (`WOPI_ENABLED=false`)
- Полностью независим от Nextcloud
- Работает в standalone режиме
### 2. Настроен Nginx
**Файл:** `/etc/nginx/conf.d/01-onlyoffice-standalone.conf`
```nginx
server {
listen 147.45.146.17:9443 ssl http2;
server_name onlyoffice.clientright.ru office.clientright.ru;
# SSL от office.clientright.ru (используем тот же сертификат)
ssl_certificate /etc/letsencrypt/live/office.clientright.ru/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/office.clientright.ru/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8083;
# ВАЖНО: Host с портом для правильной генерации URL кеша
proxy_set_header Host $host:$server_port;
proxy_set_header X-Forwarded-Host $host:$server_port;
...
}
}
```
**Доступ:** `https://office.clientright.ru:9443`
### 3. Создан open_file_v3_standalone.php
**Файл:** `crm_extensions/file_storage/api/open_file_v3_standalone.php`
**Особенности:**
- ✅ Использует standalone OnlyOffice (порт 9443)
- ✅ Прямой S3 URL (публичный bucket)
- ✅ Уникальный `documentKey` при каждом запросе (обход кеша)
- ✅ Сохраняет маппинг `documentKey → S3 path` в Redis
- ✅ Поддержка Range requests из коробки (S3)
- ✅ CORS настроен правильно
### 4. Обновлён onlyoffice_callback.php
**Файл:** `crm_extensions/file_storage/api/onlyoffice_callback.php`
**Что делает:**
- ✅ Принимает callback от OnlyOffice при сохранении
- ✅ Скачивает изменённый файл от OnlyOffice
- ✅ Загружает в S3 (временно в папку `onlyoffice_saved/`)
- ⚠️ **TODO:** Сохранять в оригинальный путь (используя Redis маппинг)
### 5. Обновлён open_file_v2.php
**Файл:** `crm_extensions/file_storage/api/open_file_v2.php`
**Изменения:**
- ✅ Переключён на standalone OnlyOffice (9443)
- ✅ Использует прямой S3 URL (без proxy)
- ✅ Добавлена информация о пользователе (`user.id`, `user.name`)
- ✅ Улучшен CSS для полноэкранного режима
### 6. Создан s3_proxy.php
**Файл:** `crm_extensions/file_storage/api/s3_proxy.php`
**Особенности:**
- ✅ Поддержка HTTP Range requests (206 Partial Content)
- ✅ Правильные CORS headers
- ✅ Обработка HEAD requests
- ✅ Проксирование файлов из S3
*(Сейчас не используется, так как S3 bucket публичный и OnlyOffice работает напрямую)*
---
## 🚀 КАК ЭТО РАБОТАЕТ
### Открытие файла
1. **CRM** → Кнопка "Редактировать" → `open_file_v2.php?recordId=X&fileName=...`
2. **PHP скрипт:**
- Извлекает S3 путь из URL
- Генерирует уникальный `documentKey`
- Сохраняет маппинг `documentKey → S3 path` в Redis (TTL 24ч)
- Отдаёт HTML с OnlyOffice редактором
3. **Браузер:**
- Загружает OnlyOffice API с `office.clientright.ru:9443`
- OnlyOffice скачивает файл **напрямую из S3** (публичный bucket)
- Документ открывается в редакторе
### Сохранение файла
1. **Пользователь редактирует** → Автосохранение каждые несколько секунд
2. **OnlyOffice**`POST https://crm.clientright.ru/.../onlyoffice_callback.php`
3. **Callback скрипт:**
- Получает `status: 2` (файл сохранён)
- Скачивает изменённый файл от OnlyOffice
- **ВРЕМЕННО:** Сохраняет в `onlyoffice_saved/` в S3
- **TODO:** Получить оригинальный путь из Redis и перезаписать файл там
---
## ⚠️ TODO (НА ЗАВТРА)
### 1. Доработать сохранение в оригинальный путь
**Проблема:** Сейчас файлы сохраняются в `onlyoffice_saved/`, а не в оригинальный путь.
**Решение:**
```php
// В onlyoffice_callback.php:
$redis = new Predis\Client([...]);
$mapping = $redis->get("crm:onlyoffice:key:$documentKey");
$data = json_decode($mapping, true);
$originalPath = $data['s3_path'];
// Сохраняем в ОРИГИНАЛЬНЫЙ путь
$s3Client->putObject([
'Bucket' => $bucket,
'Key' => $originalPath, // ← СЮДА!
'Body' => $fileContent
]);
```
### 2. Обновить CRM JS для использования нового скрипта
**Файл:** `layouts/v7/lib/nextcloud-editor.js`
Изменить вызов с `open_file_v2.php` на `open_file_v3_standalone.php`.
### 3. Тестирование
- [ ] Открытие разных типов файлов (docx, xlsx, pptx)
- [ ] Редактирование и сохранение
- [ ] Совместная работа (несколько пользователей)
- [ ] Проверка что файлы сохраняются в правильный путь
### 4. Очистка
Удалить тестовые файлы:
- `test_onlyoffice_simple.php`
- `test_onlyoffice_direct.php`
- `test_presigned.php`
---
## 📊 РЕЗУЛЬТАТ
### ✅ ЧТО РАБОТАЕТ
- **OnlyOffice Standalone запущен и работает**
- **Файлы открываются БЫСТРО** (напрямую из S3)
- **Редактор загружается полностью**
- **Поддержка Range requests** (S3)
- **CORS настроен правильно**
- **Callback вызывается** при сохранении
### ⚠️ ЧТО НУЖНО ДОРАБОТАТЬ
- **Сохранение в оригинальный путь** (сейчас в `onlyoffice_saved/`)
- **Тестирование разных файлов** (некоторые могут быть повреждены)
- **Обновление CRM JS** для использования нового скрипта
---
## 🔧 ТЕХНИЧЕСКАЯ ИНФОРМАЦИЯ
### Порты
- **8081:** Старый OnlyOffice (через Nextcloud/WOPI) — `office.clientright.ru:8443`
- **8083:** **Новый Standalone OnlyOffice**`office.clientright.ru:9443`
### URLs
- **OnlyOffice API:** `https://office.clientright.ru:9443/web-apps/apps/api/documents/api.js`
- **Открытие файла:** `https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php`
- **Callback:** `https://crm.clientright.ru/crm_extensions/file_storage/api/onlyoffice_callback.php`
- **S3 Bucket:** `https://s3.twcstorage.ru/f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/`
### Redis Keys
```
crm:onlyoffice:key:{documentKey} → {"s3_path":"...", "record_id":"...", "created_at":...}
TTL: 86400 секунд (24 часа)
```
---
## 🐛 ИЗВЕСТНЫЕ ПРОБЛЕМЫ
### 1. Некоторые файлы показывают только toolbar
**Причина:** Документ может быть повреждён или пустой
**Решение:** Проверить файл вручную, попробовать открыть другой
### 2. Pre-signed URL не работают для публичного bucket
**Причина:** S3 Timeweb возвращает 403 для pre-signed URL если bucket публичный
**Решение:** Используем прямой S3 URL (bucket публичный, подпись не нужна)
### 3. Старый OnlyOffice (8443) использует Nextcloud кеш
**Причина:** OnlyOffice на порту 8443 интегрирован с Nextcloud через WOPI
**Решение:** Используем новый standalone OnlyOffice (9443)
---
## 📝 КОМАНДЫ ДЛЯ УПРАВЛЕНИЯ
### Перезапуск OnlyOffice Standalone
```bash
cd /var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage
docker-compose -f docker-compose.onlyoffice-standalone.yml restart
```
### Просмотр логов
```bash
docker logs -f onlyoffice-standalone
```
### Очистка кеша
```bash
docker exec onlyoffice-standalone rm -rf /var/lib/onlyoffice/documentserver/App_Data/cache/files/*
docker restart onlyoffice-standalone
```
### Проверка Redis маппинга
```bash
redis-cli -h crm.clientright.ru -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
KEYS 'crm:onlyoffice:key:*'
```
---
## 🎓 ЧТО УЗНАЛИ
1. **OnlyOffice Community Edition** работает в standalone режиме без ограничений на открытие файлов
2. **S3 публичный bucket** поддерживает Range requests и CORS из коробки
3. **Pre-signed URL** не работают для публичных bucket на S3 Timeweb
4. **OnlyOffice требует уникальный documentKey** для каждого открытия (иначе использует старый кеш)
5. **Host header важен** — OnlyOffice генерирует URL для кеша на основе Host
6. **Callback обязателен** для режима редактирования
---
**Автор:** AI Assistant
**Дата:** 31.10.2025 23:40
**Следующий шаг:** Доработать callback для сохранения в оригинальный путь S3

View File

@@ -0,0 +1,401 @@
# OnlyOffice Document Server - Настройки
**Важно:** У OnlyOffice Community Edition **НЕТ веб-интерфейса** для настроек!
Все настраивается через **JSON конфиг файлы**.
---
## 📂 Конфигурационные файлы
### Расположение:
```
/etc/onlyoffice/documentserver/
```
### Основные файлы:
1. **`default.json`** - базовые настройки по умолчанию (не трогать!)
2. **`production-linux.json`** - настройки для production (автоматически)
3. **`local.json`** - **локальные переопределения** (редактируем этот!)
4. **`local-production-linux.json`** - дополнительные кастомные настройки
**Приоритет:** `local-*.json` > `local.json` > `production-*.json` > `default.json`
---
## 🎛️ Что можно настроить
### 1. **JWT Token (безопасность)**
**Файл:** `local.json`
```json
{
"services": {
"CoAuthoring": {
"token": {
"enable": {
"request": {
"inbox": false, Проверять JWT в запросах от браузера
"outbox": false Добавлять JWT в callback запросы
},
"browser": false Требовать JWT от браузера
}
},
"secret": {
"inbox": {
"string": "ВАШ_СЕКРЕТНЫЙ_КЛЮЧ" Секрет для JWT
}
}
}
}
}
```
**Сейчас:** JWT отключен (`false`) - любой может использовать!
**Для продакшна:** Включить JWT (`true`) + установить секрет!
---
### 2. **SSL/TLS для callback**
**Файл:** `local-production-linux.json`
```json
{
"services": {
"CoAuthoring": {
"utils": {
"rejectUnauthorized": false Игнорировать SSL ошибки (у нас включено!)
}
}
}
}
```
**Зачем:** Если callback URL имеет самоподписанный сертификат.
---
### 3. **Фильтрация IP адресов**
**Файл:** `local-production-linux.json`
```json
{
"services": {
"CoAuthoring": {
"request-filtering-agent": {
"allowPrivateIPAddress": true, Разрешить приватные IP (у нас включено!)
"allowMetaIPAddress": true Разрешить мета-адреса
}
}
}
}
```
**Зачем:** Чтобы OnlyOffice мог вызывать callback на локальные IP (147.45.146.17).
---
### 4. **База данных (PostgreSQL)**
**Файл:** `local.json`
```json
{
"services": {
"CoAuthoring": {
"sql": {
"type": "postgres",
"dbHost": "localhost",
"dbPort": "5432",
"dbName": "onlyoffice",
"dbUser": "onlyoffice",
"dbPass": "onlyoffice"
}
}
}
}
```
**Сейчас:** Используется встроенная PostgreSQL в контейнере.
---
### 5. **Лимиты и производительность**
```json
{
"services": {
"CoAuthoring": {
"server": {
"port": 8000,
"workersPerCpu": 1, Воркеров на CPU (сейчас: 1)
"limits_tempfile_upload": 104857600, Макс размер загружаемого файла (100MB)
"limits_image_size": 26214400, Макс размер изображения (25MB)
"limits_image_download_timeout": 120000 Таймаут загрузки (2 мин)
}
}
}
}
```
---
### 6. **WOPI (интеграция с Microsoft)**
**Файл:** `local.json`
```json
{
"wopi": {
"enable": false WOPI отключен (не нужен нам)
}
}
```
**Зачем:** Для интеграции с Office Online, SharePoint и т.д.
---
### 7. **Логирование**
**Файл:** `/etc/onlyoffice/documentserver/log4js/production.json`
```json
{
"appenders": {
"file": {
"type": "file",
"filename": "/var/log/onlyoffice/documentserver/docservice/out.log",
"maxLogSize": 10485760, Макс размер лога (10MB)
"backups": 3 Количество бэкапов
}
},
"levels": {
"nodeJS": "WARN" Уровень логирования (DEBUG, INFO, WARN, ERROR)
}
}
```
---
## 🔧 Как редактировать настройки
### Вариант 1: Через `docker exec` (быстро)
```bash
# Редактировать local.json
docker exec -it onlyoffice-standalone nano /etc/onlyoffice/documentserver/local.json
# Перезапустить OnlyOffice
docker restart onlyoffice-standalone
```
### Вариант 2: Создать новый конфиг файл (рекомендуется)
```bash
# Создать кастомный конфиг
docker exec onlyoffice-standalone bash -c 'cat > /etc/onlyoffice/documentserver/local-production-linux.json << "EOF"
{
"services": {
"CoAuthoring": {
"server": {
"workersPerCpu": 2 ← Увеличить воркеры
},
"utils": {
"rejectUnauthorized": false
}
}
}
}
EOF
'
# Перезапустить
docker restart onlyoffice-standalone
```
---
## 📊 Текущие настройки (у нас)
### `local.json`:
- ✅ JWT **отключен** (`false`)
- ✅ PostgreSQL на `localhost:5432`
- ✅ RabbitMQ на `localhost`
- ✅ WOPI **отключен**
### `local-production-linux.json`:
-`rejectUnauthorized: false` - игнорируем SSL ошибки
-`allowPrivateIPAddress: true` - разрешаем callback на 147.45.146.17
-`allowMetaIPAddress: true`
---
## ⚙️ Полезные команды
### Просмотр текущих настроек:
```bash
# Основной конфиг
docker exec onlyoffice-standalone cat /etc/onlyoffice/documentserver/local.json | python3 -m json.tool
# Наш кастомный конфиг
docker exec onlyoffice-standalone cat /etc/onlyoffice/documentserver/local-production-linux.json | python3 -m json.tool
# Все конфиги
docker exec onlyoffice-standalone find /etc/onlyoffice/documentserver -name "*.json" -type f
```
### Редактирование:
```bash
# Редактировать через nano
docker exec -it onlyoffice-standalone nano /etc/onlyoffice/documentserver/local.json
# Или через vi
docker exec -it onlyoffice-standalone vi /etc/onlyoffice/documentserver/local.json
```
### Проверка синтаксиса JSON:
```bash
docker exec onlyoffice-standalone cat /etc/onlyoffice/documentserver/local.json | python3 -m json.tool
# Если ошибка - JSON невалидный!
```
### Перезапуск после изменений:
```bash
docker restart onlyoffice-standalone
sleep 20 # Подождать полного запуска
docker logs --tail 20 onlyoffice-standalone
```
### Просмотр логов:
```bash
# Логи Document Server
docker exec onlyoffice-standalone tail -100 /var/log/onlyoffice/documentserver/docservice/out.log
# Логи ошибок
docker exec onlyoffice-standalone tail -100 /var/log/onlyoffice/documentserver/docservice/err.log
# Логи nginx
docker exec onlyoffice-standalone tail -100 /var/log/onlyoffice/documentserver/nginx.error.log
```
---
## 🎯 Важные параметры для продакшна
### 1. Включить JWT (обязательно!)
```json
{
"services": {
"CoAuthoring": {
"token": {
"enable": {
"request": {
"inbox": true,
"outbox": true
}
}
},
"secret": {
"inbox": {
"string": "СГЕНЕРИРУЙ_СЛУЧАЙНЫЙ_КЛЮЧ_64_СИМВОЛА"
},
"outbox": {
"string": "СГЕНЕРИРУЙ_СЛУЧАЙНЫЙ_КЛЮЧ_64_СИМВОЛА"
}
}
}
}
}
```
### 2. Увеличить воркеры (если много пользователей)
```json
{
"services": {
"CoAuthoring": {
"server": {
"workersPerCpu": 2 По умолчанию 1, можно 2-4
}
}
}
}
```
### 3. Увеличить лимиты файлов
```json
{
"services": {
"CoAuthoring": {
"server": {
"limits_tempfile_upload": 209715200 200MB вместо 100MB
}
}
}
}
```
---
## 📚 Официальная документация
**Ссылки:**
- Конфигурация: https://api.onlyoffice.com/editors/config/
- JWT: https://api.onlyoffice.com/editors/signature/
- Docker: https://github.com/ONLYOFFICE/Docker-DocumentServer
---
## 🔍 Мониторинг OnlyOffice
### Healthcheck:
```bash
curl https://office.clientright.ru:9443/healthcheck
# Должен вернуть: true
```
### Версия:
```bash
docker exec onlyoffice-standalone cat /var/www/onlyoffice/documentserver/server/DocService/package.json | grep version
# Сейчас: 9.0.4
```
### Статистика:
```bash
docker stats onlyoffice-standalone
# Показывает: CPU, RAM, Network
```
---
## 💡 Советы
1. **Всегда делай бэкап конфигов перед изменениями:**
```bash
docker exec onlyoffice-standalone cp /etc/onlyoffice/documentserver/local.json /etc/onlyoffice/documentserver/local.json.backup
```
2. **Проверяй синтаксис JSON после редактирования:**
```bash
cat local.json | python3 -m json.tool
```
3. **Читай логи после перезапуска:**
```bash
docker logs -f onlyoffice-standalone
```
4. **Для продакшна - обязательно включи JWT!** Иначе кто угодно может использовать твой OnlyOffice сервер!
---
**Если нужен веб-интерфейс - смотри в сторону:**
- **ONLYOFFICE Workspace** (платный, но с полной админ-панелью)
- **Nextcloud + OnlyOffice** (есть UI в Nextcloud)
- **ownCloud + OnlyOffice** (есть UI в ownCloud)
**У нас:** Standalone Document Server (нет UI, но работает быстро!)

111
PROJECT_390983_FIXED.md Normal file
View File

@@ -0,0 +1,111 @@
# ✅ ПРОЕКТ 390983 - ВСЁ ИСПРАВЛЕНО!
## 📊 ИТОГОВАЯ СТАТИСТИКА:
**БЫЛО:**
-Все файлы недоступны (HTTP 403/404)
- ❌ Кнопки не работают
- ❌ Пути битые
**СТАЛО:**
- ✅ 8 файлов полностью работают
-Все кнопки работают
- ✅ Пути исправлены
## 🔧 ИСПРАВЛЕННЫЕ ПРОБЛЕМЫ:
### 1. Кнопка "📁 Папка в Nextcloud" не работала
**Проблема:** Отсутствовали JS функции
**Решение:** Добавлены:
- `openRecordFolder(moduleName, recordId, recordName)`
- `openAccountFolder(accountId, accountName)`
- `openContactFolder(contactId, firstName, lastName)`
- `openProjectFolder(projectId, projectName)`
### 2. Файлы в неправильной папке
**Проблема:** Файлы были в `Documents/Макарова...` вместо `Documents/Project/Макарова...`
**Решение:** Скопированы в правильную папку
### 3. Файлы были приватные
**Проблема:** ACL не позволял публичный доступ
**Решение:** Установлен `public-read` для всех файлов
### 4. HTML Entities в путях
**Проблема:** В БД были `&ETH;&oelig;...` вместо нормальных букв
**Решение:** Обновлены пути с правильным UTF-8
### 5. Пробелы не закодированы
**Проблема:** URL содержали пробелы вместо `%20`
**Решение:** Сохранены правильно URL-encoded пути
### 6. Кнопка "📚 Через Nextcloud" не работала
**Проблема:** Двойная кодировка при передаче URL из Smarty в JavaScript
**Решение:** Теперь `nextcloud_open.php` получает filename из БД по recordId
## 🎯 ФАЙЛЫ ПРОЕКТА 390983:
| ID | Файл | Статус |
|----|------|--------|
| 390986 | Договор | ✅ HTTP 200 |
| 390988 | Подтверждение оплаты | ✅ HTTP 200 |
| 390990 | Претензия | ✅ HTTP 200 |
| 390992 | Ответ на претензию | ✅ HTTP 200 |
| 390994 | Прочие документы | ✅ HTTP 200 |
| 390996 | 7 заявление потребителя | ✅ HTTP 200 |
| 391199 | 11 Доказательство соблюдения | ✅ HTTP 200 |
| 395695 | Исковое заявление (проект) | ✅ HTTP 200 |
| 396839 | Счёт и акт Аэрофлот | ❌ Отсутствует |
| 396840 | analytical_report | ❌ Отсутствует |
**ИТОГО: 8/10 (80%) восстановлено**
## 🚀 ЧТО ТЕПЕРЬ РАБОТАЕТ:
### ⚡ Кнопка "Быстро" (editInNextcloud)
- ✅ Открывает файлы прямо из S3 в OnlyOffice
- ✅ Без Nextcloud — быстрее!
- ✅ Работает идеально
- **РЕКОМЕНДУЕТСЯ ИСПОЛЬЗОВАТЬ**
### 📚 Кнопка "Через Nextcloud" (openViaNextcloud)
- ✅ ИСПРАВЛЕНА! Теперь работает
- Получает filename из БД (нет проблем с кодировкой)
- Открывает файл в Nextcloud Files UI
- Доступно версионирование
### 📄 Кнопка "Скачать"
- ✅ Работает для всех 8 файлов
- Прямая ссылка на S3
### 📁 Кнопка "Папка в Nextcloud"
- ✅ ИСПРАВЛЕНА во всех модулях!
- Открывает папку записи в Nextcloud
## 📝 ТЕХНИЧЕСКИЕ ДЕТАЛИ:
### Файлы в S3:
```
s3://f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/
crm2/CRM_Active_Files/Documents/Project/
Макарова_ИП_Большакова_Иннаорисовна_390983/
├─ Договор_390986.pdf
├─ Подтверждение_оплаты_390988.pdf
├─ Претензия_390990.pdf
├─ Ответ_на_претензию_390992.pdf
├─ Прочиеокументы_390994.pdf
├─ 7 заявление потребителя_390996.pdf
├─ 11 Доказательство соблюдения претензионного порядк_391199.pdf
└─ Исковое заявление (проект)_395695.docx
```
### В БД (vtiger_notes):
- `filename` = полный URL-encoded S3 URL
- `s3_key` = путь без домена
- `s3_bucket` = bucket ID
- `filelocationtype` = 'E' (External)
## 📅 ДАТА ИСПРАВЛЕНИЯ:
02.11.2025
## 🎉 РЕЗУЛЬТАТ:
**ВСЁ РАБОТАЕТ! МОЖНО ИСПОЛЬЗОВАТЬ!**

146
RESTORE_INSTRUCTIONS.md Normal file
View File

@@ -0,0 +1,146 @@
# Инструкция по восстановлению удаленных файлов и защите от повторных удалений
## 📋 Что было сделано:
1. ✅ Создан скрипт для настройки Nextcloud (`fix_nextcloud_settings.php`)
2. ✅ Создан скрипт для восстановления файлов (`restore_all_deleted_files.php`)
3. ✅ Создан скрипт для регулярной индексации (`nextcloud_scan_files.sh`)
---
## 🚀 Порядок выполнения:
### Шаг 1: Настройка Nextcloud (защита от удалений)
```bash
cd /var/www/fastuser/data/www/crm.clientright.ru
php fix_nextcloud_settings.php
```
**Что делает:**
- Отключает `DeleteOrphanedItems` (главная причина удалений)
- Включает `readonly` для External Storage
- Увеличивает retention корзины до 365 дней
- Создает скрипт для регулярной индексации
---
### Шаг 2: Восстановление файлов (сначала проверка)
**Сначала проверка (dry-run):**
```bash
php restore_all_deleted_files.php --dry-run
```
Это покажет, сколько файлов будет восстановлено без реального восстановления.
**Ограничение количества (для теста):**
```bash
php restore_all_deleted_files.php --dry-run 100
```
**Восстановление всех файлов:**
```bash
php restore_all_deleted_files.php
```
**Восстановление с ограничением (для безопасности):**
```bash
php restore_all_deleted_files.php "" 1000
```
**Восстановление только файлов проекта:**
```bash
php restore_all_deleted_files.php "" "" "crm2/CRM_Active_Files/Documents/Project/"
```
---
### Шаг 3: Настройка регулярной индексации
**Добавить в crontab:**
```bash
crontab -e
```
**Добавить строку:**
```
0 */6 * * * /var/www/fastuser/data/www/crm.clientright.ru/nextcloud_scan_files.sh
```
Это будет сканировать файлы каждые 6 часов.
**Или сканировать только внешнее хранилище (быстрее):**
Отредактируйте `nextcloud_scan_files.sh` и раскомментируйте строку:
```bash
docker exec -u www-data nextcloud-fresh php occ files:scan --path="/crm"
```
---
## 📊 Статистика удалений:
- **Всего delete markers:** ~25,200
- **Пик удалений:** 1 ноября 2025, 09:00 утра (7,080 файлов)
- **Причина:** DeleteOrphanedItems в Nextcloud
---
## ⚠️ ВАЖНО:
1. **Сначала настройте Nextcloud** (Шаг 1), чтобы предотвратить новые удаления
2. **Проверьте dry-run** перед массовым восстановлением
3. **Восстанавливайте постепенно** (по 1000-5000 файлов за раз)
4. **Проверяйте логи** после восстановления
---
## 🔍 Проверка статуса:
**Проверить статус задач Nextcloud:**
```bash
docker exec -u www-data nextcloud-fresh php occ background-job:list
```
**Проверить настройки External Storage:**
```bash
docker exec -u www-data nextcloud-fresh php occ files_external:list
```
**Проверить retention корзины:**
```bash
docker exec -u www-data nextcloud-fresh php occ config:app:get files trashbin_retention_obligation
```
**Проверить логи восстановления:**
```bash
ls -lh /var/www/fastuser/data/www/crm.clientright.ru/restore_log_*.json
```
---
## 🛡️ Защита от повторных удалений:
После выполнения всех шагов система будет защищена:
1. ✅ DeleteOrphanedItems отключен
2. ✅ External Storage в режиме readonly
3. ✅ Retention корзины увеличен до 365 дней
4. ✅ Регулярная индексация файлов настроена
---
## 📝 Логи:
- Логи восстановления: `restore_log_YYYY-MM-DD_HH-MM-SS.json`
- Логи индексации: `/var/log/nextcloud_scan.log`
---
## 🆘 Если что-то пошло не так:
1. Проверьте логи восстановления
2. Проверьте доступность Docker контейнера Nextcloud
3. Проверьте права доступа к S3
4. Проверьте логи Nextcloud: `docker logs nextcloud-fresh`

108
SESSION_LOG_ARCHIVE_FIX.md Normal file
View File

@@ -0,0 +1,108 @@
# Лог сессии: Исправление архивации проектов с S3 файлами
## Дата: 2025-11-21
## Проблема
Архивация проектов не работала после миграции на S3:
- Возвращался ответ `{"success":true,"result":"Nothing to archive"}`
- Затем появились ошибки `"_ is missing!"` для всех документов
- После исправления появились ошибки `"S3 file download failed"` для всех файлов
## Причины проблем
### 1. Неправильная обработка S3 файлов
- Метод `getPaths()` пытался обработать S3 файлы как локальные
- `Vtiger_Record_Model` не всегда содержит поля `s3_bucket`, `s3_key`, `filelocationtype`
- Нужно было явно запрашивать эти данные из БД
### 2. Отсутствие поддержки связанных документов
- Архив включал только документы самого проекта
- Не включались документы из связанных сущностей (контакты, контрагенты)
- Аналогично функционалу отправки исковых писем через pochta-sud.ru
### 3. Ошибки при скачивании из S3
- Неправильный путь к `vendor/autoload.php` (относительный путь не работал)
- Слишком длинное имя временного файла (`File name too long`)
- Использовался `basename($fileName)` где `$fileName` содержал URL-encoded полный путь
## Решение
### 1. Добавлен метод `getRelatedDocs($projectId)`
- Получает документы из связанных сущностей проекта:
- Контакт (`linktoaccountscontacts`)
- Контрагенты (`cf_1994`, `cf_2274`, `cf_2276`)
- Возвращает массив документов с полями: `notesid`, `title`, `filename`, `filelocationtype`, `s3_bucket`, `s3_key`
### 2. Добавлен метод `downloadS3File($s3Bucket, $s3Key, $fileName)`
- Скачивает файлы из S3 во временную папку
- Использует AWS SDK для работы с S3
- Сохраняет пути временных файлов для последующей очистки
- Обрабатывает ошибки с подробным логированием
### 3. Добавлен метод `cleanupTempFiles()`
- Очищает все временные файлы после создания архива
- Вызывается в `finally` блоке для гарантированной очистки
### 4. Исправлен метод `getPaths($docs)`
- Поддержка как `Vtiger_Record_Model` объектов, так и массивов из `getRelatedDocs`
- **ВСЕГДА** запрашивает `s3_bucket`, `s3_key`, `filelocationtype` из БД для Record Models
- Правильно определяет S3 файлы (`filelocationtype == 'E' && !empty($s3Bucket) && !empty($s3Key)`)
- Для S3 файлов вызывает `downloadS3File()`
- Для локальных файлов использует `getFileDetails()`
### 5. Исправлен метод `getArchive($id)`
- Для проектов собирает документы из основной записи и связанных сущностей
- Предотвращает дубликаты документов
- Вызывает `getPaths()` с объединенным списком документов
- Добавлено подробное логирование для отладки
- Обработка ошибок с возвратом детальной информации
### 6. Исправления в `downloadS3File()`
- Поиск `vendor/autoload.php` по нескольким путям (относительный и абсолютный)
- Использование короткого имени временного файла (только расширение, без полного пути)
- Подробное логирование в `/tmp/s3_download_debug.log`
## Измененные файлы
### `modules/Vtiger/services/Base.php`
- Добавлен метод `getRelatedDocs($projectId)` - получение документов из связанных сущностей
- Добавлен метод `downloadS3File($s3Bucket, $s3Key, $fileName)` - скачивание из S3
- Добавлен метод `cleanupTempFiles()` - очистка временных файлов
- Добавлено свойство `private static $tempFiles = []` - хранение путей временных файлов
- Исправлен метод `getPaths($docs)` - поддержка S3 и связанных документов
- Исправлен метод `getArchive($id)` - сбор документов из связанных сущностей для проектов
## Тестирование
### Тестовый скрипт `test_s3_download.php`
- Создан для прямого тестирования `downloadS3File()`
- Успешно скачал файл из S3 (9.5 МБ)
- Подтвердил работоспособность исправлений
### Результат
- ✅ Архив успешно создается с 25 документами для проекта 396447
- ✅ Включаются документы из проекта и связанных сущностей
- ✅ S3 файлы корректно скачиваются и добавляются в архив
- ✅ Временные файлы автоматически очищаются
## Технические детали
### S3 конфигурация
- Используется конфиг из `crm_extensions/file_storage/config.php`
- Endpoint: `https://s3.twcstorage.ru`
- Bucket и Key берутся из полей `vtiger_notes.s3_bucket` и `vtiger_notes.s3_key`
### Временные файлы
- Сохраняются в `sys_get_temp_dir()` (обычно `/tmp`)
- Имена: `s3_{uniqid}.{extension}`
- Автоматически удаляются после создания архива
### Логирование
- Основные логи: `error_log()` (системный лог PHP)
- Отладочные логи: `/tmp/s3_download_debug.log` (временный, удален после исправления)
- Ошибки: `/tmp/s3_download_errors.log` (временный, удален после исправления)
## Коммит
Изменения закоммичены в git с описанием исправлений.

View File

@@ -0,0 +1,191 @@
# 🔍 КАК РАБОТАЕТ WORKFLOW OCR
## ⚠️ **ВАЖНО: Нет ручного запуска!**
**Workflow OCR** работает **ТОЛЬКО через правила автоматизации**!
В нём **НЕТ** кнопки "Submit to OCR" в меню файла.
---
## 🔄 **КАК ЭТО РАБОТАЕТ:**
### **Принцип работы:**
```
1. Создаёшь правило workflow
2. Загружаешь файл в Nextcloud
3. Workflow Engine видит событие
4. Запускает OCR автоматически
5. Результат сохраняется
```
**Это АВТОМАТИЗАЦИЯ, а не ручной инструмент!**
---
## 📝 **КАК СОЗДАТЬ ПРАВИЛО:**
### **Вариант 1: Через интерфейс Nextcloud**
**Шаг 1: Открой настройки Flow**
```
https://office.clientright.ru:8443/settings/admin/workflow
```
**Шаг 2: Добавь правило**
1. Нажми **"Add new workflow"**
2. Выбери условия:
- **When:** File created
- **And:** File MIME type is `application/pdf`
- **And:** Path matches `Documents/Project/*`
3. Выбери действие:
- **Then:** `OCR processing`
4. Нажми **Save**
---
### **Вариант 2: Через команду (автоматически)**
Я могу создать правило автоматически через БД!
Команда создаст правило:
```
Имя: "CRM - Автоматический OCR судебных документов"
Условия:
- Файл создан
- Тип: PDF
- Имя содержит: решение, определение, постановление, договор, иск
Действие:
- OCR с русским + английским
- Создать searchable PDF
```
---
## 🧪 **КАК ПРОТЕСТИРОВАТЬ:**
### **После создания правила:**
1. **Загрузи тестовый PDF** в папку проекта
2. **Дождись 30-60 секунд** (OCR работает в фоне)
3. **Проверь через поиск:**
```
Files → Search (🔍)
Введи слово из документа
```
4. Если находит → **OCR работает!** ✅
---
## 📊 **КАК ПРОВЕРИТЬ ЧТО OCR ВЫПОЛНЕН:**
### **Способ 1: Через теги**
После OCR файл получит тег в Nextcloud (если настроено)
### **Способ 2: Через логи**
```bash
docker exec nextcloud-fresh tail -100 /var/www/html/data/nextcloud.log | grep -i ocr
```
### **Способ 3: Через поиск**
Если файл находится по тексту из него → OCR выполнен!
---
## ⚡ **АЛЬТЕРНАТИВА: Ручной OCR**
### **Если нужен РУЧНОЙ OCR (не автоматический):**
Есть другое приложение: **OCR** (не workflow_ocr!)
**Установка:**
```bash
docker exec -u www-data nextcloud-fresh php occ app:install ocr
```
**В нём есть:**
- Кнопка "OCR" в меню файла
- Ручной запуск для выбранных файлов
- Выбор языка
**НО:** Это старое приложение, может не работать на Nextcloud 31
---
## 🎯 **ЧТО ДЕЛАТЬ СЕЙЧАС:**
### **Вариант А: Я создам правило автоматически**
Создам через команду, займёт 10 секунд:
```sql
INSERT INTO oc_flow_operations ...
```
**Результат:** Все новые PDF в проектах автоматически пройдут OCR
---
### **Вариант Б: Ты создашь сам через интерфейс**
Я покажу пошагово со скриншотами:
```
Settings → Flow → Add rule → ...
```
**Результат:** Ты понимаешь как создавать правила
---
### **Вариант В: Установить приложение для ручного OCR**
```bash
docker exec -u www-data nextcloud-fresh php occ app:install ocr
```
**Результат:** Появится кнопка OCR в меню файла
**⚠️ Внимание:** Может не работать на Nextcloud 31!
---
## 💡 **МОЯ РЕКОМЕНДАЦИЯ:**
**Используй АВТОМАТИЧЕСКИЙ OCR** (Workflow):
- ✅ Настроил один раз → работает всегда
- ✅ Не нужно помнить запускать OCR
- ✅ Все документы обрабатываются одинаково
- ✅ Современный подход
**Ручной OCR** нужен редко (только для старых файлов)
---
## 🚀 **ДАВАЙ СОЗДАМ ПРАВИЛО?**
**Что создать:**
**Правило 1: Судебные документы**
```
PDF с словами: решение, определение, постановление, иск
→ OCR (русский + английский)
```
**Правило 2: Договоры**
```
PDF в папке Contracts
→ OCR (русский + английский)
```
**Правило 3: Все PDF в проектах < 10MB**
```
PDF в Documents/Project/*, размер < 10MB
→ OCR (русский + английский)
```
---
**Какое правило создать?** Или все три сразу? 😊

View File

@@ -0,0 +1,277 @@
# 🔄 WORKFLOW ПРИЛОЖЕНИЯ В NEXTCLOUD
## 1⃣ **Workflow OCR Backend** (workflow_ocr)
### **ЧТО ЭТО:**
Приложение для **автоматического распознавания текста (OCR)** из изображений и PDF файлов.
### **КАК РАБОТАЕТ:**
```
PDF/Изображение загружено →
→ Workflow Engine видит событие →
→ Tesseract OCR обрабатывает →
→ Создаёт текстовый файл с распознанным текстом
```
### **ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:**
**1. Автоматическое распознавание сканов:**
```
Правило: Если загружен файл *.pdf в папку /Scans
Действие: Распознать текст и создать .txt файл
```
**2. Обработка чеков:**
```
Правило: Если загружена фотография в /Receipts
Действие: OCR → поиск по тексту
```
**3. Юридические документы:**
```
Правило: Если загружен скан решения суда
Действие: Распознать текст → сделать документ searchable
```
### **НАСТРОЙКА:**
**1. Создать правило:**
```
Settings → Flow → Add rule:
- When: File created
- And: Mime type is image/* OR application/pdf
- And: Path matches /Scans/*
- Then: OCR processing
```
**2. Выбрать язык:**
- Russian (rus)
- English (eng)
- Multi (rus+eng)
### **ТЕКУЩИЙ СТАТУС:**
- ✅ Установлено: workflow_ocr 1.31.3
- ✅ Tesseract OCR: 5.5.0 (движок распознавания)
- ❌ Правил НЕТ (не настроено)
**Вывод:** Приложение установлено, но **НЕ ИСПОЛЬЗУЕТСЯ** (нет правил).
---
## 2⃣ **Flow Notifications** (flow_notifications)
### **ЧТО ЭТО:**
Приложение для **отправки уведомлений** на основе событий Workflow.
### **КАК РАБОТАЕТ:**
```
Событие в Nextcloud →
→ Workflow Engine проверяет правила →
→ Flow Notifications отправляет уведомление
```
### **ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:**
**1. Уведомление при загрузке важного файла:**
```
Правило: Если файл загружен в /Important
Действие: Отправить уведомление "Загружен важный файл"
```
**2. Уведомление о новом контракте:**
```
Правило: Если PDF загружен в /Contracts
Действие: Уведомить юриста
```
**3. Алерт при большом файле:**
```
Правило: Если размер файла > 100MB
Действие: Уведомить администратора
```
### **ТИПЫ УВЕДОМЛЕНИЙ:**
- 🔔 Push-уведомления в браузере
- 📧 Email
- 📱 Nextcloud app (Android/iOS)
- 🔗 Webhook (для интеграций)
### **НАСТРОЙКА:**
**Пример правила:**
```
Settings → Flow → Add rule:
- When: File created
- And: File size > 100000000 (100MB)
- Then: Send notification
- User: admin
- Message: "Large file uploaded: {file}"
```
### **ТЕКУЩИЙ СТАТУС:**
- ✅ Установлено: flow_notifications 2.0.1
- ❌ Правил НЕТ (не настроено)
**Вывод:** Приложение установлено, но **НЕ ИСПОЛЬЗУЕТСЯ**.
---
## 3⃣ **Workflow Engine** (workflowengine)
### **ЧТО ЭТО:**
**Основной движок** для всех workflow - автоматизация действий на основе событий.
### **АРХИТЕКТУРА:**
```
┌─────────────────┐
│ Workflow Engine │
│ (ядро) │
└────────┬─────────┘
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ workflow_ocr │ │flow_notifications││workflow_script│
│ (OCR) │ │ (уведомления) │ │ (скрипты) │
└───────────────┘ └───────────────┘ └───────────────┘
```
### **ДОСТУПНЫЕ ДЕЙСТВИЯ:**
**Стандартные:**
- ✅ Отправить уведомление
- ✅ Добавить тег
- ✅ Переместить файл
- ✅ Конвертировать в PDF
- ✅ Запустить скрипт
**С установленными приложениями:**
- ✅ OCR обработка (workflow_ocr)
- ✅ Конвертация PDF (workflow_pdf_converter)
- ✅ Запуск скриптов (workflow_script)
---
## 4⃣ **Другие установленные Workflow приложения:**
### **workflow_pdf_converter**
Конвертация документов в PDF:
- Word → PDF
- Excel → PDF
- Images → PDF
### **workflow_script**
Запуск произвольных скриптов при событиях:
```bash
#!/bin/bash
# Пример: отправка файла по API
curl -X POST https://api.example.com/upload \
-F "file=@$1"
```
---
## 🎯 **ТЕКУЩАЯ СИТУАЦИЯ:**
### **Установлено:**
```
✅ workflowengine: 2.13.0 (ядро)
✅ workflow_ocr: 1.31.3 (OCR)
✅ workflow_pdf_converter: 2.0.0 (PDF конвертер)
✅ workflow_script: 2.0.0 (скрипты)
✅ flow_notifications: 2.0.1 (уведомления)
```
### **Активных правил:** 0 ❌
**Вывод:** Все приложения установлены, но **НЕ НАСТРОЕНЫ** и **НЕ ИСПОЛЬЗУЮТСЯ**.
---
## 💡 **РЕКОМЕНДАЦИИ:**
### **Вариант 1: ИСПОЛЬЗОВАТЬ для автоматизации**
**Полезные сценарии для CRM:**
**1. OCR судебных решений:**
```
Правило: PDF загружен в Documents/Project/*/Судебные решения
Действие: OCR → текст извлечён → можно искать
```
**2. Уведомления о новых документах:**
```
Правило: Файл загружен в Documents/Project/*
Действие: Уведомить ответственного юриста
```
**3. Автоматическая конвертация в PDF:**
```
Правило: Word/Excel загружен
Действие: Конвертировать в PDF
```
### **Вариант 2: ОТКЛЮЧИТЬ если не нужно**
Если не планируете использовать автоматизацию:
```bash
docker exec -u www-data nextcloud-fresh php occ app:disable workflow_ocr
docker exec -u www-data nextcloud-fresh php occ app:disable flow_notifications
docker exec -u www-data nextcloud-fresh php occ app:disable workflow_pdf_converter
docker exec -u www-data nextcloud-fresh php occ app:disable workflow_script
```
**Оставить только ядро:** `workflowengine` (базовая функциональность)
---
## 🔧 **КАК НАСТРОИТЬ (если нужно):**
### **Шаг 1: Открыть настройки Flow**
```
https://office.clientright.ru:8443/settings/admin/workflow
```
### **Шаг 2: Создать правило**
```
1. Add rule
2. Выбрать событие (File created, File updated, etc.)
3. Добавить условия (path, mime type, size)
4. Выбрать действие (OCR, notification, convert)
5. Save
```
### **Шаг 3: Протестировать**
Загрузить тестовый файл → проверить что правило сработало
---
## 📊 **ИСПОЛЬЗУЕМЫЕ РЕСУРСЫ:**
### **Tesseract OCR:**
- Версия: 5.5.0
- Языки: русский, английский (установлены)
- CPU: ~50-100% на файл
- Время: ~10-30 сек на страницу
### **Дисковое пространство:**
OCR создаёт дополнительные текстовые файлы = +10-20% от размера PDF
---
## ⚠️ **ВАЖНО:**
**Если НЕ используете эти функции** → лучше **отключить**:
- Меньше нагрузка на систему
- Меньше ошибок в логах
- Проще поддержка
**Если НУЖНА автоматизация** → настроить правила workflow
---
**Вам нужна автоматизация (OCR, уведомления)?** Или отключить эти приложения? 🤔

520
ai_dialogs_export.txt Normal file
View File

@@ -0,0 +1,520 @@
╔══════════════════════════════════════════════════════════════════════════════╗
║ 📚 ИСТОРИЯ AI ДИАЛОГОВ С КЛИЕНТАМИ ║
║ Дата выгрузки: 29.10.2025 12:01:36 ║
╚══════════════════════════════════════════════════════════════════════════════╝
Всего записей: 29
════════════════════════════════════════════════════════════════════════════════
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 ПРОЕКТ: Курманжанова ИП Жгенти Дав<D0B0> (ID: 36937)
👤 КЛИЕНТ: Карина Курманжанова
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─ Диалог #1 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-20 11:37:53 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ [document] │
│ │
│ 🤖 ОТВЕТ: │
На основе найденных фрагментов, я могу │
│ рассказать о требованиях к │
│ документам: │
│ │
│ 1. Документы нужно предоставлять в │
│ формате PDF │
│ 2. Если у вас бумажные документы - их │
│ можно сфотографировать, но важно │
│ делать это ровно и четко, захватывая │
│ все страницы │
│ 3. Для подачи в суд документы лучше │
│ собрать в один PDF-файл │
│ 4. При личном участии в суде нужно │
│ иметь с собой: │
│ - распечатанные копии всех документов │
│ - паспорт │
│ - оригиналы документов │
│ │
│ Если вам нужна помощь с конкретными │
│ документами по вашему делу, уточните │
│ пожалуйста ваш вопрос. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 ПРОЕКТ: Юдин ИП Николаенков Васили (ID: 87121)
👤 КЛИЕНТ:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 ПРОЕКТ: Коробков _Тим_ДТП (ID: 354918)
👤 КЛИЕНТ: Fedor -
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─ Диалог #1 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-16 16:39:10 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ да, скажи пожалуйста, какой статус? │
│ │
│ 🤖 ОТВЕТ: │
На данный момент дело инициировано │
│ (находится в начальной стадии │
│ претензионно-исковой работы). Это │
│ значит, что работа по делу начата, но │
│ до суда дело еще не дошло. В системе │
│ есть загруженные документы, включая │
│ претензию, но окончательное решение │
│ по делу пока не принято. │
│ │
│ Отмечу важные моменты: │
│ - Дело активно (не завершено) │
│ - Находится на стадии досудебного │
│ урегулирования │
│ - Плановая дата завершения │
│ установлена на 16.11.2025 │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 ПРОЕКТ: Казаков УНИВЕРСИТЕТ СИНЕРГ (ID: 370163)
👤 КЛИЕНТ: Антон Казаков
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─ Диалог #1 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-10 11:35:19 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ Подробную статью Гайд давай │
│ │
│ 🤖 ОТВЕТ: │
│ Простите, но я не могу составить для │
│ вас подробную статью-гайд или │
│ рекомендации для публикации. Это │
│ выходит за рамки моих полномочий как │
│ юридического помощника по вашему │
│ делу. │
│ │
│ Вместо этого я рекомендую: │
│ │
│ 1. Обратиться к вашему юристу, который │
│ вел дело - он сможет дать │
│ квалифицированную консультацию о том, │
│ как безопасно делиться информацией о
│ деле. │
│ │
│ 2. Любые публикации о деле лучше │
│ предварительно согласовать с вашим │
│ юристом, чтобы избежать возможных │
│ рисков. │
│ │
│ Я могу отвечать на ваши вопросы по │
│ существу дела: о датах заседаний, │
│ суммах, статусе, имеющихся документах. │
Но составление рекомендаций для │
│ публикации - это отдельная │
│ юридическая услуга, которая требует │
│ участия вашего представителя. │
│ │
│ Хотите узнать конкретную информацию │
│ по вашему делу? │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 ПРОЕКТ: Сатюкова ООО ЭДЭКС (ID: 379166)
👤 КЛИЕНТ: Дария Сатюкова
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─ Диалог #1 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-14 20:20:03 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ [photo] │
│ │
│ 🤖 ОТВЕТ: │
│ Я не могу обрабатывать или │
│ анализировать фотографии. Если у вас │
│ есть вопрос по вашему делу, │
│ пожалуйста, задайте его текстом, и я │
│ постараюсь помочь найти нужную │
│ информацию. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ Диалог #2 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-14 20:20:53 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ Мне прислали письмо о неявке в суд │
│ │
│ 🤖 ОТВЕТ: │
│ Если вы получили письмо о неявке в суд, │
│ это важное уведомление, которое │
│ требует вашего внимания. Давайте я │
│ уточню детали вашей ситуации: │
│ │
│ 1. Получите письмо и внимательно │
│ ознакомьтесь с его содержанием │
│ 2. Если вы действительно не можете │
│ присутствовать на судебном заседании, │
│ вам нужно уведомить суд о причинах │
│ неявки │
│ 3. Для получения более точной │
│ информации о необходимых действиях в │
│ вашей конкретной ситуации, рекомендую │
│ обратиться к вашему юристу, который │
│ ведет ваше дело │
│ │
│ Также вы можете отслеживать │
│ информацию о деле, позвонив │
│ непосредственно в суд. │
│ │
│ Для того чтобы дать вам более точный │
│ ответ по вашей конкретной ситуации, │
│ мне нужны дополнительные детали. │
│ Пожалуйста, уточните: │
│ - Получили ли вы уже само письмо или │
│ только уведомление о нем? │
│ - От какого суда пришло письмо? │
│ - На какую дату назначено заседание? │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ Диалог #3 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-14 20:22:36 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ Я уже получила письмо │
│ От Гагаринского районного суда города │
│ Москвы │
На 25.11.2025 │
│ │
│ 🤖 ОТВЕТ: │
│ Если вы не можете присутствовать на │
│ судебном заседании 25.11.2025 в │
│ Гагаринском районном суде города │
│ Москвы, вам необходимо: │
│ │
│ 1. Подать в суд ходатайство об
│ отложении дела или о рассмотрении │
│ дела в ваше отсутствие. Это можно │
│ сделать: │
│ - через сайт суда в электронном виде │
│ - направить почтой │
│ - подать непосредственно в │
│ канцелярию суда │
│ │
│ 2. В ходатайстве нужно: │
│ - указать причины вашей неявки │
│ - приложить подтверждающие │
│ документы (если есть) │
│ - указать, просите ли вы рассмотреть │
│ дело без вашего участия или перенести │
│ заседание │
│ │
│ Для получения более подробной │
│ консультации по вашей конкретной │
│ ситуации рекомендую обратиться к │
│ вашему юристу, который ведет ваше │
│ дело. Он лучше знает обстоятельства и │
│ сможет подсказать оптимальное │
│ решение. │
│ │
│ Также вы можете отслеживать │
│ информацию о движении дела на │
│ официальном сайте суда. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ Диалог #4 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-16 16:19:25 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ Я уже получила письмо от Гагаринского │
│ районного суда │
│ │
│ 🤖 ОТВЕТ: │
│ Если вы не можете присутствовать на │
│ судебном заседании... │
└──────────────────────────────────────────────────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 ПРОЕКТ: Гафиев ООО ЭДЭКС (ID: 384256)
👤 КЛИЕНТ: Марсель Гафиев
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─ Диалог #1 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-25 17:39:54 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ Здравствуйте! Подготовьте пожалуйста │
│ памятку о дольнейсших действиях, │
│ (исполнении решения, и сроки) │
│ благодарю. │
│ │
│ 🤖 ОТВЕТ: │
На основании имеющейся информации, │
│ вот памятка о дальнейших действиях по │
│ исполнению решения суда: │
│ │
│ 1. Получение исполнительного листа: │
│ - После вступления решения в законную │
│ силу подайте заявление в суд о выдаче │
│ исполнительного листа │
│ - Вы можете получить исполнительный │
│ лист лично в суде или попросить │
│ направить его почтой │
│ - Срок для получения исполнительного │
│ листа - 3 года с момента вступления │
│ решения в законную силу │
│ │
│ 2. Сроки действия исполнительного │
│ листа: │
│ - Исполнительный лист действует 3 года │
с момента его выдачи │
│ - В течение этого срока вы можете │
│ предъявить его к исполнению │
│ │
│ 3. Порядок исполнения: │
│ - После получения исполнительного │
│ листа вы можете: │
│ * Предъявить его в службу судебных │
│ приставов │
│ * Если у вас есть банковские │
│ реквизиты должника, направить │
│ исполнительный лист напрямую в банк │
│ │
│ Если у вас возникнут вопросы по │
│ конкретным действиям или потребуется │
│ дополнительная информация, │
│ обратитесь к вашему юристу, который │
│ сможет дать более детальные │
│ рекомендации с учетом особенностей │
│ вашего дела. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 ПРОЕКТ: Лучина ООО ОТКРЫТЫЙ МИР (ID: 392972)
👤 КЛИЕНТ: Галина Лучина
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
┌─ Диалог #1 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-22 09:49:48 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ Возврат денежных средств выполнен в │
│ полном объёме │
│ │
│ 🤖 ОТВЕТ: │
│ По имеющимся данным, возвращена │
│ только часть суммы: │
│ - Цена договора составляла 130 960,00 ₽ │
│ - Возвращено 57 456 ₽ (частичный возврат, │
│ выполнен 29.09.2025) │
│ │
│ Таким образом, возврат денежных │
│ средств выполнен не в полном объеме. │
│ Остаток задолженности составляет 73 │
│ 504,00 ₽. │
│ │
│ Дело находится в претензионной │
│ стадии, ожидается ответ на претензию │
│ от ООО &quot;Открытый Мир&quot;. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ Диалог #2 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-22 09:51:29 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ Возврат выполнен 18.10 по итогам спора, │
│ открытого банком, картой которого │
│ произведена оплата. │
│ │
│ 🤖 ОТВЕТ: │
В данных нет информации о возврате │
│ денежных средств 18.10 через банк. Для │
│ корректного отражения в деле этой │
│ информации рекомендую: │
│ │
│ 1. Предоставить подтверждающие │
│ документы о возврате (чек, выписка или │
│ уведомление от банка) │
│ 2. После этого информация о полном │
│ возврате будет добавлена в карточку │
│ дела │
│ │
На текущий момент в деле отражен │
│ только частичный возврат 57 456 ₽ от │
│ 29.09.2025. Если у вас есть документы о
│ дополнительном возврате, пожалуйста, │
│ направьте их вашему юристу для │
│ актуализации информации по делу. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ Диалог #3 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-22 09:59:28 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ [photo] │
│ │
│ 🤖 ОТВЕТ: │
│ Да, я вижу подтверждение вашей │
│ информации. Действительно, возврат │
│ денежных средств по спору, │
│ инициированному банком, произошел. │
│ Однако для корректного отражения этой │
│ информации в деле нужно: │
│ │
│ 1. Предоставить подтверждающие │
│ документы о возврате (на выбор): │
│ - чек │
│ - выписка │
│ - уведомление от банка │
│ │
│ После предоставления подтверждающих │
│ документов информация о полном │
│ возврате будет добавлена в карточку │
│ дела. │
│ │
На данный момент в деле отражен только │
│ первый частичный возврат в размере 57 │
│ 456 ₽ от 29.09.2025. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ Диалог #4 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-22 10:03:46 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ [photo] │
│ │
│ 🤖 ОТВЕТ: │
В деле действительно сейчас учтен │
│ только частичный возврат денежных │
│ средств в размере 54 567 ₽. Чтобы │
│ зафиксировать полный возврат по спору │
│ через банк от 18.10, пожалуйста, │
│ предоставьте любой из подтверждающих │
│ документов: │
│ │
│ - чек, или │
│ - выписку, или │
│ - уведомление от банка │
│ │
│ После получения документа информация │
о полном возврате денежных средств │
│ будет добавлена в материалы дела. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ Диалог #5 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-22 10:03:50 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ [photo] │
│ │
│ 🤖 ОТВЕТ: │
│ Я вижу фотоподтверждение возврата │
│ средств. Для корректного отражения │
│ полного возврата в деле необходимо │
│ предоставить один из официальных │
│ подтверждающих документов: │
│ │
│ 1. Чек │
│ 2. Банковскую выписку │
│ 3. Уведомление от банка о возврате │
│ средств │
│ │
│ После получения любого из этих │
│ документов информация о полном │
│ возврате будет добавлена в карточку │
│ дела. │
│ │
На текущий момент в деле отражен │
│ только частичный возврат в размере 57 │
│ 456 ₽. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
┌─ Диалог #6 ─────────────────────────────────────────────────────────────────┐
│ 📅 2025-10-22 10:04:43 │
├──────────────────────────────────────────────────────────────────────────────┤
│ ❓ ВОПРОС: │
│ Иных документов не имею. │
│ │
│ 🤖 ОТВЕТ: │
│ Для корректного отражения полного │
│ возврата средств в деле, необходимо │
│ предоставить один из следующих │
│ подтверждающих документов: │
│ │
│ 1. Чек │
│ 2. Банковскую выписку │
│ 3. Уведомление от банка о возврате │
│ средств │
│ │
На данный момент в деле учтен только │
│ частичный возврат в размере 57 456 ₽. │
│ │
│ После предоставления любого из │
│ указанных документов, информация о
│ полном возврате будет добавлена в │
│ карточку дела. │
│ │
│ &lt;b&gt;Если у вас остались вопросы &mdash; │
│ мы с радостью ответим!&lt;/b&gt; │
│ Напишите их прямо здесь. │
└──────────────────────────────────────────────────────────────────────────────┘
════════════════════════════════════════════════════════════════════════════════
Конец выгрузки

416
aiassist/ai_sse.php Normal file
View File

@@ -0,0 +1,416 @@
<?php
/**
* SSE endpoint для AI Drawer через Redis Pub/Sub
*
* Подписывается на Redis канал и отправляет ответы AI через SSE
*/
// Отключаем буферизацию
while (@ob_end_flush());
// Настройки SSE
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache');
header('Connection: keep-alive');
header('Access-Control-Allow-Origin: *');
header('X-Accel-Buffering: no');
// Отключаем лимит времени
@ini_set('zlib.output_compression', 0);
@ini_set('implicit_flush', 1);
set_time_limit(0);
ignore_user_abort(false);
if (!function_exists('ai_sse_debug_log')) {
function ai_sse_debug_log($message) {
$logFile = '/var/www/fastuser/data/www/crm.clientright.ru/logs/ai_sse_debug.log';
$timestamp = date('Y-m-d H:i:s');
file_put_contents($logFile, "[{$timestamp}] {$message}\n", FILE_APPEND);
}
}
if (!function_exists('ai_sse_cache_response')) {
function ai_sse_cache_response($taskId, $responseText, $status = 'completed') {
$cacheKey = "ai:response:cache:{$taskId}";
$payload = [
'task_id' => $taskId,
'response' => $responseText,
'status' => $status,
'timestamp' => date('Y-m-d H:i:s')
];
$cacheValue = json_encode($payload, JSON_UNESCAPED_UNICODE);
ai_sse_debug_log("{$taskId}: caching response (status={$status})");
try {
if (class_exists('Redis')) {
$cacheRedis = new Redis();
if (!$cacheRedis->connect('crm.clientright.ru', 6379)) {
throw new Exception('Redis cache connection failed');
}
$cacheRedis->auth('CRM_Redis_Pass_2025_Secure!');
$result = $cacheRedis->setex($cacheKey, 300, $cacheValue);
$cacheRedis->close();
ai_sse_debug_log("{$taskId}: cache set via phpredis result=" . var_export($result, true));
return (bool)$result;
}
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$cacheRedis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'crm.clientright.ru',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!',
]);
$result = $cacheRedis->setex($cacheKey, 300, $cacheValue);
ai_sse_debug_log("{$taskId}: cache set via Predis result=" . var_export($result, true));
return (bool)$result;
} catch (Throwable $e) {
$message = "Failed to cache response for {$taskId}: " . $e->getMessage();
error_log("[AI SSE] {$message}");
ai_sse_debug_log("{$taskId}: {$message}");
return false;
}
}
}
// Отправляем начальный padding для Nginx
echo str_repeat(' ', 4096);
echo "\n\n";
flush();
// Функция для отправки события
function sendSSE($type, $data) {
// Для кастомных событий используем event:, для обычных data:
if (in_array($type, ['connected', 'response', 'error', 'heartbeat'])) {
echo "event: {$type}\n";
}
echo "data: " . json_encode([
'type' => $type,
'data' => $data,
'timestamp' => date('Y-m-d H:i:s')
], JSON_UNESCAPED_UNICODE) . "\n\n";
flush();
}
try {
// Получаем task_id из GET параметра
$taskId = $_GET['task_id'] ?? null;
if (!$taskId) {
sendSSE('error', ['message' => 'Missing task_id parameter']);
exit();
}
error_log("[AI SSE] Starting SSE connection for task_id: {$taskId}");
// Отправляем событие подключения
sendSSE('connected', [
'message' => 'Подключено к AI событиям',
'task_id' => $taskId
]);
// Сначала проверяем - может ответ уже есть в Redis ключе (если публикация была до подписки)
$cacheKey = "ai:response:cache:{$taskId}";
$cachedResponse = null;
try {
if (class_exists('Redis')) {
$checkRedis = new Redis();
if ($checkRedis->connect('crm.clientright.ru', 6379)) {
$checkRedis->auth('CRM_Redis_Pass_2025_Secure!');
$cachedResponse = $checkRedis->get($cacheKey);
$checkRedis->close();
}
} else {
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$checkRedis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'crm.clientright.ru',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!',
]);
$cachedResponse = $checkRedis->get($cacheKey);
}
if ($cachedResponse) {
error_log("[AI SSE] Found cached response in Redis key: {$cacheKey}");
$responseData = json_decode($cachedResponse, true);
if ($responseData && isset($responseData['response'])) {
sendSSE('response', [
'task_id' => $taskId,
'response' => $responseData['response']
]);
exit();
} elseif ($cachedResponse && !$responseData) {
// Если это просто строка
sendSSE('response', [
'task_id' => $taskId,
'response' => $cachedResponse
]);
exit();
}
}
} catch (Exception $e) {
error_log("[AI SSE] Error checking cache: " . $e->getMessage());
// Продолжаем с подпиской на канал
}
// Подключаемся к Redis
if (class_exists('Redis')) {
// Используем расширение Redis
$redis = new Redis();
if (!$redis->connect('crm.clientright.ru', 6379)) {
throw new Exception('Redis connection failed');
}
$redis->auth('CRM_Redis_Pass_2025_Secure!');
// Отправляем начальное событие
sendSSE('connected', [
'message' => 'Подключено к AI событиям',
'task_id' => $taskId
]);
// Подписываемся на канал для конкретного task_id
$channel = "ai:response:{$taskId}";
error_log("[AI SSE] Subscribing to channel: {$channel}");
// Используем правильный метод subscribe для расширения Redis
$redis->setOption(Redis::OPT_READ_TIMEOUT, -1); // Без таймаута
$redis->subscribe([$channel], function($redis, $channel, $message) use ($taskId) {
error_log("[AI SSE] Received message on channel {$channel}, length: " . strlen($message));
error_log("[AI SSE] Message preview: " . substr($message, 0, 500));
// Декодируем событие (может быть JSON или просто строка)
$event = json_decode($message, true);
// Если это не JSON, значит n8n отправил просто строку ответа - ОТЛИЧНО!
if (!$event || !is_array($event)) {
error_log("[AI SSE] Message is plain text (not JSON), treating as response");
ai_sse_debug_log("{$taskId}: plain text message received");
$cacheStored = ai_sse_cache_response($taskId, $message);
if ($cacheStored) {
error_log("[AI SSE] Cached plain text response for {$taskId}");
ai_sse_debug_log("{$taskId}: plain text response cached successfully");
} else {
error_log("[AI SSE] Failed to cache plain text response for {$taskId}");
}
// Отправляем ответ клиенту
sendSSE('response', [
'task_id' => $taskId,
'response' => $message // Используем сообщение как есть
]);
error_log("[AI SSE] Response sent to client, unsubscribing");
$redis->unsubscribe([$channel]);
return;
}
error_log("[AI SSE] Decoded event: " . json_encode($event, JSON_UNESCAPED_UNICODE));
// Проверяем что это сообщение для нашего task_id
// Принимаем если task_id совпадает ИЛИ если это просто ответ (может быть без task_id)
$eventTaskId = $event['task_id'] ?? $event['taskId'] ?? null;
$isOurTask = ($eventTaskId === $taskId) || ($eventTaskId === null && $channel === "ai:response:{$taskId}");
if ($isOurTask) {
// Отправляем событие клиенту
if (isset($event['response']) && !empty($event['response'])) {
error_log("[AI SSE] Sending response event for task {$taskId}");
ai_sse_debug_log("{$taskId}: JSON response received");
$cacheStored = ai_sse_cache_response($taskId, $event['response'], $event['status'] ?? 'completed');
if ($cacheStored) {
error_log("[AI SSE] Cached JSON response for {$taskId}");
ai_sse_debug_log("{$taskId}: cached JSON response");
} else {
error_log("[AI SSE] Failed to cache JSON response for {$taskId}");
}
sendSSE('response', [
'task_id' => $taskId,
'response' => $event['response']
]);
// Отписываемся после получения ответа
$redis->unsubscribe([$channel]);
} elseif (isset($event['error']) && !empty($event['error'])) {
error_log("[AI SSE] Sending error event for task {$taskId}");
sendSSE('error', [
'task_id' => $taskId,
'error' => $event['error']
]);
// Отписываемся после получения ошибки
$redis->unsubscribe([$channel]);
} else {
error_log("[AI SSE] Event received but no response/error field. Event keys: " . implode(', ', array_keys($event)));
// Если есть другие поля, попробуем найти ответ
$possibleResponse = $event['message'] ?? $event['text'] ?? $event['content'] ?? null;
if ($possibleResponse) {
error_log("[AI SSE] Found response in alternative field");
ai_sse_debug_log("{$taskId}: alternative response field detected");
$cacheStored = ai_sse_cache_response($taskId, $possibleResponse);
if ($cacheStored) {
error_log("[AI SSE] Cached alternative response for {$taskId}");
ai_sse_debug_log("{$taskId}: cached alternative response");
} else {
error_log("[AI SSE] Failed to cache alternative response for {$taskId}");
}
sendSSE('response', [
'task_id' => $taskId,
'response' => $possibleResponse
]);
$redis->unsubscribe([$channel]);
}
}
} else {
error_log("[AI SSE] Event task_id mismatch: expected {$taskId}, got {$eventTaskId}");
}
// Проверяем не отключился ли клиент
if (connection_aborted()) {
error_log("[AI SSE] Client disconnected, unsubscribing");
$redis->unsubscribe([$channel]);
}
});
} else {
// Используем Predis через Composer
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'crm.clientright.ru',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!',
'database' => 0,
]);
// Отправляем начальное событие
sendSSE('connected', [
'message' => 'Подключено к AI событиям через Predis',
'task_id' => $taskId
]);
// Подписываемся на канал
$channel = "ai:response:{$taskId}";
$pubsub = $redis->pubSubLoop();
$pubsub->subscribe($channel);
$lastHeartbeat = time();
foreach ($pubsub as $message) {
// Heartbeat каждые 15 секунд
if (time() - $lastHeartbeat > 15) {
sendSSE('heartbeat', ['timestamp' => time()]);
$lastHeartbeat = time();
}
// Обрабатываем только сообщения
if ($message->kind === 'message') {
error_log("[AI SSE] Received message via Predis on channel {$channel}: " . substr($message->payload, 0, 200));
$event = json_decode($message->payload, true);
// Если это не JSON, значит n8n отправил просто строку ответа
if (!$event || !is_array($event)) {
error_log("[AI SSE] Message is not JSON via Predis, treating as plain text response");
ai_sse_debug_log("{$taskId}: plain text message via Predis");
$cacheStored = ai_sse_cache_response($taskId, $message->payload);
if ($cacheStored) {
error_log("[AI SSE] Cached plain text response via Predis for {$taskId}");
ai_sse_debug_log("{$taskId}: cached plain text via Predis");
} else {
error_log("[AI SSE] Failed to cache plain text response via Predis for {$taskId}");
}
// Если это просто строка - отправляем как ответ
sendSSE('response', [
'task_id' => $taskId,
'response' => $message->payload // Используем сообщение как есть
]);
$pubsub->unsubscribe($channel);
break;
}
error_log("[AI SSE] Decoded event via Predis: " . json_encode($event, JSON_UNESCAPED_UNICODE));
// Проверяем что это сообщение для нашего task_id
$eventTaskId = $event['task_id'] ?? $event['taskId'] ?? null;
$isOurTask = ($eventTaskId === $taskId) || ($eventTaskId === null && $channel === "ai:response:{$taskId}");
if ($isOurTask) {
if (isset($event['response']) && !empty($event['response'])) {
error_log("[AI SSE] Sending response event via Predis for task {$taskId}");
ai_sse_debug_log("{$taskId}: JSON response via Predis");
$cacheStored = ai_sse_cache_response($taskId, $event['response'], $event['status'] ?? 'completed');
if ($cacheStored) {
error_log("[AI SSE] Cached JSON response via Predis for {$taskId}");
ai_sse_debug_log("{$taskId}: cached JSON via Predis");
} else {
error_log("[AI SSE] Failed to cache JSON response via Predis for {$taskId}");
}
sendSSE('response', [
'task_id' => $taskId,
'response' => $event['response']
]);
// Отписываемся после получения ответа
$pubsub->unsubscribe($channel);
break;
} elseif (isset($event['error']) && !empty($event['error'])) {
error_log("[AI SSE] Sending error event via Predis for task {$taskId}");
sendSSE('error', [
'task_id' => $taskId,
'error' => $event['error']
]);
// Отписываемся после получения ошибки
$pubsub->unsubscribe($channel);
break;
} else {
error_log("[AI SSE] Event received via Predis but no response/error field. Event keys: " . implode(', ', array_keys($event)));
// Если есть другие поля, попробуем найти ответ
$possibleResponse = $event['message'] ?? $event['text'] ?? $event['content'] ?? null;
if ($possibleResponse) {
error_log("[AI SSE] Found response in alternative field via Predis");
ai_sse_debug_log("{$taskId}: alternative response via Predis");
$cacheStored = ai_sse_cache_response($taskId, $possibleResponse);
if ($cacheStored) {
error_log("[AI SSE] Cached alternative response via Predis for {$taskId}");
ai_sse_debug_log("{$taskId}: cached alt response via Predis");
} else {
error_log("[AI SSE] Failed to cache alternative response via Predis for {$taskId}");
}
sendSSE('response', [
'task_id' => $taskId,
'response' => $possibleResponse
]);
$pubsub->unsubscribe($channel);
break;
}
}
} else {
error_log("[AI SSE] Event task_id mismatch via Predis: expected {$taskId}, got {$eventTaskId}");
}
}
// Проверяем не отключился ли клиент
if (connection_aborted()) {
$pubsub->unsubscribe($channel);
break;
}
}
}
} catch (Exception $e) {
error_log("[AI SSE] Error: " . $e->getMessage());
sendSSE('error', ['message' => 'Redis error: ' . $e->getMessage()]);
}
?>

View File

@@ -0,0 +1,107 @@
<?php
/**
* check_redis_response.php
* Проверка ответа в Redis (fallback если SSE не работает)
* Проверяет ключ Redis где хранится последний ответ
*/
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit();
}
try {
$taskId = $_GET['task_id'] ?? null;
if (!$taskId) {
throw new Exception('Missing task_id parameter');
}
$redisKey = "ai:response:cache:{$taskId}";
// Подключаемся к Redis
if (class_exists('Redis')) {
$redis = new Redis();
if (!$redis->connect('crm.clientright.ru', 6379)) {
throw new Exception('Redis connection failed');
}
$redis->auth('CRM_Redis_Pass_2025_Secure!');
// Пробуем получить ответ из кеша
$cachedResponse = $redis->get($redisKey);
if ($cachedResponse) {
$responseData = json_decode($cachedResponse, true);
if ($responseData && isset($responseData['response'])) {
echo json_encode([
'found' => true,
'response' => $responseData['response'],
'status' => $responseData['status'] ?? 'completed',
'timestamp' => $responseData['timestamp'] ?? null
]);
} else {
// Если это просто строка
echo json_encode([
'found' => true,
'response' => $cachedResponse
]);
}
} else {
echo json_encode([
'found' => false,
'message' => 'Ответ еще не готов или истек TTL'
]);
}
$redis->close();
} else {
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'crm.clientright.ru',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!',
]);
$cachedResponse = $redis->get($redisKey);
if ($cachedResponse) {
$responseData = json_decode($cachedResponse, true);
if ($responseData && isset($responseData['response'])) {
echo json_encode([
'found' => true,
'response' => $responseData['response'],
'status' => $responseData['status'] ?? 'completed',
'timestamp' => $responseData['timestamp'] ?? null
]);
} else {
echo json_encode([
'found' => true,
'response' => $cachedResponse
]);
}
} else {
echo json_encode([
'found' => false,
'message' => 'Ответ еще не готов или истек TTL'
]);
}
}
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'found' => false,
'error' => $e->getMessage()
]);
}
?>

View File

@@ -1,6 +1,7 @@
<?php
// aiassist/n8n_proxy.php
// Proxy между AI Drawer и n8n с асинхронной обработкой
// Proxy между AI Drawer и n8n
// n8n обрабатывает запрос и публикует ответ напрямую в Redis
header('Content-Type: application/json');
header('Access-Control-Allow-Origin: *');
@@ -22,17 +23,6 @@ if (!function_exists('curl_init')) {
}
try {
// Подключаемся к БД
include_once('../config.inc.php');
$conn = new mysqli($dbconfig['db_server'], $dbconfig['db_username'], $dbconfig['db_password'], $dbconfig['db_name']);
if ($conn->connect_error) {
throw new Exception("DB connection failed: " . $conn->connect_error);
}
$conn->set_charset('utf8mb4');
$input = json_decode(file_get_contents('php://input'), true);
if (!$input) {
@@ -48,22 +38,8 @@ try {
error_log("N8N Proxy: New task {$taskId} for session {$sessionId}");
// Сохраняем начальный статус в БД
$requestData = json_encode(['message' => $message, 'context' => $context], JSON_UNESCAPED_UNICODE);
$stmt = $conn->prepare("INSERT INTO ai_responses (task_id, session_id, status, request_data) VALUES (?, ?, 'processing', ?)");
$stmt->bind_param('sss', $taskId, $sessionId, $requestData);
if (!$stmt->execute()) {
throw new Exception("Failed to save task: " . $stmt->error);
}
$stmt->close();
$conn->close();
// Формируем callback URL
$callbackUrl = 'https://crm.clientright.ru/callback_ai_response.php';
// Отправляем запрос в n8n
// n8n обработает и опубликует ответ напрямую в Redis: ai:response:{taskId}
$n8nWebhookUrl = 'https://n8n.clientright.pro/webhook/0b20bf1e-7cda-4dc8-899e-a7c3be4096c0';
$payload = [
@@ -71,12 +47,15 @@ try {
'context' => $context,
'sessionId' => $sessionId,
'taskId' => $taskId,
'callbackUrl' => $callbackUrl,
'redisChannel' => "ai:response:{$taskId}", // Канал для публикации ответа
'redisHost' => 'crm.clientright.ru',
'redisPort' => 6379,
'redisPassword' => 'CRM_Redis_Pass_2025_Secure!',
'timestamp' => date('Y-m-d H:i:s'),
'source' => 'crm-client'
];
error_log("N8N Proxy: Sending to n8n - Task: {$taskId}");
error_log("N8N Proxy: Sending to n8n - Task: {$taskId}, Redis channel: ai:response:{$taskId}");
$ch = curl_init();
curl_setopt_array($ch, [
@@ -105,11 +84,13 @@ try {
}
// Возвращаем task_id клиенту
// Клиент подпишется на SSE и получит ответ когда n8n опубликует в Redis
echo json_encode([
'success' => true,
'task_id' => $taskId,
'status' => 'accepted',
'message' => 'Запрос принят в обработку'
'message' => 'Запрос принят в обработку',
'redisChannel' => "ai:response:{$taskId}" // Для информации
]);
} catch (Exception $e) {

View File

@@ -0,0 +1,236 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
$s3Bucket = $config['s3']['bucket'];
echo "Детальный анализ паттернов удалений\n";
echo str_repeat("=", 80) . "\n\n";
try {
$s3Client = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => $config['s3']['region'],
'endpoint' => $config['s3']['endpoint'],
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $config['s3']['key'],
'secret' => $config['s3']['secret'],
],
'suppress_php_deprecation_warning' => true
]);
// Анализируем удаления по времени
echo "1. Анализ удалений по времени суток...\n";
$deletionsByHour = [];
$deletionsByDay = [];
$batchDeletions = []; // Массовые удаления (много файлов за короткое время)
$totalChecked = 0;
$maxToCheck = 10000;
try {
$isTruncated = true;
$continuationToken = null;
$pageCount = 0;
$maxPages = 20;
$currentBatch = [];
$lastDeleteTime = null;
while ($isTruncated && $pageCount < $maxPages && $totalChecked < $maxToCheck) {
$params = [
'Bucket' => $s3Bucket,
'Prefix' => 'crm2/CRM_Active_Files/Documents/Project/',
'MaxKeys' => 1000
];
if ($continuationToken) {
$params['ContinuationToken'] = $continuationToken;
}
$versions = $s3Client->listObjectVersions($params);
$pageCount++;
if (isset($versions['DeleteMarkers'])) {
foreach ($versions['DeleteMarkers'] as $marker) {
$totalChecked++;
$deleteDate = isset($marker['LastModified']) ? $marker['LastModified'] : null;
if ($deleteDate) {
$dateTime = new DateTime($deleteDate);
$hour = $dateTime->format('H');
$day = $dateTime->format('Y-m-d');
if (!isset($deletionsByHour[$hour])) {
$deletionsByHour[$hour] = 0;
}
$deletionsByHour[$hour]++;
if (!isset($deletionsByDay[$day])) {
$deletionsByDay[$day] = 0;
}
$deletionsByDay[$day]++;
// Определяем массовые удаления (более 10 файлов за минуту)
$deleteTimestamp = strtotime($deleteDate);
if ($lastDeleteTime && abs($deleteTimestamp - $lastDeleteTime) < 60) {
$currentBatch[] = $marker;
} else {
if (count($currentBatch) > 10) {
$batchDeletions[] = [
'count' => count($currentBatch),
'time' => date('Y-m-d H:i:s', $lastDeleteTime),
'files' => array_slice($currentBatch, 0, 5) // Первые 5 для примера
];
}
$currentBatch = [$marker];
}
$lastDeleteTime = $deleteTimestamp;
}
}
}
$isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated'];
$continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null;
if (!$isTruncated) {
break;
}
}
// Проверяем последний батч
if (count($currentBatch) > 10) {
$batchDeletions[] = [
'count' => count($currentBatch),
'time' => $lastDeleteTime ? date('Y-m-d H:i:s', $lastDeleteTime) : 'неизвестно',
'files' => array_slice($currentBatch, 0, 5)
];
}
echo " Проверено delete markers: $totalChecked\n\n";
echo " Удаления по часам суток:\n";
ksort($deletionsByHour);
foreach ($deletionsByHour as $hour => $count) {
if ($count > 0) {
echo " {$hour}:00 - " . ($hour + 1) . ":00: $count удалений\n";
}
}
echo "\n";
echo " Удаления по дням (топ 15):\n";
arsort($deletionsByDay);
$count = 0;
foreach ($deletionsByDay as $day => $deleteCount) {
echo " $day: $deleteCount удалений\n";
if (++$count >= 15) break;
}
echo "\n";
if (!empty($batchDeletions)) {
echo " Массовые удаления (более 10 файлов за минуту): " . count($batchDeletions) . "\n";
foreach (array_slice($batchDeletions, 0, 5) as $batch) {
echo " Время: {$batch['time']}, удалено файлов: {$batch['count']}\n";
}
echo "\n";
}
} catch (\Aws\Exception\AwsException $e) {
echo " Ошибка: " . $e->getMessage() . "\n";
}
// Проверяем, может быть это связано с удалением документов в CRM
echo "2. Проверка связи с удалением документов в CRM...\n";
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// Проверяем, сколько документов было удалено в последние дни
$stmt = $pdo->prepare('
SELECT DATE(e.modifiedtime) as delete_date, COUNT(*) as count
FROM vtiger_crmentity e
INNER JOIN vtiger_notes n ON n.notesid = e.crmid
WHERE e.deleted = 1
AND n.filelocationtype = "E"
AND e.modifiedtime >= DATE_SUB(NOW(), INTERVAL 60 DAY)
GROUP BY DATE(e.modifiedtime)
ORDER BY delete_date DESC
LIMIT 20
');
$stmt->execute();
$deletedDocs = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($deletedDocs)) {
echo " Удаленные документы в CRM (за последние 60 дней):\n";
foreach ($deletedDocs as $doc) {
echo " {$doc['delete_date']}: {$doc['count']} документов\n";
}
echo "\n";
} else {
echo " Удаленных документов не найдено\n\n";
}
// Сравниваем даты удалений в S3 и CRM
echo "3. Сравнение дат удалений в S3 и CRM...\n";
if (!empty($deletionsByDay) && !empty($deletedDocs)) {
echo " Сравнение:\n";
foreach ($deletedDocs as $doc) {
$crmDate = $doc['delete_date'];
$s3Count = $deletionsByDay[$crmDate] ?? 0;
$crmCount = $doc['count'];
if ($s3Count > 0) {
echo " $crmDate:\n";
echo " Удалено в CRM: $crmCount документов\n";
echo " Delete markers в S3: $s3Count\n";
if ($s3Count > $crmCount * 2) {
echo " ⚠️ В S3 удалено значительно больше файлов!\n";
} elseif (abs($s3Count - $crmCount) <= 10) {
echo " ✅ Количество примерно совпадает\n";
}
echo "\n";
}
}
}
echo str_repeat("=", 80) . "\n";
echo "ВЫВОДЫ:\n\n";
// Определяем наиболее вероятную причину
$maxHour = array_search(max($deletionsByHour), $deletionsByHour);
$maxDay = array_search(max($deletionsByDay), $deletionsByDay);
$maxDayCount = max($deletionsByDay);
echo "1. Пик удалений:\n";
echo " - Время: {$maxHour}:00\n";
echo " - День: $maxDay ($maxDayCount удалений)\n\n";
if ($maxHour >= 6 && $maxHour <= 8) {
echo "2. 💡 ВЕРОЯТНАЯ ПРИЧИНА: Автоматическая задача (cron job)\n";
echo " Удаления происходят рано утром (6-8 утра) - типичное время для cron\n\n";
}
if (!empty($batchDeletions)) {
echo "3. 💡 ВЕРОЯТНАЯ ПРИЧИНА: Массовое удаление (скрипт или автоматизация)\n";
echo " Найдено " . count($batchDeletions) . " случаев массового удаления\n\n";
}
echo "4. 💡 РЕКОМЕНДАЦИЯ: Проверить:\n";
echo " - DeleteOrphanedItems в Nextcloud (запускается ежедневно)\n";
echo " - Cron задачи, которые могут удалять файлы\n";
echo " - Логи Nextcloud на предмет массовых удалений\n";
} catch (Exception $e) {
echo "ОШИБКА: " . $e->getMessage() . "\n";
echo "Trace: " . $e->getTraceAsString() . "\n";
}

203
analyze_deletions.php Normal file
View File

@@ -0,0 +1,203 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
$s3Bucket = $config['s3']['bucket'];
echo "Анализ удалений файлов из S3\n";
echo str_repeat("=", 80) . "\n\n";
try {
$s3Client = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => $config['s3']['region'],
'endpoint' => $config['s3']['endpoint'],
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $config['s3']['key'],
'secret' => $config['s3']['secret'],
],
'suppress_php_deprecation_warning' => true
]);
// Анализируем паттерны удалений
echo "1. Анализ паттернов удалений по датам...\n";
$deletionsByDate = [];
$deletionsByProject = [];
$totalChecked = 0;
$maxToCheck = 5000; // Ограничиваем для скорости
try {
$isTruncated = true;
$continuationToken = null;
$pageCount = 0;
$maxPages = 10;
while ($isTruncated && $pageCount < $maxPages && $totalChecked < $maxToCheck) {
$params = [
'Bucket' => $s3Bucket,
'Prefix' => 'crm2/CRM_Active_Files/Documents/Project/',
'MaxKeys' => 1000
];
if ($continuationToken) {
$params['ContinuationToken'] = $continuationToken;
}
$versions = $s3Client->listObjectVersions($params);
$pageCount++;
if (isset($versions['DeleteMarkers'])) {
foreach ($versions['DeleteMarkers'] as $marker) {
$totalChecked++;
$key = $marker['Key'];
$deleteDate = isset($marker['LastModified']) ? $marker['LastModified'] : null;
if ($deleteDate) {
$dateKey = substr($deleteDate, 0, 10); // YYYY-MM-DD
if (!isset($deletionsByDate[$dateKey])) {
$deletionsByDate[$dateKey] = 0;
}
$deletionsByDate[$dateKey]++;
}
// Извлекаем ID проекта из пути
if (preg_match('/Project\/([^\/]+)_(\d+)\//', $key, $matches)) {
$projectId = $matches[2];
if (!isset($deletionsByProject[$projectId])) {
$deletionsByProject[$projectId] = 0;
}
$deletionsByProject[$projectId]++;
}
}
}
$isTruncated = isset($versions['IsTruncated']) && $versions['IsTruncated'];
$continuationToken = isset($versions['NextContinuationToken']) ? $versions['NextContinuationToken'] : null;
if (!$isTruncated) {
break;
}
}
echo " Проверено delete markers: $totalChecked\n\n";
// Сортируем по датам
krsort($deletionsByDate);
echo " Удаления по датам (топ 20):\n";
$count = 0;
foreach ($deletionsByDate as $date => $count) {
if ($count > 0) {
echo " $date: $count удалений\n";
if (++$count >= 20) break;
}
}
echo "\n";
// Сортируем проекты по количеству удалений
arsort($deletionsByProject);
echo " Проекты с наибольшим количеством удалений (топ 10):\n";
$count = 0;
foreach ($deletionsByProject as $projectId => $deleteCount) {
echo " Проект $projectId: $deleteCount удалений\n";
if (++$count >= 10) break;
}
echo "\n";
} catch (\Aws\Exception\AwsException $e) {
echo " Ошибка: " . $e->getMessage() . "\n";
}
// Проверяем логи системы
echo "2. Проверка логов на наличие записей об удалениях...\n";
$logFiles = [
'/var/log/nginx/error.log',
'/var/log/apache2/error.log',
'/var/www/fastuser/data/www/crm.clientright.ru/logs/debug.log',
'/var/www/fastuser/data/www/crm.clientright.ru/logs/s3_debug.log',
];
foreach ($logFiles as $logFile) {
if (file_exists($logFile)) {
echo " Проверка: $logFile\n";
$lines = file($logFile);
if ($lines) {
$deleteLines = array_filter($lines, function($line) {
return stripos($line, 'delete') !== false &&
(stripos($line, 's3') !== false || stripos($line, 'file') !== false);
});
if (!empty($deleteLines)) {
echo " Найдено строк с упоминанием удалений: " . count($deleteLines) . "\n";
echo " Последние 5 записей:\n";
foreach (array_slice($deleteLines, -5) as $line) {
echo " " . substr(trim($line), 0, 150) . "\n";
}
} else {
echo " Записей об удалениях не найдено\n";
}
}
echo "\n";
}
}
// Проверяем, есть ли скрипты крона, которые могут удалять файлы
echo "3. Поиск скриптов, которые могут удалять файлы...\n";
$cronFiles = [
'/var/spool/cron/crontabs/root',
'/etc/cron.d/',
'/var/www/fastuser/data/www/crm.clientright.ru/cron/',
];
foreach ($cronFiles as $cronPath) {
if (file_exists($cronPath)) {
echo " Проверка: $cronPath\n";
if (is_dir($cronPath)) {
$files = glob($cronPath . '*');
foreach ($files as $file) {
if (is_file($file)) {
$content = file_get_contents($file);
if (stripos($content, 'delete') !== false || stripos($content, 'remove') !== false) {
echo " Найден файл: $file\n";
echo " Содержит упоминания удаления\n";
}
}
}
} else {
$content = file_get_contents($cronPath);
if (stripos($content, 'delete') !== false || stripos($content, 'remove') !== false) {
echo " Файл содержит упоминания удаления\n";
}
}
}
}
echo "\n";
// Проверяем, может быть это связано с синхронизацией Nextcloud
echo "4. Проверка связи с Nextcloud синхронизацией...\n";
$nextcloudFiles = glob('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/*sync*.php');
if (!empty($nextcloudFiles)) {
echo " Найдено файлов синхронизации: " . count($nextcloudFiles) . "\n";
foreach ($nextcloudFiles as $file) {
$content = file_get_contents($file);
if (stripos($content, 'delete') !== false) {
echo " $file содержит код удаления\n";
}
}
} else {
echo " Файлы синхронизации не найдены\n";
}
echo "\n";
} catch (Exception $e) {
echo "ОШИБКА: " . $e->getMessage() . "\n";
echo "Trace: " . $e->getTraceAsString() . "\n";
}

251
api_attach_documents.php Normal file
View File

@@ -0,0 +1,251 @@
<?php
/**
* API для привязки документов к проекту/заявке
*
* Использование из n8n:
* POST https://crm.clientright.ru/api_attach_documents.php
*
* Входные данные (JSON массив):
* [
* {
* "contact_id": "320096",
* "project_id": "396868",
* "ticket_id": "396936",
* "filename": "boarding_pass.pdf",
* "file_type": "flight_delay_boarding_or_ticket",
* "file": "/bucket/path/to/file.pdf"
* }
* ]
*/
error_reporting(E_ALL);
ini_set('display_errors', '0');
// Функция для логирования
function log_message($message) {
$timestamp = date('Y-m-d H:i:s');
$line = "[$timestamp] $message\n";
@file_put_contents(__DIR__ . '/logs/api_attach_documents.log', $line, FILE_APPEND | LOCK_EX);
error_log('[api_attach_documents] ' . $message);
}
// Функция для JSON ответа
function json_response($data, $code = 200) {
if (!headers_sent()) {
http_response_code($code);
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type');
}
echo json_encode($data, JSON_UNESCAPED_UNICODE);
exit;
}
// CORS preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
json_response(['status' => 'ok']);
}
// Только POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
json_response(['success' => false, 'error' => 'Method not allowed'], 405);
}
log_message('=== START API REQUEST ===');
// Получаем входные данные
$input = file_get_contents('php://input');
log_message('Raw input: ' . substr($input, 0, 500));
$input = ltrim($input, "\xEF\xBB\xBF\x00\x09\x0A\x0D\x20");
$data = json_decode($input, true);
if (json_last_error() !== JSON_ERROR_NONE) {
log_message('❌ JSON Error: ' . json_last_error_msg());
json_response([
'success' => false,
'error' => 'Invalid JSON: ' . json_last_error_msg()
], 400);
}
// Поддерживаем как массив, так и одиночный объект
$documents_array = is_array($data) && isset($data[0]) ? $data : [$data];
log_message('Processing ' . count($documents_array) . ' document(s)');
// Обрабатываем каждый документ
$processed_documents = [];
$S3_HOST = 'https://s3.twcstorage.ru';
foreach ($documents_array as $idx => $doc) {
$contact_id = $doc['contact_id'] ?? null;
$project_id = $doc['project_id'] ?? null;
$ticket_id = $doc['ticket_id'] ?? null;
// Поддерживаем оба формата: file и file_url
$file_path = $doc['file'] ?? $doc['file_url'] ?? null;
if (!$file_path) {
log_message("❌ Document #{$idx}: missing 'file' or 'file_url'");
continue;
}
// Строим полный S3 URL
if (strpos($file_path, 'http') === 0) {
$file_url = $file_path;
} elseif (strpos($file_path, '/') === 0) {
$file_url = $S3_HOST . $file_path;
} else {
$file_url = $S3_HOST . '/' . $file_path;
}
// Поддерживаем оба формата: filename и file_name
$file_name = $doc['filename'] ?? $doc['file_name'] ?? null;
if (!$file_name) {
log_message("❌ Document #{$idx}: missing 'filename' or 'file_name'");
continue;
}
$file_type = $doc['file_type'] ?? 'Документ';
// Валидация обязательных полей
if (!$contact_id || !$project_id) {
log_message("❌ Document #{$idx}: missing contact_id or project_id");
continue;
}
log_message(" [{$idx}] {$file_name} (type: {$file_type})");
log_message(" Contact: {$contact_id}, Project: {$project_id}, Ticket: " . ($ticket_id ?: 'N/A'));
log_message(" File URL: {$file_url}");
$processed_documents[] = [
'url' => $file_url,
'file_name' => $file_name,
'description' => $file_type,
'projectid' => (int)$project_id,
'ticket_id' => $ticket_id ? (int)$ticket_id : null,
'contactid' => (int)$contact_id,
'pages' => 1
];
}
if (empty($processed_documents)) {
log_message('❌ No valid documents to process');
json_response([
'success' => false,
'error' => 'No valid documents to process'
], 400);
}
log_message('📤 Sending ' . count($processed_documents) . ' documents to upload_documents_to_crm.php');
// Формируем запрос к upload_documents_to_crm.php
$upload_url = 'https://crm.clientright.ru/upload_documents_to_crm.php';
// Берем общие параметры из первого документа
$first_doc = $processed_documents[0];
$payload = json_encode([
'documents' => $processed_documents,
'projectid' => $first_doc['projectid'],
'ticket_id' => $first_doc['ticket_id'],
'user_id' => 1
], JSON_UNESCAPED_UNICODE);
log_message('Payload: ' . substr($payload, 0, 500));
// Отправляем запрос
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $upload_url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 60);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Content-Length: ' . strlen($payload)
]);
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($response === false) {
log_message('❌ CURL error: ' . curl_error($ch));
json_response([
'success' => false,
'error' => 'Internal error: ' . curl_error($ch)
], 500);
}
log_message("Response HTTP code: {$http_code}");
log_message("Response: " . substr($response, 0, 500));
// Парсим ответ
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
log_message('❌ Failed to parse response JSON: ' . json_last_error_msg());
json_response([
'success' => false,
'error' => 'Invalid response from upload service'
], 500);
}
// Проверяем успешность
if ($result && $result['success'] && isset($result['results'])) {
$results_array = $result['results'];
// Формируем ответ
$processed_results = [];
$errors = [];
foreach ($results_array as $idx => $res) {
if ($res['status'] === 'success') {
$crm_result = $res['crm_result'] ?? [];
$processed_results[] = [
'document_id' => $crm_result['document_id'] ?? null,
'document_numeric_id' => $crm_result['document_numeric_id'] ?? null,
'attached_to' => isset($res['ticket_id']) && $res['ticket_id'] ? 'ticket' : 'project',
'attached_to_id' => $res['ticket_id'] ?? $res['projectid'] ?? null,
'file_name' => $res['file_name'] ?? null,
'file_type' => $res['description'] ?? null,
's3_bucket' => $crm_result['s3_bucket'] ?? null,
's3_key' => $crm_result['s3_key'] ?? null,
'file_size' => $crm_result['file_size'] ?? null,
'message' => $crm_result['message'] ?? null
];
log_message(" ✅ [{$idx}] {$res['file_name']}{$crm_result['document_id']}");
} else {
$error_msg = $res['crm_result']['message'] ?? 'Unknown error';
$errors[] = [
'file_name' => $res['file_name'] ?? 'Unknown',
'error' => $error_msg
];
log_message(" ❌ [{$idx}] {$res['file_name']}: {$error_msg}");
}
}
log_message('✅ Success: ' . count($processed_results) . ' documents attached');
json_response([
'success' => true,
'total_processed' => count($results_array),
'successful' => count($processed_results),
'failed' => count($errors),
'results' => $processed_results,
'errors' => !empty($errors) ? $errors : null
]);
} else {
log_message('❌ Upload failed: ' . ($result['error']['message'] ?? 'Unknown error'));
json_response([
'success' => false,
'error' => $result['error']['message'] ?? 'Upload failed'
], 500);
}

518
browserless_login_esia.js Normal file
View File

@@ -0,0 +1,518 @@
// Авторизация на ej.sudrf.ru через ЕСИА (Госуслуги)
// n8n → HTTP Request → Browserless (Puppeteer)
//
// Вход: $credentials.login, $credentials.password
// Выход: { status, cookies, screenshot, url, session_data }
export default async function ({ page, context: browserContext }, input = {}) {
// Варианты передачи логина/пароля:
// 1) Предпочтительно: отдельными полями body запроса Browserless:
// { code, login, pass } или { code, context: { login, pass } }
// 2) Если тянете из предыдущей ноды прямо в поле code — используйте JSON.stringify, чтобы не ломать JS:
// const login = {{ JSON.stringify($json.login) }};
// const pass = {{ JSON.stringify($json.pass) }};
// Эти строки можно включить в n8n (expression), если вы не передаёте login/pass отдельными полями:
// eslint-disable-next-line no-unused-vars
const __FALLBACK_LOGIN__ = {{ JSON.stringify($json.login ?? "") }};
// eslint-disable-next-line no-unused-vars
const __FALLBACK_PASS__ = {{ JSON.stringify($json.pass ?? "") }};
const fallbackLogin = (typeof __FALLBACK_LOGIN__ !== 'undefined') ? __FALLBACK_LOGIN__ : '';
const fallbackPass = (typeof __FALLBACK_PASS__ !== 'undefined') ? __FALLBACK_PASS__ : '';
const login = String(input.login ?? input.context?.login ?? fallbackLogin ?? '').trim();
const password = String(input.pass ?? input.password ?? input.context?.pass ?? input.context?.password ?? fallbackPass ?? '').trim();
if (!login || !password) {
throw new Error('Не переданы login/pass во входных данных Browserless');
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const timeout = 45000;
const loadDelay = 800;
await page.setViewport({ width: 1280, height: 800 });
page.setDefaultTimeout(timeout);
const makeError = async (error_type, error_message, extra = {}) => {
const bodyText = await page.evaluate(() => document.body?.innerText || '').catch(() => '');
return {
status: 'error',
error_type,
error_message,
current_url: page.url(),
page_text: bodyText.slice(0, 1500),
screenshot: await page.screenshot({ encoding: 'base64', fullPage: true }),
...extra,
};
};
const isEsiaUrl = (u) =>
(u || '').includes('esia.gosuslugi.ru') || (u || '').includes('gosuslugi.ru');
const waitEsiaOrNav = async (ms = 30000) => {
await Promise.race([
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: ms }).catch(() => {}),
page
.waitForFunction(
() =>
location.href.includes('gosuslugi') ||
location.href.includes('esia.gosuslugi'),
{ timeout: ms }
)
.catch(() => {}),
]);
};
const clickByText = async (patterns) => {
return page
.evaluate((patterns) => {
const norm = (s) => (s || '').replace(/\s+/g, ' ').trim().toLowerCase();
const els = Array.from(
document.querySelectorAll(
'a, button, [role="button"], input[type="button"], input[type="submit"]'
)
);
const hit = els.find((el) => {
const t = norm(el.textContent || el.value || '');
const href = (el.getAttribute?.('href') || '').toLowerCase();
return patterns.some((p) => t.includes(p) || href.includes(p));
});
if (hit) {
hit.scrollIntoView({ block: 'center' });
hit.click();
return true;
}
return false;
}, patterns.map((p) => p.toLowerCase()))
.catch(() => false);
};
const clickBySelector = async (selector) => {
const el = await page.$(selector).catch(() => null);
if (!el) return false;
await el.scrollIntoViewIfNeeded?.().catch(() => {});
try {
await el.click({ delay: 30 });
return true;
} catch (_) {}
try {
const box = await el.boundingBox();
if (box) {
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2, { delay: 30 });
return true;
}
} catch (_) {}
return false;
};
// Принять пользовательское соглашение + нажать "Войти" на ej.sudrf.ru
const acceptAgreementAndLogin = async () => {
// На живой странице выяснили:
// - чекбокс имеет id="iAgree"
// - кнопка "Войти" имеет классы: btn btn-primary esia-login esiaLogin и type="submit"
// - кнопка реально disabled до отметки чекбокса
await page
.waitForSelector('#iAgree', { visible: true, timeout: 20000 })
.catch(() => {});
// 1) Отмечаем чекбокс реальным кликом (именно он включает кнопку)
const checkboxClicked = await page
.evaluate(() => {
const cb = document.querySelector('#iAgree');
if (!cb) return false;
cb.scrollIntoView({ block: 'center' });
const label =
cb.closest('label') ||
(cb.id ? document.querySelector(`label[for="${cb.id}"]`) : null);
if (label) label.click();
else cb.click();
cb.dispatchEvent(new Event('input', { bubbles: true }));
cb.dispatchEvent(new Event('change', { bubbles: true }));
return true;
})
.catch(() => false);
if (!checkboxClicked) {
return { checkboxChecked: false, loginClicked: false };
}
// 2) Ждём, что чекбокс действительно стал checked
const checkboxChecked = await page
.waitForFunction(() => !!document.querySelector('#iAgree')?.checked, { timeout: 10000 })
.then(() => true)
.catch(() => false);
if (!checkboxChecked) {
return { checkboxChecked: false, loginClicked: false };
}
// 3) Ждём, что кнопка "Войти" стала enabled (disabled снят)
const loginBtnSelector = 'button.esiaLogin, button.esia-login, button.btn.esiaLogin, button.btn.esia-login';
const loginBtnReady = await page
.waitForFunction(
(sel) => {
const btn = document.querySelector(sel);
if (!btn) return false;
// disabled должен быть снят
// @ts-ignore
if (btn.disabled === true) return false;
const rect = btn.getBoundingClientRect();
return rect.width > 0 && rect.height > 0;
},
{ timeout: 25000 },
loginBtnSelector
)
.then(() => true)
.catch(() => false);
if (!loginBtnReady) {
return { checkboxChecked: true, loginClicked: false, reason: 'login_button_still_disabled' };
}
await sleep(500);
// 4) Кликаем "Войти" и ждём перехода на ЕСИА
const beforeUrl = page.url();
await Promise.all([
waitEsiaOrNav(25000),
page
.click(loginBtnSelector, { delay: 30 })
.catch(async () => {
// фоллбек на DOM-click
await page
.evaluate((sel) => {
const btn = document.querySelector(sel);
if (btn) {
// @ts-ignore
btn.disabled = false;
btn.removeAttribute?.('disabled');
// @ts-ignore
btn.click();
}
}, loginBtnSelector)
.catch(() => {});
}),
]);
const afterUrl = page.url();
const loginClicked = isEsiaUrl(afterUrl) || afterUrl !== beforeUrl;
return { checkboxChecked: true, loginClicked, url: afterUrl };
};
// ——— 1) Открываем ej.sudrf.ru ———
await page.goto('https://ej.sudrf.ru/?fromOa=16RS0018', {
waitUntil: 'domcontentloaded',
timeout,
});
await sleep(loadDelay);
// Иногда редиректит сразу (редко), но обычно — нет
let currentUrl = page.url();
if (!isEsiaUrl(currentUrl)) {
// Подождать дорисовку страницы
await page.waitForSelector('body', { timeout: 20000 }).catch(() => {});
await sleep(400);
// ——— 2) Проверяем, на какой странице мы находимся ———
const pageType = await page
.evaluate(() => {
const t = (document.body?.innerText || '').toLowerCase();
if (t.includes('авторизация пользователя') && t.includes('есиа')) {
return 'auth_page';
}
if (t.includes('обращения') || t.includes('дела')) {
return 'main_page';
}
return 'unknown';
})
.catch(() => 'unknown');
if (pageType === 'main_page') {
// ——— 2a) На главной странице — ищем кнопку "Вход" ———
let loginLinkClicked = await clickByText(['вход']);
if (!loginLinkClicked) {
// Пытаемся найти по селекторам
loginLinkClicked = await page
.evaluate(() => {
const links = Array.from(document.querySelectorAll('a'));
const loginLink = links.find((el) => {
const text = (el.textContent || '').toLowerCase().trim();
return text === 'вход';
});
if (loginLink) {
loginLink.scrollIntoView({ block: 'center' });
loginLink.click();
return true;
}
return false;
})
.catch(() => false);
}
if (!loginLinkClicked) {
return await makeError('main_page_login_not_found', 'Не удалось найти кнопку "Вход" на главной странице');
}
// Ждём загрузки страницы авторизации с дополнительными проверками
await Promise.race([
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 15000 }),
page.waitForSelector('input[type="checkbox"]', { timeout: 15000 }),
page.waitForFunction(() => document.body?.innerText?.toLowerCase().includes('авторизация пользователя'), { timeout: 15000 })
]).catch(() => {});
await sleep(loadDelay);
}
// ——— 2b) Теперь должны быть на странице авторизации — принимаем соглашение и жмём Войти ———
const pageLooksLikeAuth = await page
.evaluate(() => {
const t = (document.body?.innerText || '').toLowerCase();
return t.includes('авторизация пользователя') && t.includes('есиа');
})
.catch(() => false);
if (pageLooksLikeAuth) {
const { checkboxState, loginClicked } = await acceptAgreementAndLogin();
// Если кнопка не нажалась (например, disabled) — ещё раз попробуем клик по "Войти" через общий поиск
if (!loginClicked) {
// Иногда кнопка активируется с задержкой после клика по чекбоксу
await sleep(600);
const fallbackLoginClick = await clickByText(['войти']);
if (!fallbackLoginClick) {
return await makeError(
'esia_login_button_not_clicked',
'Не удалось нажать "Войти" после принятия соглашения',
{ checkboxState }
);
}
}
// Ждём редирект на ЕСИА
await waitEsiaOrNav(30000);
await sleep(loadDelay);
} else {
return await makeError('auth_page_not_found', 'Не удалось попасть на страницу авторизации');
}
}
// ——— 4) Проверяем, что мы на ЕСИА ———
currentUrl = page.url();
if (!isEsiaUrl(currentUrl)) {
return await makeError('esia_redirect_failed', 'Не произошел редирект на ЕСИА (после Войти)', {
after_actions_url: currentUrl,
});
}
// ——— 5) Ввод логина на ЕСИА ———
await page
.waitForSelector('input[type="text"], input[name="login"], input[name="username"]', {
timeout: 20000,
})
.catch(() => {});
// ЕСИА часто на React/контролируемых инпутах — простая установка el.value может не сработать.
// Поэтому делаем "живой" ввод через клавиатуру + проверяем, что значение реально попало в input.value.
const normalizePhone = (v) => String(v || '').trim().replace(/^\+/, '');
const loginToType = normalizePhone(login);
const fillInput = async (selectors, value, debugKey) => {
// 1) Выбираем ВИДИМЫЙ инпут (у ESIA часто есть скрытые дубли)
for (const sel of selectors) {
const handles = await page.$$(sel).catch(() => []);
for (const handle of handles) {
try {
const box = await handle.boundingBox();
if (!box || box.width < 5 || box.height < 5) continue;
// запомним, какой селектор реально сработал (для отладки)
if (debugKey) {
debug[debugKey] = { selector: sel, box };
}
await handle.focus();
// очистка
await page.keyboard.down('Control');
await page.keyboard.press('KeyA');
await page.keyboard.up('Control');
await page.keyboard.press('Backspace');
// ввод именно через elementHandle.type (иногда надежнее чем page.keyboard.type)
await handle.type(String(value), { delay: 60 });
// blur + change (нужно ESIA, иначе пишет "Заполните поле")
await handle.evaluate((el) => {
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.blur();
}).catch(() => {});
await sleep(250);
const ok = await handle
.evaluate((el) => typeof el.value === 'string' && el.value.length > 0)
.catch(() => false);
if (ok) return true;
} catch (_) {}
}
}
// 2) Фоллбек: нативный setter + input/change (для React)
const ok = await page
.evaluate((sels, val) => {
const isVisible = (el) => {
const r = el.getBoundingClientRect();
return r.width > 5 && r.height > 5;
};
const pick = () => {
for (const s of sels) {
const list = Array.from(document.querySelectorAll(s));
const visible = list.find(isVisible);
if (visible) return visible;
}
return null;
};
const el = pick();
if (!el) return false;
el.focus();
const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set;
if (setter) {
setter.call(el, '');
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
setter.call(el, String(val));
} else {
// @ts-ignore
el.value = String(val);
}
el.dispatchEvent(new Event('input', { bubbles: true }));
el.dispatchEvent(new Event('change', { bubbles: true }));
el.blur();
// @ts-ignore
return typeof el.value === 'string' && el.value.length > 0;
}, selectors, value)
.catch(() => false);
return ok;
};
const debug = {};
const loginFilled = await fillInput(
[
'input[name="login"]',
'input[name="username"]',
'input[type="tel"]',
'input[type="text"]',
],
loginToType,
'loginInput'
);
if (!loginFilled) {
return await makeError('login_input_not_found', 'Не найдено поле логина на странице ЕСИА');
}
await sleep(300);
// ——— 6) Ввод пароля ———
await page.waitForSelector('input[type="password"]', { timeout: 20000 }).catch(() => {});
const passFilled = await fillInput(['input[type="password"]'], password, 'passwordInput');
if (!passFilled) {
return await makeError('password_input_not_found', 'Не найдено поле пароля на странице ЕСИА');
}
await sleep(300);
// ——— 7) Submit ———
// маленькая пауза перед сабмитом, чтобы ESIA "съела" input/change
await sleep(600);
const submitted = await page
.evaluate(() => {
const norm = (s) => (s || '').replace(/\s+/g, ' ').trim().toLowerCase();
const btn =
document.querySelector('button[type="submit"]') ||
Array.from(document.querySelectorAll('button')).find((b) =>
norm(b.textContent).includes('войти')
) ||
Array.from(document.querySelectorAll('input[type="submit"]')).find(Boolean);
if (btn) {
btn.scrollIntoView({ block: 'center' });
btn.click();
return true;
}
return false;
})
.catch(() => false);
if (!submitted) {
// fallback Enter
await page.keyboard.press('Enter').catch(() => {});
}
await sleep(500);
// ——— 8) Ждём SMS-поля (или навигацию) ———
const otpSelector =
'input[inputmode="numeric"], input[type="tel"], input[autocomplete="one-time-code"], input[name="otp"], input[name*="code"], input[id*="otp"], input[id*="code"]';
await Promise.race([
page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 30000 }).catch(() => {}),
page
.waitForFunction(
(sel) => document.querySelectorAll(sel).length > 0,
{ timeout: 30000 },
otpSelector
)
.catch(() => {}),
]);
await sleep(loadDelay);
const smsInputs = await page.$$(otpSelector).catch(() => []);
if (!smsInputs || smsInputs.length === 0) {
// Если мы всё ещё на /login/, скорее всего форма не приняла пароль/логин или показала валидацию
const urlNow = page.url();
if (urlNow.includes('esia.gosuslugi.ru/login')) {
return await makeError(
'login_failed',
'После нажатия «Войти» ЕСИА не перешла к SMS. Скорее всего, форма считает логин/пароль пустыми или произошла ошибка входа.',
{
debug,
}
);
}
return await makeError(
'sms_page_not_found',
'Не найдены поля для ввода SMS кода (возможно, иной фактор подтверждения или ошибка входа)'
);
}
// ——— 9) Сохраняем куки + скрин ———
// В browserless /function это Puppeteer: куки берём с page
const cookies = (typeof page.cookies === 'function') ? await page.cookies() : [];
const screenshot = await page.screenshot({ encoding: 'base64', fullPage: true });
return {
status: 'waiting_for_sms',
message: '✅ Дошли до ввода SMS. Ожидание кода.',
url: page.url(),
cookies,
screenshot,
sms_inputs_count: smsInputs.length,
session_data: {
created_at: new Date().toISOString(),
note: 'cookies передай во второй скрипт, чтобы продолжить сессию и ввести SMS',
},
};
}

View File

@@ -64,6 +64,53 @@ try {
error_log("Callback: Updated task {$taskId}, affected rows: {$affected}");
// Публикуем событие в Redis для мгновенной доставки через SSE
try {
if (class_exists('Redis')) {
$redis = new Redis();
if ($redis->connect('crm.clientright.ru', 6379)) {
$redis->auth('CRM_Redis_Pass_2025_Secure!');
$channel = "ai:response:{$taskId}";
$event = json_encode([
'task_id' => $taskId,
'status' => $status,
'response' => $response,
'error' => $error,
'timestamp' => date('Y-m-d H:i:s')
], JSON_UNESCAPED_UNICODE);
$redis->publish($channel, $event);
error_log("Callback: Published to Redis channel {$channel}");
$redis->close();
}
} else {
// Используем Predis если расширение Redis недоступно
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'crm.clientright.ru',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!',
]);
$channel = "ai:response:{$taskId}";
$event = json_encode([
'task_id' => $taskId,
'status' => $status,
'response' => $response,
'error' => $error,
'timestamp' => date('Y-m-d H:i:s')
], JSON_UNESCAPED_UNICODE);
$redis->publish($channel, $event);
error_log("Callback: Published to Redis channel {$channel} via Predis");
}
} catch (Exception $redisError) {
error_log("Callback: Redis publish error (non-critical): " . $redisError->getMessage());
// Не прерываем выполнение, если Redis недоступен - БД уже обновлена
}
echo json_encode([
'success' => true,
'message' => 'Response received',

View File

@@ -0,0 +1,51 @@
<?php
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$projectId = 371231;
$sql = "SELECT
n.notesid,
n.title,
n.filelocationtype,
n.filename,
n.s3_bucket,
n.s3_key
FROM vtiger_notes n
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
WHERE snr.crmid = ? AND e.deleted = 0
ORDER BY n.notesid DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$projectId]);
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "Проверка поля filename для документов проекта $projectId\n";
echo str_repeat("=", 80) . "\n\n";
foreach ($documents as $doc) {
echo "ID: {$doc['notesid']}\n";
echo " Название: {$doc['title']}\n";
echo " filelocationtype: {$doc['filelocationtype']}\n";
echo " filename (первые 200 символов): " . substr($doc['filename'], 0, 200) . "\n";
echo " s3_bucket: " . ($doc['s3_bucket'] ?? 'нет') . "\n";
echo " s3_key: " . substr($doc['s3_key'] ?? 'нет', 0, 100) . "\n";
// Проверяем, является ли filename URL
$isUrl = filter_var($doc['filename'], FILTER_VALIDATE_URL);
echo " filename является URL: " . ($isUrl ? 'ДА' : 'НЕТ') . "\n";
// Проверяем, начинается ли filename с http
$isHttp = (strpos($doc['filename'], 'http://') === 0 || strpos($doc['filename'], 'https://') === 0);
echo " filename начинается с http: " . ($isHttp ? 'ДА' : 'НЕТ') . "\n";
echo "\n";
}

149
check_project_371231.php Normal file
View File

@@ -0,0 +1,149 @@
<?php
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$projectId = 371231;
// Получаем информацию о проекте
$sqlProject = "SELECT projectid, projectname, projectstatus FROM vtiger_project WHERE projectid = ?";
$stmtProject = $pdo->prepare($sqlProject);
$stmtProject->execute([$projectId]);
$project = $stmtProject->fetch(PDO::FETCH_ASSOC);
if (!$project) {
die("❌ Проект $projectId не найден!\n");
}
echo "📋 ПРОЕКТ: {$project['projectname']}\n";
echo " ID: {$project['projectid']}\n";
echo " Статус: {$project['projectstatus']}\n";
echo "\n" . str_repeat("=", 80) . "\n\n";
// Получаем документы проекта
$sql = "SELECT
n.notesid,
n.title,
n.filename,
n.filelocationtype,
n.foldername,
n.s3_bucket,
n.s3_key,
n.nc_path,
n.filesize,
e.createdtime,
e.modifiedtime,
u.user_name,
e.deleted
FROM vtiger_notes n
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
LEFT JOIN vtiger_users u ON u.id = e.smownerid
WHERE snr.crmid = ? AND e.deleted = 0
ORDER BY e.createdtime DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$projectId]);
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = count($documents);
echo "📄 НАЙДЕНО ДОКУМЕНТОВ: $count\n\n";
if ($count == 0) {
echo "⚠️ Документы не найдены!\n";
exit;
}
$totalSize = 0;
$s3Count = 0;
$localCount = 0;
$brokenCount = 0;
$accessibleCount = 0;
$notAccessibleCount = 0;
foreach ($documents as $i => $doc) {
$num = $i + 1;
$filelocationtype = $doc['filelocationtype'] ?? 'I';
$s3Bucket = $doc['s3_bucket'] ?? null;
$s3Key = $doc['s3_key'] ?? null;
$filename = $doc['filename'] ?? '';
$title = $doc['title'] ?? 'Без названия';
$isS3 = ($filelocationtype == 'E' && !empty($s3Bucket) && !empty($s3Key));
$isLocal = ($filelocationtype == 'I' || empty($filelocationtype));
if ($isS3) {
$s3Count++;
$status = "☁️ S3";
$filePath = "s3://{$s3Bucket}/{$s3Key}";
$accessibleCount++; // Пока считаем доступными, проверим отдельно
} else {
$localCount++;
$status = "💾 Локальный";
// Для локальных файлов проверяем путь
if (!empty($filename)) {
// Парсим путь из filename
$filePath = $filename;
if (file_exists($filePath)) {
$accessibleCount++;
$status .= "";
} else {
$notAccessibleCount++;
$brokenCount++;
$status .= " ❌ ФАЙЛ НЕ НАЙДЕН";
}
} else {
$notAccessibleCount++;
$brokenCount++;
$status .= "НЕТ ПУТИ";
}
}
$size = $doc['filesize'] ?? 0;
$totalSize += $size;
$sizeStr = $size > 0 ? number_format($size / 1024, 2) . ' KB' : '0 KB';
echo sprintf(
"%3d. [%s] %s\n",
$num,
$status,
$title
);
echo sprintf(
" ID: %d | Размер: %s | Тип: %s\n",
$doc['notesid'],
$sizeStr,
$filelocationtype
);
if ($isS3) {
echo sprintf(" S3 Key: %s\n", $s3Key);
} else {
echo sprintf(" Путь: %s\n", substr($filename, 0, 100));
}
if ($notAccessibleCount > 0 && ($i == $count - 1 || ($i + 1) % 10 == 0)) {
echo "\n";
}
}
echo "\n" . str_repeat("=", 80) . "\n";
echo "📊 СТАТИСТИКА:\n";
echo " Всего документов: $count\n";
echo " S3 документов: $s3Count\n";
echo " Локальных документов: $localCount\n";
echo " Доступных: $accessibleCount\n";
echo " Недоступных: $notAccessibleCount\n";
echo " Общий размер: " . number_format($totalSize / 1024 / 1024, 2) . " MB\n";
if ($brokenCount > 0) {
echo "\n⚠️ ВНИМАНИЕ: Найдено $brokenCount недоступных файлов!\n";
}

View File

@@ -0,0 +1,51 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$projectId = 371231;
// Получаем документы проекта
$sql = "SELECT
n.notesid,
n.title,
n.filename,
n.filelocationtype,
n.s3_bucket,
n.s3_key,
n.filesize
FROM vtiger_notes n
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
WHERE snr.crmid = ? AND e.deleted = 0
ORDER BY e.createdtime DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$projectId]);
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "Найдено документов: " . count($documents) . "\n\n";
foreach ($documents as $i => $doc) {
$num = $i + 1;
echo "$num. ID: {$doc['notesid']}\n";
echo " Название: " . ($doc['title'] ?? 'Нет') . "\n";
echo " Тип хранения: " . ($doc['filelocationtype'] ?? 'I') . "\n";
if ($doc['filelocationtype'] == 'E') {
echo " S3 Bucket: " . ($doc['s3_bucket'] ?? 'нет') . "\n";
echo " S3 Key: " . ($doc['s3_key'] ?? 'нет') . "\n";
} else {
echo " Filename: " . substr($doc['filename'] ?? 'нет', 0, 100) . "\n";
}
echo "\n";
}

129
check_project_373977.php Normal file
View File

@@ -0,0 +1,129 @@
<?php
/**
* Проверка документов проекта 373977
*/
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
$projectId = 373977;
$s3Bucket = $config['s3']['bucket'];
// Документы проекта из БД
$documents = [
373981 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/8_Договора_оказание_услуг_373981.pdf',
373983 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/9_Подтверждение_оплаты_пооговору_373983.pdf',
373985 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/10_2_Скрин_личногоабинетастца_и_программа_обуч_373985.pdf',
373987 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/10_1_Скрин_личногоабинетастца_и_программа_обуч_373987.pdf',
373989 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11_1_Подтверждение_проведения_претензионной_работы_373989.pdf',
373991 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/7_заявление_потребителя_373991.pdf',
374017 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11_Доказательство_соблюдения_претензионного_порядк_374017.pdf',
375402 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.2_Претензия_в_защиту_интересов_Полулях_Ольга_1_375402.pdf',
375404 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.3_Доказательство_оплаты_направления_претензии_о_375404.pdf',
375406 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/11.4_Доказательствоаправления_претензии_ответчик_375406.pdf',
376051 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/0_Исковоеаявление_поелуолулях_7_стр_376051.pdf',
376054 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/6_Расчет_исковыхребований_Полулях_1_стр_376054.pdf',
376080 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/12.1_Доказательство_оплаты_направления_иска_ответч_376080.pdf',
376082 => 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/12.2_Доказательствоаправления_иска_ответчику_376082.pdf',
396623 => 'crm2/CRM_Active_Files/Documents/396623/ПК_451a1058-ee34-0d48-b2f4-d6dfa522928a.pdf_WITH_ENVELOPE.pdf', // Неправильное место!
];
echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n";
echo str_repeat("=", 80) . "\n\n";
try {
$s3Client = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => $config['s3']['region'],
'endpoint' => $config['s3']['endpoint'],
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $config['s3']['key'],
'secret' => $config['s3']['secret'],
],
'suppress_php_deprecation_warning' => true
]);
$stats = [
'total' => count($documents),
'exists' => 0,
'missing' => 0,
'wrong_place' => 0,
'missing_files' => [],
'wrong_place_files' => [],
];
$projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/ПолуляхУ_ДПООРОДСКАЯ_АКАДЕМИЯ_УРБАН_373977/';
foreach ($documents as $docId => $s3Key) {
$filename = basename($s3Key);
$isInProjectFolder = strpos($s3Key, $projectPrefix) === 0;
echo "Документ ID: {$docId}\n";
echo " Файл: {$filename}\n";
echo " Путь: {$s3Key}\n";
if ($s3Client->doesObjectExist($s3Bucket, $s3Key)) {
$object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $s3Key]);
$size = round($object['ContentLength'] / 1024, 2);
if (!$isInProjectFolder) {
echo " ⚠️ Файл существует, но в неправильном месте (размер: {$size} KB)\n";
$stats['wrong_place']++;
$stats['wrong_place_files'][] = [
'doc_id' => $docId,
'current_path' => $s3Key,
'should_be' => $projectPrefix . $filename,
];
} else {
echo " ✅ Файл существует (размер: {$size} KB)\n";
$stats['exists']++;
}
} else {
echo " ❌ Файл отсутствует\n";
$stats['missing']++;
$stats['missing_files'][] = [
'doc_id' => $docId,
'path' => $s3Key,
];
}
echo "\n";
}
// Итоги
echo str_repeat("=", 80) . "\n";
echo "ИТОГИ:\n";
echo "Всего документов: {$stats['total']}\n";
echo "На месте: {$stats['exists']}\n";
echo "⚠️ В неправильном месте: {$stats['wrong_place']}\n";
echo "❌ Отсутствуют: {$stats['missing']}\n\n";
if (!empty($stats['wrong_place_files'])) {
echo "ФАЙЛЫ В НЕПРАВИЛЬНОМ МЕСТЕ:\n";
foreach ($stats['wrong_place_files'] as $file) {
echo " - Документ {$file['doc_id']}: {$file['current_path']}\n";
echo " Должен быть: {$file['should_be']}\n";
}
echo "\n";
}
if (!empty($stats['missing_files'])) {
echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n";
foreach ($stats['missing_files'] as $file) {
echo " - Документ {$file['doc_id']}: {$file['path']}\n";
}
echo "\n";
}
echo "=== ГОТОВО ===\n";
} catch (Exception $e) {
echo "❌ Ошибка: " . $e->getMessage() . "\n";
exit(1);
}

103
check_project_391584.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
/**
* Проверка документов проекта 391584
*/
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
$projectId = 391584;
$s3Bucket = $config['s3']['bucket'];
$projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/ЧужбаОУ_ДПО_ОБРАЗОВАТЕЛЬНЫЕ_ТЕХНОЛОГИИ_СКИЛБОКС_(КОРОБКА_НАВЫКОВ)_391584/';
// Документы проекта из БД
$documents = [
391587 => '8_Договора_оказание_услуг_391587.pdf',
391589 => '9_Подтверждение_оплаты_пооговору_391589.pdf',
391591 => '10_1_Скрин_личногоабинетастца_и_программа_обуч_391591.pdf',
391593 => '7_заявление_потребителя_391593.pdf',
392332 => '11_Доказательство_соблюдения_претензионного_порядк_392332.pdf',
392472 => '11.1_Доказательство_соблюдения_претензионного_поря_392472.pdf',
392475 => '11.2_Доказательство_соблюдения_претензионного_поря_392475.pdf',
395136 => '6_Расчет_искаужба_395136.pdf',
395157 => '0_Исковоеаявление_поелуужбаОУ_ДПО_ОБРАЗОВА_395157.pdf',
395744 => '12.1_Доказательство_оплаты_направления_иска_ответч_395744.pdf',
];
echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n";
echo str_repeat("=", 80) . "\n\n";
try {
$s3Client = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => $config['s3']['region'],
'endpoint' => $config['s3']['endpoint'],
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $config['s3']['key'],
'secret' => $config['s3']['secret'],
],
'suppress_php_deprecation_warning' => true
]);
$stats = [
'total' => count($documents),
'exists' => 0,
'missing' => 0,
'missing_files' => [],
];
foreach ($documents as $docId => $filename) {
$s3Key = $projectPrefix . $filename;
echo "Документ ID: {$docId}\n";
echo " Файл: {$filename}\n";
echo " Путь: {$s3Key}\n";
$exists = $s3Client->doesObjectExist($s3Bucket, $s3Key);
if ($exists) {
$object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $s3Key]);
$size = round($object['ContentLength'] / 1024, 2);
echo " ✅ Файл существует (размер: {$size} KB)\n";
$stats['exists']++;
} else {
echo " ❌ Файл отсутствует\n";
$stats['missing']++;
$stats['missing_files'][] = [
'doc_id' => $docId,
'filename' => $filename,
'path' => $s3Key,
];
}
echo "\n";
}
// Итоги
echo str_repeat("=", 80) . "\n";
echo "ИТОГИ:\n";
echo "Всего документов: {$stats['total']}\n";
echo "✅ Существуют: {$stats['exists']}\n";
echo "❌ Отсутствуют: {$stats['missing']}\n\n";
if (!empty($stats['missing_files'])) {
echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n";
foreach ($stats['missing_files'] as $file) {
echo " - Документ {$file['doc_id']}: {$file['filename']}\n";
echo " Путь: {$file['path']}\n";
}
echo "\n";
}
echo "=== ГОТОВО ===\n";
} catch (Exception $e) {
echo "❌ Ошибка: " . $e->getMessage() . "\n";
exit(1);
}

158
check_project_398027.php Normal file
View File

@@ -0,0 +1,158 @@
<?php
/**
* Проверка документов проекта 398027
*/
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
$projectId = 398027;
$s3Bucket = $config['s3']['bucket'];
$projectPrefix = 'crm2/CRM_Active_Files/Documents/Project/Храмов_ООО_НЕТОЛОГИЯ_398027/';
// Документы проекта из БД
$documents = [
398030 => [
'current_path' => 'crm2/CRM_Active_Files/Documents/398030/8_Договора_оказание_услуг_11-14-2025-16-00-51_Храмов_1_CTP#realfile.pdf',
'should_be' => $projectPrefix . '8_Договора_оказание_услуг_398030.pdf',
],
398032 => [
'current_path' => 'crm2/CRM_Active_Files/Documents/398032/9_Подтверждение_оплаты_пооговору_11-14-2025-16-00-03_Храмов_1_CTP#realfile.pdf',
'should_be' => $projectPrefix . '9_Подтверждение_оплаты_пооговору_398032.pdf',
],
398034 => [
'current_path' => 'crm2/CRM_Active_Files/Documents/398034/10_2_Скрин_личногоабинетастца_и_программа_обучения_11-14-2025-15-47-26_Храмов_41_CTP#realfile.pdf',
'should_be' => $projectPrefix . '10_2_Скрин_личногоабинетастца_и_программа_обучения_398034.pdf',
],
398036 => [
'current_path' => 'crm2/CRM_Active_Files/Documents/398036/10_1_Скрин_личногоабинетастца_и_программа_обучения_11-14-2025-15-49-59_Храмов_1_CTP#realfile.pdf',
'should_be' => $projectPrefix . '10_1_Скрин_личногоабинетастца_и_программа_обучения_398036.pdf',
],
398038 => [
'current_path' => 'crm2/CRM_Active_Files/Documents/398038/Прочиеокументы_11-14-2025-16-06-07_Храмов_3_CTP#realfile.pdf',
'should_be' => $projectPrefix . рочиеокументы_398038.pdf',
],
398040 => [
'current_path' => 'crm2/CRM_Active_Files/Documents/398040/7_zayavlenie_potrebitelya_Hramov.pdf',
'should_be' => $projectPrefix . '7_заявление_потребителя_398040.pdf',
],
398063 => [
'current_path' => 'crm2/CRM_Active_Files/Documents/398063/napravleniya_pretenzii.pdf',
'should_be' => $projectPrefix . 'Направление_претензии_398063.pdf',
],
398584 => [
'current_path' => 'crm2/CRM_Active_Files/Documents/Project/Храмов_ООО_НЕТОЛОГИЯ_398027/8_Договора_оказание_услуг_398584.pdf',
'should_be' => $projectPrefix . '8_Договора_оказание_услуг_398584.pdf',
],
399067 => [
'current_path' => 'clientright/0/1763997676315.pdf',
'should_be' => $projectPrefix . окумент_399067.pdf',
],
399068 => [
'current_path' => 'clientright/0/1763997790309.pdf',
'should_be' => $projectPrefix . окумент_399068.pdf',
],
];
echo "=== ПРОВЕРКА ДОКУМЕНТОВ ПРОЕКТА {$projectId} ===\n";
echo str_repeat("=", 80) . "\n\n";
try {
$s3Client = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => $config['s3']['region'],
'endpoint' => $config['s3']['endpoint'],
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $config['s3']['key'],
'secret' => $config['s3']['secret'],
],
'suppress_php_deprecation_warning' => true
]);
$stats = [
'total' => count($documents),
'exists_correct' => 0,
'exists_wrong' => 0,
'missing' => 0,
'wrong_place_files' => [],
'missing_files' => [],
];
foreach ($documents as $docId => $paths) {
$currentPath = $paths['current_path'];
$shouldBe = $paths['should_be'];
$filename = basename($shouldBe);
echo "Документ ID: {$docId}\n";
echo " Файл: {$filename}\n";
echo " Текущий путь: {$currentPath}\n";
echo " Должен быть: {$shouldBe}\n";
$existsCurrent = $s3Client->doesObjectExist($s3Bucket, $currentPath);
$existsCorrect = $s3Client->doesObjectExist($s3Bucket, $shouldBe);
if ($existsCorrect) {
$object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $shouldBe]);
$size = round($object['ContentLength'] / 1024, 2);
echo " ✅ Файл уже в правильном месте (размер: {$size} KB)\n";
$stats['exists_correct']++;
} elseif ($existsCurrent) {
$object = $s3Client->headObject(['Bucket' => $s3Bucket, 'Key' => $currentPath]);
$size = round($object['ContentLength'] / 1024, 2);
echo " ⚠️ Файл существует, но в неправильном месте (размер: {$size} KB)\n";
$stats['exists_wrong']++;
$stats['wrong_place_files'][] = [
'doc_id' => $docId,
'current_path' => $currentPath,
'should_be' => $shouldBe,
];
} else {
echo " ❌ Файл отсутствует\n";
$stats['missing']++;
$stats['missing_files'][] = [
'doc_id' => $docId,
'path' => $currentPath,
];
}
echo "\n";
}
// Итоги
echo str_repeat("=", 80) . "\n";
echo "ИТОГИ:\n";
echo "Всего документов: {$stats['total']}\n";
echo "На месте: {$stats['exists_correct']}\n";
echo "⚠️ В неправильном месте: {$stats['exists_wrong']}\n";
echo "❌ Отсутствуют: {$stats['missing']}\n\n";
if (!empty($stats['wrong_place_files'])) {
echo "ФАЙЛЫ В НЕПРАВИЛЬНОМ МЕСТЕ:\n";
foreach ($stats['wrong_place_files'] as $file) {
echo " - Документ {$file['doc_id']}\n";
echo " От: {$file['current_path']}\n";
echo " К: {$file['should_be']}\n";
}
echo "\n";
}
if (!empty($stats['missing_files'])) {
echo "ОТСУТСТВУЮЩИЕ ФАЙЛЫ:\n";
foreach ($stats['missing_files'] as $file) {
echo " - Документ {$file['doc_id']}: {$file['path']}\n";
}
echo "\n";
}
echo "=== ГОТОВО ===\n";
} catch (Exception $e) {
echo "❌ Ошибка: " . $e->getMessage() . "\n";
exit(1);
}

View File

@@ -0,0 +1,172 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$config = require '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
$projectId = 384256;
$s3Bucket = $config['s3']['bucket'];
echo "Проверка доступности всех файлов проекта $projectId\n";
echo str_repeat("=", 80) . "\n\n";
try {
// Инициализация S3 клиента
$s3Client = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => $config['s3']['region'],
'endpoint' => $config['s3']['endpoint'],
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $config['s3']['key'],
'secret' => $config['s3']['secret'],
],
'suppress_php_deprecation_warning' => true
]);
// Подключение к БД
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
// Получаем все документы проекта
$stmt = $pdo->prepare('
SELECT n.notesid, n.title, n.s3_key, n.filename, n.filelocationtype
FROM vtiger_notes n
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
WHERE snr.crmid = ? AND e.deleted = 0
ORDER BY n.notesid ASC
');
$stmt->execute([$projectId]);
$docs = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "Всего документов в проекте: " . count($docs) . "\n\n";
$accessible = [];
$notAccessible = [];
foreach ($docs as $doc) {
$docId = $doc['notesid'];
$title = $doc['title'];
$s3Key = $doc['s3_key'];
$filelocationtype = $doc['filelocationtype'];
echo "ID: $docId | $title\n";
if ($filelocationtype == 'E' && !empty($s3Key)) {
// Проверяем доступность в S3
try {
$result = $s3Client->headObject([
'Bucket' => $s3Bucket,
'Key' => $s3Key
]);
$size = number_format($result['ContentLength'] / 1024, 2);
echo " ✅ Доступен в S3 (" . $size . " KB)\n";
echo " Путь: $s3Key\n";
$accessible[] = ['doc' => $doc, 'size' => $result['ContentLength']];
} catch (\Aws\Exception\AwsException $e) {
if ($e->getAwsErrorCode() == 'NotFound') {
echo "НЕ найден в S3\n";
echo " Ожидаемый путь: $s3Key\n";
$notAccessible[] = $doc;
} else {
echo " ⚠️ Ошибка доступа: " . $e->getAwsErrorCode() . "\n";
$notAccessible[] = $doc;
}
}
} else {
echo " ⚠️ Тип хранения: " . ($filelocationtype ?: 'не указан') . "\n";
if (!empty($doc['filename'])) {
echo " Filename: " . substr($doc['filename'], 0, 100) . "\n";
}
}
echo "\n";
}
echo str_repeat("=", 80) . "\n";
echo "СТАТИСТИКА:\n";
echo " Доступных файлов: " . count($accessible) . "\n";
echo " Недоступных файлов: " . count($notAccessible) . "\n\n";
// Поиск недоступных файлов в других местах S3
if (!empty($notAccessible)) {
echo "Поиск недоступных файлов в других местах S3...\n\n";
foreach ($notAccessible as $doc) {
$docId = $doc['notesid'];
$title = $doc['title'];
echo "Поиск файла для документа $docId: $title\n";
// Ищем по ID документа в разных местах
$searchPatterns = [
"temp/$projectId/",
"temp/",
"crm2/CRM_Active_Files/Documents/",
"Documents/",
];
$found = false;
foreach ($searchPatterns as $prefix) {
try {
$objects = $s3Client->listObjectsV2([
'Bucket' => $s3Bucket,
'Prefix' => $prefix,
'MaxKeys' => 1000
]);
if (isset($objects['Contents'])) {
foreach ($objects['Contents'] as $object) {
$key = $object['Key'];
// Ищем файлы, содержащие ID документа или похожие названия
if (strpos($key, (string)$docId) !== false ||
strpos($key, (string)($docId - 1)) !== false ||
strpos($key, (string)($docId + 1)) !== false) {
// Проверяем доступность
try {
$headResult = $s3Client->headObject([
'Bucket' => $s3Bucket,
'Key' => $key
]);
echo " ✅ НАЙДЕН: $key\n";
echo " Размер: " . number_format($headResult['ContentLength'] / 1024, 2) . " KB\n";
echo " Дата: " . ($headResult['LastModified'] ?? 'не указана') . "\n";
// Предлагаем переместить
echo " 💡 Рекомендация: переместить в правильный путь\n";
$found = true;
break 2;
} catch (\Aws\Exception\AwsException $e) {
// Пропускаем
}
}
}
}
} catch (\Aws\Exception\AwsException $e) {
// Пропускаем ошибки
}
}
if (!$found) {
echo " ❌ Файл не найден ни в одном месте S3\n";
}
echo "\n";
}
}
} catch (Exception $e) {
echo "ОШИБКА: " . $e->getMessage() . "\n";
echo "Trace: " . $e->getTraceAsString() . "\n";
}

116
check_s3_access_371231.php Normal file
View File

@@ -0,0 +1,116 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/config.php';
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$projectId = 371231;
// Получаем документы проекта
$sql = "SELECT
n.notesid,
n.title,
n.filename,
n.filelocationtype,
n.s3_bucket,
n.s3_key
FROM vtiger_notes n
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
WHERE snr.crmid = ? AND e.deleted = 0 AND n.filelocationtype = 'E'
ORDER BY n.notesid DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$projectId]);
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo "Проверка доступности S3 файлов для проекта $projectId\n";
echo str_repeat("=", 80) . "\n\n";
// Инициализируем S3 клиент
$s3Config = $config['s3'];
$awsClient = new \Aws\S3\S3Client([
'version' => 'latest',
'region' => $s3Config['region'],
'endpoint' => $s3Config['endpoint'],
'credentials' => [
'key' => $s3Config['key'],
'secret' => $s3Config['secret'],
],
'use_path_style_endpoint' => true,
]);
$accessible = 0;
$notAccessible = 0;
$errors = [];
foreach ($documents as $doc) {
$notesid = $doc['notesid'];
$title = $doc['title'];
$s3Bucket = $doc['s3_bucket'];
$s3Key = $doc['s3_key'];
try {
$exists = $awsClient->doesObjectExist($s3Bucket, $s3Key);
if ($exists) {
$accessible++;
echo "✅ ID: $notesid - $title\n";
} else {
$notAccessible++;
echo "❌ ID: $notesid - $title\n";
echo " S3 Key: $s3Key\n";
$errors[] = [
'id' => $notesid,
'title' => $title,
's3_key' => $s3Key,
'reason' => 'File does not exist in S3'
];
}
} catch (\Aws\Exception\AwsException $e) {
$notAccessible++;
echo "❌ ID: $notesid - $title\n";
echo " Ошибка: " . $e->getMessage() . "\n";
echo " S3 Key: $s3Key\n";
$errors[] = [
'id' => $notesid,
'title' => $title,
's3_key' => $s3Key,
'reason' => 'AWS Exception: ' . $e->getMessage()
];
} catch (Exception $e) {
$notAccessible++;
echo "❌ ID: $notesid - $title\n";
echo " Ошибка: " . $e->getMessage() . "\n";
$errors[] = [
'id' => $notesid,
'title' => $title,
's3_key' => $s3Key,
'reason' => 'Exception: ' . $e->getMessage()
];
}
}
echo "\n" . str_repeat("=", 80) . "\n";
echo "СТАТИСТИКА:\n";
echo " Доступных файлов: $accessible\n";
echo " Недоступных файлов: $notAccessible\n";
if (!empty($errors)) {
echo "\nНЕДОСТУПНЫЕ ФАЙЛЫ:\n";
foreach ($errors as $error) {
echo " - ID {$error['id']}: {$error['title']}\n";
echo " S3 Key: {$error['s3_key']}\n";
echo " Причина: {$error['reason']}\n\n";
}
}

191
cleanup_disk.php Normal file
View File

@@ -0,0 +1,191 @@
<?php
/**
* Скрипт для безопасной очистки диска
*
* Удаляет:
* 1. Старые бэкапы SQL (оставляет последние N)
* 2. Большие логи (очищает или удаляет старые)
*/
error_reporting(E_ALL);
ini_set('display_errors', 1);
$dryRun = !isset($argv[1]) || $argv[1] !== '--execute';
$keepBackups = isset($argv[2]) ? (int)$argv[2] : (isset($argv[1]) && is_numeric($argv[1]) ? (int)$argv[1] : 5); // Сколько бэкапов оставить
echo "Очистка диска\n";
echo str_repeat("=", 80) . "\n\n";
if (!$dryRun) {
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n";
echo " Для реального удаления запустите: php cleanup_disk.php --execute [количество_бэкапов]\n";
echo " Пример: php cleanup_disk.php --execute 5\n\n";
} else {
echo "⚠️ РЕЖИМ УДАЛЕНИЯ - файлы будут удалены!\n\n";
}
$stats = [
'backups_found' => 0,
'backups_to_delete' => 0,
'backups_size' => 0,
'logs_found' => 0,
'logs_to_clean' => 0,
'logs_size' => 0,
'total_freed' => 0
];
// 1. Обработка бэкапов SQL
echo "1. ОБРАБОТКА БЭКАПОВ SQL\n";
echo str_repeat("-", 80) . "\n";
$backupDir = __DIR__;
$backups = glob($backupDir . '/backup_before_migration_*.sql');
if (empty($backups)) {
echo " Бэкапы не найдены\n\n";
} else {
$stats['backups_found'] = count($backups);
// Сортируем по дате изменения (новые первыми)
usort($backups, function($a, $b) {
return filemtime($b) - filemtime($a);
});
echo " Найдено бэкапов: {$stats['backups_found']}\n";
echo " Оставим последних: {$keepBackups}\n";
echo " Будет удалено: " . max(0, $stats['backups_found'] - $keepBackups) . "\n\n";
$toDelete = array_slice($backups, $keepBackups);
$stats['backups_to_delete'] = count($toDelete);
if (!empty($toDelete)) {
echo " Файлы для удаления:\n";
foreach ($toDelete as $backup) {
$size = filesize($backup);
$stats['backups_size'] += $size;
$sizeMB = round($size / 1024 / 1024, 2);
$date = date('Y-m-d H:i:s', filemtime($backup));
echo " - " . basename($backup) . " ({$sizeMB}MB, {$date})\n";
}
if ($dryRun && !empty($toDelete)) {
echo "\n Удаление файлов...\n";
foreach ($toDelete as $backup) {
if (unlink($backup)) {
echo "" . basename($backup) . " - удален\n";
} else {
echo "" . basename($backup) . " - ошибка удаления\n";
}
}
}
echo "\n";
} else {
echo " Нет файлов для удаления\n\n";
}
}
// 2. Обработка больших логов
echo "2. ОБРАБОТКА ЛОГОВ\n";
echo str_repeat("-", 80) . "\n";
$largeLogs = [
__DIR__ . '/wdall.log',
__DIR__ . '/wdall2.log',
__DIR__ . '/wa_inbound.log',
__DIR__ . '/wa_outbound.log',
];
foreach ($largeLogs as $logFile) {
if (file_exists($logFile)) {
$size = filesize($logFile);
$sizeMB = round($size / 1024 / 1024, 2);
if ($size > 10 * 1024 * 1024) { // Больше 10MB
$stats['logs_found']++;
$stats['logs_size'] += $size;
echo " Найден большой лог: " . basename($logFile) . " ({$sizeMB}MB)\n";
if ($dryRun) {
// Очищаем лог (оставляем последние 1000 строк)
$lines = file($logFile);
if (count($lines) > 1000) {
$keepLines = array_slice($lines, -1000);
if (file_put_contents($logFile, implode('', $keepLines))) {
$newSize = filesize($logFile);
$freedMB = round(($size - $newSize) / 1024 / 1024, 2);
echo " ✅ Очищен (освобождено {$freedMB}MB)\n";
$stats['logs_to_clean']++;
} else {
echo " ❌ Ошибка очистки\n";
}
} else {
echo " Лог небольшой, пропущен\n";
}
} else {
echo " ⏸️ Будет очищен (dry-run)\n";
$stats['logs_to_clean']++;
}
}
}
}
// Обработка логов в папке logs/
$logsDir = __DIR__ . '/logs';
if (is_dir($logsDir)) {
$logFiles = glob($logsDir . '/*.log*');
foreach ($logFiles as $logFile) {
$size = filesize($logFile);
if ($size > 20 * 1024 * 1024) { // Больше 20MB
$sizeMB = round($size / 1024 / 1024, 2);
$mtime = filemtime($logFile);
$daysOld = (time() - $mtime) / 86400;
if ($daysOld > 7) {
$stats['logs_found']++;
$stats['logs_size'] += $size;
echo " Старый большой лог: " . basename($logFile) . " ({$sizeMB}MB, " . round($daysOld) . " дней)\n";
if ($dryRun) {
if (unlink($logFile)) {
echo " ✅ Удален\n";
$stats['logs_to_clean']++;
} else {
echo " ❌ Ошибка удаления\n";
}
} else {
echo " ⏸️ Будет удален (dry-run)\n";
$stats['logs_to_clean']++;
}
}
}
}
}
echo "\n";
// Итоговая статистика
$stats['total_freed'] = $stats['backups_size'] + $stats['logs_size'];
$totalFreedMB = round($stats['total_freed'] / 1024 / 1024, 2);
$totalFreedGB = round($stats['total_freed'] / 1024 / 1024 / 1024, 2);
echo str_repeat("=", 80) . "\n";
echo "ИТОГОВАЯ СТАТИСТИКА:\n\n";
echo "Бэкапы:\n";
echo " - Найдено: {$stats['backups_found']}\n";
echo " - Будет удалено: {$stats['backups_to_delete']}\n";
echo " - Размер: " . round($stats['backups_size'] / 1024 / 1024 / 1024, 2) . "GB\n\n";
echo "Логи:\n";
echo " - Найдено больших: {$stats['logs_found']}\n";
echo " - Будет обработано: {$stats['logs_to_clean']}\n";
echo " - Размер: " . round($stats['logs_size'] / 1024 / 1024, 2) . "MB\n\n";
echo "ОБЩЕЕ ОСВОБОЖДЕНИЕ: {$totalFreedGB}GB ({$totalFreedMB}MB)\n\n";
if (!$dryRun) {
echo "⚠️ Это был режим проверки. Для реального удаления запустите:\n";
echo " php cleanup_disk.php --execute {$keepBackups}\n\n";
} else {
echo "✅ Очистка завершена!\n\n";
}

36
cleanup_nextcloud_logs.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
# Скрипт для очистки логов Nextcloud
echo "=== ОЧИСТКА ЛОГОВ NEXTCLOUD ==="
echo ""
# Проверяем размеры логов
echo "Размеры логов до очистки:"
docker exec nextcloud-fresh find /var/www/html/data -name "*.log" -type f -exec ls -lh {} \; 2>&1 | awk '{print $5, $9}'
echo ""
echo "Очистка логов..."
# Очищаем nextcloud.log если больше 100MB
SIZE=$(docker exec nextcloud-fresh stat -c%s /var/www/html/data/nextcloud.log 2>/dev/null || echo "0")
if [ "$SIZE" -gt 104857600 ]; then
echo "nextcloud.log больше 100MB, очищаем..."
docker exec nextcloud-fresh truncate -s 0 /var/www/html/data/nextcloud.log
echo "✅ nextcloud.log очищен"
fi
# Очищаем flow.log если больше 50MB
SIZE=$(docker exec nextcloud-fresh stat -c%s /var/www/html/data/flow.log 2>/dev/null || echo "0")
if [ "$SIZE" -gt 52428800 ]; then
echo "flow.log больше 50MB, очищаем..."
docker exec nextcloud-fresh truncate -s 0 /var/www/html/data/flow.log
echo "✅ flow.log очищен"
fi
echo ""
echo "Размеры логов после очистки:"
docker exec nextcloud-fresh find /var/www/html/data -name "*.log" -type f -exec ls -lh {} \; 2>&1 | awk '{print $5, $9}'
echo ""
echo "✅ Очистка завершена"

View File

@@ -4,6 +4,7 @@
"guzzlehttp/guzzle": "^7.8",
"tecnickcom/tcpdf": "^6.7",
"aws/aws-sdk-php": "^3.337",
"predis/predis": "^3.2"
"predis/predis": "^3.2",
"phpoffice/phpword": "^1.4"
}
}

View File

@@ -0,0 +1,3 @@
S3_ACCESS_KEY=2OMAK5ZNM900TAXM16J7
S3_SECRET_KEY=f4ADllb5VZBAt2HdsyB8WcwVEU7U74MwFCa1DARG
S3_BUCKET=f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c

View File

@@ -0,0 +1,277 @@
# 🏗️ Архитектура системы мониторинга файлов
## 🎯 Общая схема
```
┌─────────────────────────────────────────────────────────────────────────┐
│ ИСТОЧНИКИ ФАЙЛОВ │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌────────────┐ │
│ │ CRM PHP │ │ Nextcloud │ │ S3 Browser │ │ Внешние │ │
│ │ │ │ WebUI │ │ Cyberduck │ │ скрипты │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └─────┬──────┘ │
│ │ │ │ │ │
│ └─────────────────┼─────────────────┼─────────────────┘ │
│ │ │ │
└───────────────────────────┼─────────────────┼───────────────────────────┘
▼ ▼
┌──────────────────────────────────────┐
│ │
│ TWC S3 Storage (Cloud) │
│ f9825c87-18698658-c378-... │
│ │
└────┬─────────────────────────────┬───┘
│ │
│ (mounted via │ (polling)
│ external │
│ storage) │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ Nextcloud │ │ n8n S3 Monitor │
│ Docker Container │ │ Workflow │
│ │ │ │
│ Files change → │ │ Every 30s: │
│ notify_storage_ │ │ 1. List S3 files │
│ update (Redis) │ │ 2. Compare with prev │
└──────────┬───────────┘ │ 3. Detect changes │
│ └─────────┬────────────────┘
│ │
▼ ▼
┌──────────────────────┐ ┌─────────────────────────┐
│ redis_bridge.js │ │ │
│ (Node.js) │ │ (n8n internal) │
│ │ │ │
│ 1. Subscribe NC │ │ │
│ Redis │ │ │
│ 2. Parse events │ │ │
│ 3. Enrich data │ │ │
└──────────┬───────────┘ └─────────┬───────────────┘
│ │
│ │
└────────────┬───────────────┘
┌──────────────────────────────┐
│ CRM Redis │
│ 147.45.146.17:6379 │
│ │
│ Channel: │
│ crm:file:events │
└───────────┬──────────────────┘
│ (subscribe)
┌─────────────────┼─────────────────┐
│ │ │
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌────────────┐
│ n8n │ │ CRM PHP │ │ Другие │
│ Workflows │ │ Listeners │ │ сервисы │
└────────────┘ └────────────┘ └────────────┘
```
---
## 📊 Потоки событий
### Поток 1: Файлы через Nextcloud WebUI
```
User uploads file → Nextcloud → S3 Storage →
→ Nextcloud detects change → notify_storage_update (NC Redis) →
→ redis_bridge.js → crm:file:events (CRM Redis) → Обработчики
```
⏱️ **Задержка:** ~1-2 секунды (реал-тайм)
---
### Поток 2: Файлы напрямую в S3
```
External tool → S3 Storage →
→ n8n S3 Monitor (polling every 30s) → Detects change →
→ crm:file:events (CRM Redis) → Обработчики
```
⏱️ **Задержка:** 0-30 секунд (зависит от интервала polling)
---
## 🔄 Компоненты системы
### 1. **redis_bridge.js** (Node.js, работает на хосте)
- **Задача:** Мост между Nextcloud Redis и CRM Redis
- **Вход:** `notify_storage_update` (Nextcloud Redis, port 6380)
- **Выход:** `crm:file:events` (CRM Redis, port 6379)
- **Статус:** ✅ Работает
- **Запуск:**
```bash
cd /var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage
/usr/bin/nodejs redis_bridge.js &
```
### 2. **n8n S3 Monitor Workflow** (в n8n)
- **Задача:** Мониторинг прямых изменений в S3
- **Метод:** Polling (каждые 30 секунд)
- **Вход:** TWC S3 API (ListObjectsV2)
- **Выход:** `crm:file:events` (CRM Redis)
- **Статус:** ⚙️ Нужно настроить
- **Файл:** `n8n_s3_monitor_workflow.json`
### 3. **CRM Redis** (центральная шина событий)
- **Адрес:** `147.45.146.17:6379`
- **Канал:** `crm:file:events`
- **Формат события:**
```json
{
"type": "file_created|file_modified|file_deleted",
"source": "nextcloud|s3_monitor|crm",
"timestamp": "2025-10-30T12:34:56Z",
"path": "full/path/to/file.txt",
"filename": "file.txt",
"size": 12345,
"file_id": 73460,
"etag": "abc123...",
"operation": "create|update|delete"
}
```
---
## 🎯 Сценарии использования
### Сценарий 1: Обработка ERV файлов
```
1. Загружаешь ERV файл в S3 через S3 Browser
2. n8n S3 Monitor обнаруживает файл (через 0-30с)
3. Событие публикуется в Redis → crm:file:events
4. n8n ERV Processor подхватывает событие
5. Скачивает файл из S3
6. Обрабатывает ERV
7. Загружает в CRM
8. Отправляет уведомление
```
### Сценарий 2: Автоматическая обработка PDF счетов
```
1. Клиент загружает PDF в Nextcloud
2. Nextcloud → notify_storage_update → redis_bridge.js
3. Событие в Redis (реал-тайм, ~1с)
4. n8n Invoice Processor:
- Распознаёт текст (OCR)
- Извлекает данные
- Создаёт счёт в CRM
- Уведомляет бухгалтера
```
### Сценарий 3: Синхронизация с внешней системой
```
1. Внешний скрипт пишет в S3
2. n8n S3 Monitor (через 30с)
3. Redis событие
4. n8n External Sync:
- Проверяет формат файла
- Отправляет в external API
- Логирует в CRM
```
---
## ⚙️ Настройки производительности
### Интервал polling S3 Monitor
| Интервал | Задержка | Нагрузка на API | Рекомендация |
|----------|----------|-----------------|--------------|
| 10 сек | 0-10с | Высокая | Только для критичных файлов |
| 30 сек | 0-30с | Средняя | ✅ **Оптимально** |
| 60 сек | 0-60с | Низкая | Для некритичных файлов |
| 300 сек | 0-5мин | Минимальная | Для архивов |
### Количество файлов
- **< 1000 файлов:** Мониторь весь bucket
- **1000-10000:** Используй `Prefix` для фильтрации папок
- **> 10000:** Создай отдельные workflows для разных папок
---
## 🔧 Мониторинг и логи
### Проверка работы redis_bridge.js
```bash
ps aux | grep redis_bridge.js
tail -f /var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/redis_bridge.log
```
### Проверка событий в Redis
```bash
redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
SUBSCRIBE crm:file:events
```
### Проверка n8n workflow
- n8n UI → Workflows → S3 File Monitor → Executions
---
## 🚀 Расширение системы
### Добавление новых источников событий
Любой компонент может публиковать в `crm:file:events`:
**Пример из PHP (CRM):**
```php
$redis = new Redis();
$redis->connect('147.45.146.17', 6379);
$redis->auth('CRM_Redis_Pass_2025_Secure!');
$event = [
'type' => 'file_created',
'source' => 'crm_api',
'timestamp' => date('c'),
'path' => $filePath,
'filename' => basename($filePath),
'size' => filesize($localPath),
'user_id' => $userId
];
$redis->publish('crm:file:events', json_encode($event));
```
**Пример из Python:**
```python
import redis
import json
from datetime import datetime
r = redis.Redis(
host='147.45.146.17',
port=6379,
password='CRM_Redis_Pass_2025_Secure!'
)
event = {
'type': 'file_created',
'source': 'python_script',
'timestamp': datetime.utcnow().isoformat() + 'Z',
'path': file_path,
'filename': os.path.basename(file_path)
}
r.publish('crm:file:events', json.dumps(event))
```
---
## 📝 Итого
✅ **Nextcloud файлы** → реал-тайм через `redis_bridge.js`
✅ **S3 прямые загрузки** → polling через n8n (0-30с задержка)
✅ **CRM файлы** → прямая публикация в Redis
✅ **Единая точка обработки** → Redis канал `crm:file:events`
**Гибкость** → любой сервис может подписаться и обработать
**Всё работает через один канал Redis! 🎉**

View File

@@ -0,0 +1,238 @@
# 🎉 ФИНАЛЬНАЯ СВОДКА - S3 МОНИТОРИНГ
## ✅ ЧТО РАБОТАЕТ:
### 1⃣ **S3 Monitor** (Docker контейнер)
```
Контейнер: s3-monitor
Статус: ✅ Работает
Bucket: f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c
Файлов: 24,523
Интервал: каждые 30 секунд
Автоперезапуск: ✅
```
**Что делает:**
- Каждые 30 секунд сканирует S3 bucket
- Получает **ВСЕ** файлы (24,523 штук через pagination)
- Сравнивает с предыдущим состоянием
- Публикует изменения в Redis
**Команды:**
```bash
docker logs s3-monitor -f # Логи в реальном времени
docker stop s3-monitor # Остановить
docker start s3-monitor # Запустить
docker restart s3-monitor # Перезапустить
```
---
### 2⃣ **Redis Bridge** (Nextcloud → CRM Redis)
```
Процесс: /usr/bin/nodejs redis_bridge.js
Статус: ✅ Работает
Порт NC Redis: 127.0.0.1:6380
Порт CRM Redis: 147.45.146.17:6379
```
**Что делает:**
- Слушает Nextcloud Redis канал `notify_storage_update`
- Фильтрует временные файлы (.part, .lock, cache)
- **Дедупликация** - один file_id = одно событие (в течение 5 сек)
- Публикует в CRM Redis канал `crm:file:events`
**Команды:**
```bash
tail -f redis_bridge.log # Логи
ps aux | grep redis_bridge # Статус
pkill -f redis_bridge.js # Остановить
nohup /usr/bin/nodejs redis_bridge.js > redis_bridge.log 2>&1 & # Запустить
```
---
### 3⃣ **Redis Канал** (централизованная шина событий)
```
Host: 147.45.146.17
Port: 6379
Password: CRM_Redis_Pass_2025_Secure!
Channel: crm:file:events
```
---
## 📊 ФОРМАТ СОБЫТИЙ:
### От S3 Monitor:
```json
{
"type": "file_created",
"source": "s3_monitor",
"timestamp": "2025-10-30T20:49:31.593Z",
"path": "folder/subfolder/file.xlsx",
"filename": "file.xlsx",
"action": "created",
"size": 8224,
"etag": "7004954627252c9d0a7e6417f8325d07",
"last_modified": "2025-10-30T20:49:14.132Z"
}
```
### От Nextcloud (redis_bridge):
```json
{
"type": "file_update",
"source": "nextcloud",
"timestamp": "2025-10-30T20:52:21.236Z",
"storage_id": 4,
"path": "crm2/CRM_Active_Files/file.pdf",
"file_id": 42594,
"filename": "file.pdf",
"operation": "update"
}
```
**Оба публикуются в один канал:** `crm:file:events`
---
## 🎯 КАК ОБРАБАТЫВАТЬ В N8N:
### Шаг 1: Импортируй workflow обработчик
Файл: `n8n_s3_event_processor.json`
### Шаг 2: Настрой Redis Trigger
```
Node: Redis Subscribe
Channel: crm:file:events
Credential: CRM Redis
```
### Шаг 3: Parse JSON (если нужно)
```javascript
// n8n автоматически парсит, но если нужно:
const message = $json.message;
const event = typeof message === 'string' ? JSON.parse(message) : message;
return [{ json: event }];
```
### Шаг 4: Фильтруй по типу
```javascript
// Только новые файлы
$json.type === 'file_created'
// Только XLSX
$json.filename.endsWith('.xlsx')
// Только из определённой папки
$json.path.includes('Documents/')
```
### Шаг 5: Обработай файл
```
Switch по расширению:
├─ .xlsx → Обработка Excel
├─ .pdf → OCR и извлечение данных
├─ .jpg → Обработка изображений
└─ Другие → Логирование
```
---
## 🧪 ПРОВЕРКА СОБЫТИЙ:
### Подпишись на Redis:
```bash
redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
SUBSCRIBE crm:file:events
```
### Загрузи файл:
- Через Nextcloud WebUI
- Через S3 Browser
- Через aws-cli
### Увидишь событие:
```
1) "message"
2) "crm:file:events"
3) "{\"type\":\"file_created\",\"filename\":\"test.pdf\",...}"
```
---
## 📋 ИСТОЧНИКИ СОБЫТИЙ:
| Источник | Метод | Задержка | Фильтры |
|----------|-------|----------|---------|
| **Nextcloud WebUI** | Redis Bridge | 1-2 сек | ✅ Дедупликация<br>✅ Фильтр .part<br>✅ Фильтр cache |
| **S3 прямая загрузка** | S3 Monitor | 0-30 сек | Нет |
| **CRM API** | Прямая публикация | 0 сек | Настраивается |
---
## 🔧 ДОПОЛНИТЕЛЬНАЯ ФИЛЬТРАЦИЯ В N8N:
Если всё ещё приходит много событий от Nextcloud, добавь в n8n:
```javascript
// Фильтр: пропускаем дубликаты по filename + timestamp
const cache = $getWorkflowStaticData('node');
cache.recent = cache.recent || [];
const key = `${$json.filename}_${$json.file_id}`;
const now = Date.now();
// Очищаем старые (>10 секунд)
cache.recent = cache.recent.filter(item => (now - item.time) < 10000);
// Проверяем дубликат
if (cache.recent.find(item => item.key === key)) {
return []; // Пропускаем дубликат
}
// Добавляем в кеш
cache.recent.push({ key, time: now });
return [$input.item];
```
---
## 📁 ФАЙЛЫ:
Все в:
```
/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/
```
**Рабочие скрипты:**
-`s3_monitor_docker.js` - S3 мониторинг (запущен в Docker)
-`redis_bridge.js` - Nextcloud→CRM мост (запущен)
-`.env.s3monitor` - credentials для S3
**N8N Workflows для импорта:**
- `n8n_s3_event_processor.json` - обработчик событий (НОВЫЙ!)
- `n8n_nextcloud_activity_monitor.json` - альтернатива через Activity API
- `n8n_s3_monitor_workflow.json` - альтернатива S3 Monitor (если не хочешь Docker)
**Документация:**
- `SETUP_CHECKLIST.md` - чек-лист настройки
- `S3_MONITORING_GUIDE.md` - руководство по S3
- `ARCHITECTURE.md` - схема архитектуры
- `NEXTCLOUD_API_OVERVIEW.md` - обзор Nextcloud API
---
## 🎉 ГОТОВО!
**Система полностью работает:**
- ✅ S3 мониторинг (24,523 файла)
- ✅ Nextcloud мониторинг (с дедупликацией)
- ✅ Redis публикация
- ✅ Автоперезапуск
- ✅ Фильтрация дубликатов
**Теперь импортируй `n8n_s3_event_processor.json` в n8n и начинай обрабатывать файлы!** 🚀

View File

@@ -0,0 +1,377 @@
# 🌐 Nextcloud API - Обзор и возможности
## 📋 Доступные API
### 1. **WebDAV API** ✅ (РАБОТАЕТ)
**Базовый URL:** `https://office.clientright.ru:8443/remote.php/webdav/`
**Аутентификация:** Basic Auth (admin + app password)
**Возможности:**
- ✅ Список файлов и папок (PROPFIND)
- ✅ Загрузка файлов (PUT)
- ✅ Скачивание файлов (GET)
- ✅ Удаление файлов (DELETE)
- ✅ Создание папок (MKCOL)
- ✅ Перемещение/копирование (MOVE/COPY)
- ✅ Получение метаданных (PROPFIND)
**Пример - список файлов:**
```bash
curl -u "admin:APP_PASSWORD" \
-X PROPFIND \
"https://office.clientright.ru:8443/remote.php/webdav/" \
-H "Depth: 1"
```
**Пример - загрузка файла:**
```bash
curl -u "admin:APP_PASSWORD" \
-T /path/to/local/file.txt \
"https://office.clientright.ru:8443/remote.php/webdav/file.txt"
```
**Пример - скачивание файла:**
```bash
curl -u "admin:APP_PASSWORD" \
"https://office.clientright.ru:8443/remote.php/webdav/file.txt" \
-o downloaded.txt
```
---
### 2. **OCS API (Open Collaboration Services)** ✅
**Базовый URL:** `https://office.clientright.ru:8443/ocs/v2.php/`
**Заголовки:** `OCS-APIRequest: true`
**Формат ответа:** JSON (`?format=json`)
#### 2.1 **Capabilities API** (информация о сервере)
```bash
curl -u "admin:APP_PASSWORD" \
"https://office.clientright.ru:8443/ocs/v1.php/cloud/capabilities?format=json" \
-H "OCS-APIRequest: true"
```
**Ответ включает:**
- Версию Nextcloud (31.0.9)
- Доступные возможности
- Лимиты загрузки
- Настройки шаринга
---
#### 2.2 **Activity API** ✅ (СОБЫТИЯ ФАЙЛОВ!)
**URL:** `https://office.clientright.ru:8443/ocs/v2.php/apps/activity/api/v2/activity`
**Параметры:**
- `format=json` - формат ответа
- `limit=N` - количество событий
- `since=TIMESTAMP` - события после определённой даты
**Пример:**
```bash
curl -u "admin:APP_PASSWORD" \
"https://office.clientright.ru:8443/ocs/v2.php/apps/activity/api/v2/activity?format=json&limit=10" \
-H "OCS-APIRequest: true"
```
**Типы событий:**
- `file_created` - файл создан
- `file_changed` - файл изменён
- `file_deleted` - файл удалён
- `file_restored` - файл восстановлен
- `file_shared` - файл расшарен
**Структура события:**
```json
{
"activity_id": 195,
"type": "file_created",
"subject": "Вы создали «experimental_report.xlsx»",
"message": "",
"object_type": "files",
"object_id": 73460,
"object_name": "/experimental_report.xlsx",
"datetime": "2025-10-30T12:53:40+00:00",
"user": "admin"
}
```
**⚠️ Ограничения:**
- Нет real-time уведомлений (только polling)
- События агрегируются (несколько файлов в одном событии)
- Может быть задержка до минуты
---
#### 2.3 **Users API**
**Список пользователей:**
```bash
curl -u "admin:APP_PASSWORD" \
"https://office.clientright.ru:8443/ocs/v1.php/cloud/users?format=json" \
-H "OCS-APIRequest: true"
```
**Информация о пользователе:**
```bash
curl -u "admin:APP_PASSWORD" \
"https://office.clientright.ru:8443/ocs/v1.php/cloud/users/admin?format=json" \
-H "OCS-APIRequest: true"
```
---
#### 2.4 **Sharing API**
**Список расшаренных файлов:**
```bash
curl -u "admin:APP_PASSWORD" \
"https://office.clientright.ru:8443/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json" \
-H "OCS-APIRequest: true"
```
**Создать публичную ссылку:**
```bash
curl -u "admin:APP_PASSWORD" \
-X POST \
-d "path=/file.txt&shareType=3" \
"https://office.clientright.ru:8443/ocs/v2.php/apps/files_sharing/api/v1/shares?format=json" \
-H "OCS-APIRequest: true"
```
---
### 3. **Direct Download URL**
Для файлов можно получить прямую ссылку скачивания:
**Формат:**
```
https://office.clientright.ru:8443/index.php/apps/files/ajax/download.php?dir=/&files=filename.txt
```
Или через WebDAV:
```
https://office.clientright.ru:8443/remote.php/webdav/filename.txt
```
---
## 🎯 Использование для мониторинга файлов
### Вариант 1: Activity API Polling (РЕКОМЕНДУЮ для Nextcloud)
**Создай n8n workflow:**
```
┌─────────────────────────────────────────────┐
│ Schedule (каждые 30 сек) │
└─────────────────┬───────────────────────────┘
┌─────────────────────────────────────────────┐
│ HTTP Request │
│ GET /ocs/v2.php/apps/activity/api/v2/ │
│ activity?format=json&limit=100 │
└─────────────────┬───────────────────────────┘
┌─────────────────────────────────────────────┐
│ Code (JS) │
│ - Фильтруем file_created/changed/deleted │
│ - Сохраняем последний activity_id │
│ - Возвращаем только новые события │
└─────────────────┬───────────────────────────┘
┌─────────────────────────────────────────────┐
│ Redis Publish │
│ Channel: crm:file:events │
└─────────────────────────────────────────────┘
```
**Преимущества:**
- ✅ Официальный API
- ✅ Надёжный
- ✅ Детальная информация о событиях
-Не требует доступа к Redis Nextcloud
**Недостатки:**
- ⚠️ Задержка 30-60 сек (polling)
- ⚠️ События агрегируются
---
### Вариант 2: Redis Bridge (УЖЕ РАБОТАЕТ!)
```
Nextcloud Redis (notify_storage_update) →
→ redis_bridge.js →
→ CRM Redis (crm:file:events)
```
**Преимущества:**
- ✅ Real-time (~1-2 сек)
-Не нагружает Nextcloud API
**Недостатки:**
- ⚠️ Требует доступ к внутреннему Redis
- ⚠️ Меньше метаданных
---
## 📊 Сравнение подходов
| Метод | Задержка | Детали | Сложность | Надёжность |
|-------|----------|--------|-----------|------------|
| **Activity API** | 30-60с | ⭐⭐⭐ | Низкая | ⭐⭐⭐ |
| **Redis Bridge** | 1-2с | ⭐⭐ | Средняя | ⭐⭐⭐ |
| **S3 Monitor** | 0-30с | ⭐⭐ | Низкая | ⭐⭐⭐ |
| **WebDAV Poll** | 60с+ | ⭐ | Низкая | ⭐⭐ |
---
## 🛠️ Примеры кода
### PHP - Получение последних событий
```php
<?php
$username = 'admin';
$password = 'APP_PASSWORD';
$url = 'https://office.clientright.ru:8443/ocs/v2.php/apps/activity/api/v2/activity?format=json&limit=10';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
curl_setopt($ch, CURLOPT_HTTPHEADER, ['OCS-APIRequest: true']);
$response = curl_exec($ch);
$data = json_decode($response, true);
foreach ($data['ocs']['data'] as $activity) {
if (in_array($activity['type'], ['file_created', 'file_changed', 'file_deleted'])) {
echo "Event: {$activity['type']}\n";
echo "File: {$activity['object_name']}\n";
echo "Time: {$activity['datetime']}\n";
echo "---\n";
}
}
?>
```
### Node.js - Activity API Monitor
```javascript
const axios = require('axios');
const Redis = require('ioredis');
const CONFIG = {
nextcloud: {
url: 'https://office.clientright.ru:8443',
username: 'admin',
password: 'APP_PASSWORD'
},
redis: {
host: '147.45.146.17',
port: 6379,
password: 'CRM_Redis_Pass_2025_Secure!'
},
pollInterval: 30000 // 30 секунд
};
const redis = new Redis(CONFIG.redis);
let lastActivityId = 0;
async function checkActivities() {
try {
const response = await axios.get(
`${CONFIG.nextcloud.url}/ocs/v2.php/apps/activity/api/v2/activity`,
{
params: { format: 'json', limit: 100 },
headers: { 'OCS-APIRequest': 'true' },
auth: {
username: CONFIG.nextcloud.username,
password: CONFIG.nextcloud.password
}
}
);
const activities = response.data.ocs.data;
const fileEvents = activities.filter(a =>
['file_created', 'file_changed', 'file_deleted'].includes(a.type) &&
a.activity_id > lastActivityId
);
for (const event of fileEvents.reverse()) {
const payload = {
type: event.type,
source: 'nextcloud_activity',
timestamp: event.datetime,
file_id: event.object_id,
path: event.object_name,
filename: event.object_name.split('/').pop(),
user: event.user
};
await redis.publish('crm:file:events', JSON.stringify(payload));
console.log(`📤 Published: ${event.type} - ${event.object_name}`);
lastActivityId = Math.max(lastActivityId, event.activity_id);
}
} catch (error) {
console.error('❌ Error:', error.message);
}
}
// Запуск
console.log('🚀 Nextcloud Activity Monitor');
setInterval(checkActivities, CONFIG.pollInterval);
checkActivities();
```
---
## 🎯 Рекомендации
### Для твоего use case:
**Комбинируй несколько подходов:**
1. **Nextcloud файлы (WebUI)****Redis Bridge** (real-time, 1-2с)
2. **S3 прямые загрузки****n8n S3 Monitor** (polling, 0-30с)
3. **CRM загрузки****Прямая публикация в Redis** (instant)
**Все события в одном канале:** `crm:file:events`
**Альтернатива (если нужна простота):**
- Используй **только Activity API** через n8n для Nextcloud событий
- Плюс **S3 Monitor** для прямых загрузок в S3
---
## 📚 Полезные ссылки
- [Nextcloud WebDAV Documentation](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/)
- [Nextcloud OCS API](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/)
- [Activity API](https://github.com/nextcloud/activity/blob/master/docs/endpoint-v2.md)
---
## 🔐 Безопасность
**App Password:** `tGHKS-3cC9m-7Hggb-65Awk-zxWQE`
**Пользователь:** `admin`
⚠️ **Важно:** Используй App Passwords, а не основной пароль!
---
**Готово! Теперь ты знаешь все доступные API Nextcloud! 🎉**

View File

@@ -0,0 +1,169 @@
# 🔑 Nextcloud Redis - Креды и подключение
## 📋 Основная информация
**Контейнер:** `nextcloud-redis`
**IP внутри Docker сети:** `172.24.0.4`
**Порт:** `6379`
**Пароль:** `НЕТ` (не установлен)
---
## 🔌 Варианты подключения
### **ВАРИАНТ 1: Из хоста (если проброшен порт)**
**Если пробросить порт наружу:**
```bash
# Перезапустить контейнер с проброшенным портом:
docker stop nextcloud-redis
docker start -p 6378:6379 nextcloud-redis
```
**Тогда подключаться так:**
```
Host: localhost (или 147.45.146.17)
Port: 6378
Password: (пусто)
```
---
### **ВАРИАНТ 2: Через Docker network IP**
**Из другого контейнера в той же сети:**
```
Host: 172.24.0.4
Port: 6379
Password: (пусто)
```
---
### **ВАРИАНТ 3: Из самого хоста (если в той же сети)**
**Если хост в той же Docker сети:**
```
Host: 172.24.0.4
Port: 6379
Password: (пусто)
```
---
## 📡 Каналы Redis для подписки
### **Главный канал для файловых изменений:**
```
notify_storage_update
```
Этот канал публикует события когда:
- Файл создан/удалён/переименован
- Папка создана/удалена/переименована
### **Другие полезные каналы:**
```
notify_activity # Общая активность
notify_notification # Уведомления пользователей
notify_pre_auth # Пред-аутентификация
```
---
## 🧪 Тест подключения
### **Из командной строки:**
```bash
# Подключиться к Redis через docker exec:
docker exec -it nextcloud-redis redis-cli
# Или если порт проброшен:
redis-cli -h localhost -p 6378
```
**Тест подписки на канал:**
```redis
SUBSCRIBE notify_storage_update
```
---
## 📝 Формат сообщений
Сообщения в канале `notify_storage_update` имеют формат:
```json
{
"type": "notify_storage_update",
"path": "/admin/files/Documents/Projects/Проект_390983/файл_395695.docx",
"user": "admin",
"action": "write" | "delete" | "rename",
"oldPath": "...", // только для rename
"timestamp": "..."
}
```
---
## 🔧 Для n8n подключения
**Настройки в n8n:**
```
Host: 172.24.0.4 (или localhost:6378 если проброшен порт)
Port: 6379 (или 6378 если проброшен)
Password: (оставить пустым)
Database: 0
```
**Триггер:**
- Использовать "Redis Trigger" ноду
- Channel: `notify_storage_update`
---
## 🚀 Для нашего Node.js listener
**Обновить `nextcloud_listener.js`:**
```javascript
const redis = new Redis({
host: '172.24.0.4', // или localhost если проброшен порт
port: 6379, // или 6378 если проброшен
password: '' // пусто
});
redis.subscribe('notify_storage_update');
redis.on('message', (channel, message) => {
const event = JSON.parse(message);
// Обработка события
});
```
---
## ⚠️ ВАЖНО
1. **Безопасность:** Redis БЕЗ пароля доступен только из Docker сети!
2. **Если нужен доступ снаружи:** Обязательно установи пароль!
3. **Мониторинг:** Следи за нагрузкой на Redis при подписке на каналы
---
## 🔐 Рекомендация: Установить пароль
Если планируешь пробрасывать порт наружу:
```bash
docker exec nextcloud-redis redis-cli CONFIG SET requirepass аш_пароль"
```
И добавить в Nextcloud config.php:
```php
'redis' => array (
'atype' => 'redis',
'host' => 'nextcloud-redis',
'port' => 6379,
'password' => аш_пароль',
),
```

View File

@@ -0,0 +1,182 @@
# 🎉 PRODUCTION READY - Мониторинг файлов
## ✅ ЧТО РАБОТАЕТ:
### 1⃣ Nextcloud Activity Monitor
**Скрипт:** `nextcloud_activity_monitor.js`
**Запущен:** ✅ (PID: 2122)
**Лог:** `nextcloud_activity.log`
**Мониторит:**
- Файлы загруженные через Nextcloud WebUI
- Файлы созданные/изменённые в Nextcloud
**Метод:**
- Nextcloud Activity API (polling каждые 30 сек)
- Разбивает агрегированные события на отдельные файлы
- БЕЗ дубликатов!
**Формат события:**
```json
{
"type": "file_created",
"source": "nextcloud_activity",
"timestamp": "2025-10-30T12:53:40+00:00",
"file_id": 73460,
"path": "/experimental_report.xlsx",
"filename": "experimental_report.xlsx",
"user": "admin",
"action": "created"
}
```
**Команды:**
```bash
# Статус
ps aux | grep nextcloud_activity_monitor
# Логи
tail -f nextcloud_activity.log
# Остановить
pkill -f nextcloud_activity_monitor.js
# Запустить
cd /var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage
nohup /usr/bin/nodejs nextcloud_activity_monitor.js > nextcloud_activity.log 2>&1 &
```
---
### 2⃣ S3 Monitor (Docker)
**Контейнер:** `s3-monitor`
**Запущен:** ✅ (Up 40 минут)
**Скрипт:** `s3_monitor_docker.js`
**Мониторит:**
- Файлы загруженные напрямую в S3
- Через S3 Browser, aws-cli, Cyberduck, скрипты
- Любые изменения в bucket
**Метод:**
- S3 ListObjectsV2 API (polling каждые 30 сек)
- Pagination - получает ВСЕ файлы (24,522 файла!)
- Сравнение по ETag
**Bucket:** `f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c`
**Формат события:**
```json
{
"type": "file_created",
"source": "s3_monitor",
"timestamp": "2025-10-30T20:49:31.593Z",
"path": "crm2/CRM_Active_Files/Documents/file.xlsx",
"filename": "file.xlsx",
"size": 8224,
"etag": "7004954627252c9d0a7e6417f8325d07",
"last_modified": "2025-10-30T20:49:14.132Z",
"action": "created"
}
```
**Команды:**
```bash
# Статус
docker ps | grep s3-monitor
# Логи
docker logs s3-monitor -f
# Остановить
docker stop s3-monitor
# Запустить
docker start s3-monitor
# Перезапустить
docker restart s3-monitor
```
---
### 3⃣ Redis Канал (центральная шина)
**Host:** `147.45.146.17`
**Port:** `6379`
**Password:** `CRM_Redis_Pass_2025_Secure!`
**Channel:** `crm:file:events`
**Подписаться:**
```bash
redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
SUBSCRIBE crm:file:events
```
---
## 🚫 ЧТО ОСТАНОВЛЕНО:
### ❌ redis_bridge.js
**Причина:** Давал неполные данные, много дубликатов
**Заменён на:** Nextcloud Activity Monitor (даёт чистые данные)
---
## 📊 СРАВНЕНИЕ ДАННЫХ:
| Поле | Nextcloud Activity | S3 Monitor |
|------|-------------------|------------|
| **type** | ✅ file_created/changed/deleted | ✅ file_created/modified/deleted |
| **source** | nextcloud_activity | s3_monitor |
| **filename** | ✅ | ✅ |
| **path** | ✅ (Nextcloud путь) | ✅ (S3 полный путь) |
| **file_id** | ✅ | ❌ |
| **user** | ✅ | ❌ |
| **size** | ❌ | ✅ |
| **etag** | ❌ | ✅ |
| **mime_type** | ❌ | ❌ |
**Дополняют друг друга!** 🎯
---
## 🔧 АВТОЗАПУСК ПРИ ПЕРЕЗАГРУЗКЕ СЕРВЕРА:
### S3 Monitor:
✅ Уже настроен (`--restart unless-stopped`)
### Nextcloud Activity Monitor:
Создам systemd service:
```bash
sudo tee /etc/systemd/system/nextcloud-activity-monitor.service << 'EOF'
[Unit]
Description=Nextcloud Activity Monitor
After=network.target redis.service
[Service]
Type=simple
User=root
WorkingDirectory=/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage
ExecStart=/usr/bin/nodejs nextcloud_activity_monitor.js
Restart=always
RestartSec=10
StandardOutput=append:/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/nextcloud_activity.log
StandardError=append:/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/nextcloud_activity.log
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable nextcloud-activity-monitor
sudo systemctl start nextcloud-activity-monitor
```
---
## 🎯 ГОТОВО!
**Система полностью работает в production режиме!** 🚀
Хочешь чтобы я создал systemd service для автозапуска Nextcloud Activity Monitor? 😊

View File

@@ -0,0 +1,104 @@
# ⚡ Quick Start - Мониторинг файлов
## 🎯 Быстрая проверка системы
```bash
bash START.sh
```
Покажет статус всех компонентов и запустит остановленные.
---
## 📊 Что работает прямо сейчас:
### ✅ Nextcloud Activity Monitor
- **PID:** 2122
- **Лог:** `nextcloud_activity.log`
- **Что делает:** Ловит файлы из Nextcloud WebUI
- **Задержка:** 30 сек
### ✅ S3 Monitor (Docker)
- **Контейнер:** `s3-monitor`
- **Uptime:** 40+ минут
- **Что делает:** Ловит файлы загруженные напрямую в S3
- **Задержка:** 30 сек
- **Файлов:** 24,522
### ✅ Redis канал
- **Адрес:** `147.45.146.17:6379`
- **Канал:** `crm:file:events`
- **Статус:** Оба монитора публикуют события ✅
---
## 🧪 Быстрый тест
### Тест 1: Загрузи файл в Nextcloud
```bash
# В другом терминале подпишись на события
redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
SUBSCRIBE crm:file:events
```
Загрузи файл через Nextcloud WebUI → через 30 сек увидишь событие!
### Тест 2: Загрузи файл в S3
```bash
echo "test" > /tmp/test.txt
aws s3 cp /tmp/test.txt \
s3://f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/test_$(date +%s).txt \
--endpoint-url https://s3.twcstorage.ru
```
Через 0-30 сек увидишь событие!
---
## 🔧 Команды управления
```bash
# Проверка статуса
bash START.sh
# Логи в реальном времени
tail -f nextcloud_activity.log
docker logs s3-monitor -f
# Остановка
pkill -f nextcloud_activity_monitor.js
docker stop s3-monitor
# Запуск
bash START.sh
```
---
## 📚 Документация
- `README.md` - обзор системы
- `PRODUCTION_READY.md` - production конфигурация
- `ARCHITECTURE.md` - архитектура
---
## 🆘 Troubleshooting
**Нет событий?**
1. Проверь статус: `bash START.sh`
2. Проверь логи: `tail -f nextcloud_activity.log`
3. Проверь Redis: подпишись на канал
**Много дубликатов?**
- От Nextcloud: дедупликация включена (1 событие вместо 6)
- От S3: дубликатов нет
**Нужна помощь?**
- Читай `PRODUCTION_READY.md`
- Проверяй логи
---
**Всё готово! 🚀**

View File

@@ -0,0 +1,137 @@
# 🔔 Система мониторинга файлов CRM
Автоматический мониторинг изменений файлов в S3 и Nextcloud с публикацией событий в Redis.
## 🎯 Компоненты системы
### 1. Nextcloud Activity Monitor
**Файл:** `nextcloud_activity_monitor.js`
**Назначение:** Мониторинг файлов загруженных через Nextcloud WebUI
**Метод:** Nextcloud Activity API
**Интервал:** 30 секунд
**Запуск:**
```bash
cd /var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage
nohup /usr/bin/nodejs nextcloud_activity_monitor.js > nextcloud_activity.log 2>&1 &
```
**Управление:**
```bash
ps aux | grep nextcloud_activity_monitor # Статус
tail -f nextcloud_activity.log # Логи
pkill -f nextcloud_activity_monitor.js # Остановить
```
---
### 2. S3 Monitor (Docker)
**Контейнер:** `s3-monitor`
**Назначение:** Мониторинг файлов загруженных напрямую в S3
**Bucket:** `f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c`
**Интервал:** 30 секунд
**Файлов:** ~24,500
**Управление:**
```bash
docker ps | grep s3-monitor # Статус
docker logs s3-monitor -f # Логи
docker stop s3-monitor # Остановить
docker start s3-monitor # Запустить
docker restart s3-monitor # Перезапустить
```
---
### 3. Redis (центральная шина событий)
**Адрес:** `147.45.146.17:6379`
**Канал:** `crm:file:events`
**Password:** `CRM_Redis_Pass_2025_Secure!`
**Подписка:**
```bash
redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
SUBSCRIBE crm:file:events
```
---
## 📊 Формат событий
### От Nextcloud Activity Monitor:
```json
{
"type": "file_created",
"source": "nextcloud_activity",
"timestamp": "2025-10-30T12:53:40+00:00",
"file_id": 73460,
"path": "/experimental_report.xlsx",
"filename": "experimental_report.xlsx",
"user": "admin",
"action": "created"
}
```
### От S3 Monitor:
```json
{
"type": "file_created",
"source": "s3_monitor",
"timestamp": "2025-10-30T20:49:31.593Z",
"path": "crm2/CRM_Active_Files/Documents/file.xlsx",
"filename": "file.xlsx",
"size": 8224,
"etag": "7004954627252c9d0a7e6417f8325d07",
"last_modified": "2025-10-30T20:49:14.132Z",
"action": "created"
}
```
---
## 🔧 Credentials
### Nextcloud API:
```
URL: https://office.clientright.ru:8443
User: admin
App Password: tGHKS-3cC9m-7Hggb-65Awk-zxWQE
```
### S3 (TWC Storage):
```
Endpoint: https://s3.twcstorage.ru
Region: ru-1
Access Key: 2OMAK5ZNM900TAXM16J7
Secret Key: f4ADllb5VZBAt2HdsyB8WcwVEU7U74MwFCa1DARG
Bucket: f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c
```
---
## 📚 Дополнительные документы
- `PRODUCTION_READY.md` - полное описание production конфигурации
- `ARCHITECTURE.md` - схема архитектуры системы
- `S3_MONITORING_GUIDE.md` - руководство по S3 мониторингу
- `NEXTCLOUD_API_OVERVIEW.md` - обзор Nextcloud API
- `SETUP_CHECKLIST.md` - чек-лист настройки
---
## 🚀 Для разработчиков
### n8n Workflows (готовые для импорта):
- `n8n_s3_event_processor.json` - обработчик событий из Redis
- `n8n_nextcloud_activity_monitor.json` - альтернатива через n8n
- `n8n_s3_monitor_workflow.json` - альтернатива S3 Monitor через n8n
### Утилиты:
- `get_s3_credentials.sh` - получение S3 credentials
---
**Дата создания:** 30 октября 2025
**Версия:** 1.0
**Статус:** Production Ready ✅

View File

@@ -79,9 +79,25 @@ class S3Client {
/**
* Создание временной ссылки для скачивания
* @param string $s3Key S3 ключ файла
* @param mixed $expiresIn Время жизни URL в секундах (число) или строка типа '+10 minutes'
*/
public function getPresignedUrl($s3Key, $expiresIn = 3600) {
try {
// Преобразуем строку TTL в секунды, если нужно
if (is_string($expiresIn)) {
// Если строка начинается с '+', используем её как есть для strtotime
if (strpos($expiresIn, '+') === 0) {
$expiresIn = strtotime($expiresIn) - time();
} else {
// Иначе пытаемся распарсить как число секунд
$expiresIn = (int)$expiresIn;
}
}
// Минимум 60 секунд, максимум 7 дней
$expiresIn = max(60, min($expiresIn, 604800));
$cmd = $this->client->getCommand('GetObject', [
'Bucket' => $this->bucket,
'Key' => $s3Key
@@ -97,7 +113,9 @@ class S3Client {
} catch (AwsException $e) {
return [
'success' => false,
'error' => $e->getMessage()
'error' => $e->getMessage(),
'error_code' => $e->getAwsErrorCode(),
'request_id' => $e->getAwsRequestId()
];
}
}

View File

@@ -0,0 +1,248 @@
# 🔍 Руководство по мониторингу S3
## Проблема
Тебе нужно ловить **любые** изменения файлов в S3 bucket, даже если они загружены:
- Вручную через S3 Browser / Cyberduck
- Из внешних скриптов
- Не через CRM или Nextcloud
## ❌ Почему не Event Notifications?
TWC Storage **не поддерживает** S3 Event Notifications официально (не документировано).
## ✅ РЕШЕНИЕ: n8n Workflow с Polling
### Как работает:
```
n8n каждые 30 сек → Список файлов в S3 → Сравнение с предыдущим состоянием →
→ Обнаружены изменения → Публикация в Redis → Твои обработчики
```
---
## 📋 Пошаговая инструкция
### Шаг 1: Импортируй workflow в n8n
1. Открой n8n: https://n8n.clientright.pro
2. Создай новый workflow
3. Нажми **Import from File**
4. Загрузи файл: `n8n_s3_monitor_workflow.json`
### Шаг 2: Настрой S3 Credentials
В n8n создай credentials:
**Тип:** AWS S3
**Имя:** `TWC S3`
**Параметры:**
```
Access Key ID: [твой S3 ключ из Nextcloud]
Secret Access Key: [твой S3 секрет из Nextcloud]
Region: ru-1
Custom Endpoints: ✅ (включить!)
S3 Endpoint: https://s3.twcstorage.ru
Force Path Style: ✅ (включить!)
```
**Как получить ключи:**
```bash
docker exec nextcloud-fresh php occ files_external:list --output=json | jq -r '.[0].configuration | "Key: \(.key)\nSecret: \(.secret)\nBucket: \(.bucket)"'
```
### Шаг 3: Настрой Redis Credentials
**Тип:** Redis
**Имя:** `CRM Redis`
**Параметры:**
```
Host: 147.45.146.17
Port: 6379
Password: CRM_Redis_Pass_2025_Secure!
Database: 0
```
### Шаг 4: Настрой Environment Variables (если нужно)
В n8n Settings → Environment Variables:
```
S3_BUCKET=f9825c87-18698658-c378-4aa7-91cc-0c131bebccda
WEBHOOK_URL=https://твой-эндпоинт.com/webhook (опционально)
```
### Шаг 5: Настрой Bucket и Prefix
В ноде **List S3 Files**:
- `Bucket Name`: укажи свой bucket (или используй `{{$env.S3_BUCKET}}`)
- `Prefix`: укажи папку для мониторинга (например `nextcloud/data/admin/files/`)
- Оставь пустым для мониторинга всего bucket
### Шаг 6: Активируй Workflow
1. Нажми **Active** (включи workflow)
2. Первый запуск создаст начальное состояние файлов
3. Последующие запуски будут сравнивать с предыдущим состоянием
---
## 🧪 Тестирование
### 1. Загрузи тестовый файл в S3
Через S3 Browser, Cyberduck или aws-cli:
```bash
echo "test" > test.txt
aws s3 cp test.txt s3://твой-bucket/test.txt \
--endpoint-url https://s3.twcstorage.ru
```
### 2. Подожди 30 секунд (интервал polling)
### 3. Проверь Redis
```bash
redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
SUBSCRIBE crm:file:events
```
Должно прийти:
```json
{
"type": "file_created",
"source": "s3_monitor",
"timestamp": "2025-10-30T12:34:56Z",
"path": "test.txt",
"filename": "test.txt",
"size": 5,
"etag": "\"098f6bcd4621d373cade4e832627b4f6\"",
"last_modified": "2025-10-30T12:34:50Z"
}
```
---
## ⚙️ Настройка интервала
Измени интервал проверки в ноде **"Каждые 30 секунд"**:
- **10 секунд** - почти реал-тайм, но больше нагрузка на S3 API
- **30 секунд** - оптимальный баланс (рекомендуется)
- **60 секунд** - экономия API запросов
**Важно:** TWC может лимитировать количество API запросов!
---
## 📊 Типы событий
Workflow генерирует 3 типа событий:
1. **`file_created`** - новый файл появился в bucket
2. **`file_modified`** - файл изменился (другой ETag)
3. **`file_deleted`** - файл удалён из bucket
---
## 🔄 Обработка событий в других workflows
Создай новый workflow в n8n:
### Trigger: Redis (Subscribe)
```
Host: 147.45.146.17
Port: 6379
Password: CRM_Redis_Pass_2025_Secure!
Channel: crm:file:events
```
### Filter: По типу события
```javascript
// Обрабатываем только создание файлов
return $json.type === 'file_created';
```
### Switch: По расширению файла
```javascript
const ext = $json.filename.split('.').pop().toLowerCase();
switch(ext) {
case 'pdf':
return [0]; // Обработка PDF
case 'jpg':
case 'png':
return [1]; // Обработка изображений
case 'xlsx':
case 'csv':
return [2]; // Обработка таблиц
default:
return [3]; // Остальные файлы
}
```
---
## 🎯 Пример: Автоматическая обработка ERV файлов
```
Redis Subscribe (crm:file:events) →
→ Filter (filename contains "erv") →
→ Download from S3 →
→ Process ERV →
→ Upload to CRM →
→ Notify user
```
---
## 📝 Мониторинг нескольких buckets
Если тебе нужно мониторить несколько buckets:
1. **Вариант 1:** Создай отдельный workflow для каждого bucket
2. **Вариант 2:** Используй Loop в workflow для перебора buckets
---
## 🚨 Важные замечания
1. **State сохраняется в Static Data** - не удаляй workflow без экспорта!
2. **Первый запуск не генерирует события** - только создаёт начальное состояние
3. **ETag используется для определения изменений** - если файл перезаписан с тем же содержимым, событие не сработает
4. **Polling = задержка** - событие придёт через 0-30 секунд после реального изменения
---
## 🔧 Troubleshooting
### Проблема: Не приходят события
- Проверь что workflow **Active** (зелёная галочка)
- Проверь логи workflow (Execute → View executions)
- Проверь credentials (S3 и Redis)
### Проблема: Слишком много событий
- Уменьши область мониторинга через `Prefix`
- Увеличь интервал polling до 60-120 секунд
### Проблема: Пропускаются изменения
- Уменьши интервал до 10-15 секунд
- Проверь что State сохраняется (Static Data)
---
## 📚 Дополнительные материалы
- [n8n AWS S3 Node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.awss3/)
- [n8n Redis Node](https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.redis/)
- [n8n Code Node](https://docs.n8n.io/code-examples/methods-variables-reference/)
---
## 🎉 Готово!
Теперь у тебя есть полноценный мониторинг S3 bucket в реальном времени!
Все изменения файлов будут автоматически публиковаться в Redis канал `crm:file:events`,
откуда их могут подхватить другие твои системы (CRM, другие n8n workflows, и т.д.)

View File

@@ -0,0 +1,228 @@
# ✅ Чек-лист настройки S3 мониторинга
## 📋 Что нужно сделать
### Шаг 1: Получи S3 Credentials ✅
```bash
cd /var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage
bash get_s3_credentials.sh
```
**Сохрани куда-нибудь:**
- Access Key ID
- Secret Access Key
- Bucket name
- Endpoint: `https://s3.twcstorage.ru`
- Region: `ru-1`
---
### Шаг 2: Импортируй workflow в n8n ⏳
1. Открой https://n8n.clientright.pro
2. Нажми **"+"** → **Import from File**
3. Выбери файл: `/var/www/.../n8n_s3_monitor_workflow.json`
4. Нажми **Import**
---
### Шаг 3: Настрой S3 Credentials в n8n ⏳
1. В n8n иди в **Settings** (⚙️) → **Credentials**
2. Нажми **Add Credential** → выбери **AWS**
3. Заполни:
- **Name:** `TWC S3`
- **Access Key ID:** (из шага 1)
- **Secret Access Key:** (из шага 1)
- **Region:** `ru-1`
4. **Включи** `Custom Endpoints`
5. **S3 Endpoint:** `https://s3.twcstorage.ru`
6. **Включи** `Force Path Style`
7. Нажми **Test** → должно быть ✅
8. **Save**
---
### Шаг 4: Настрой Redis Credentials в n8n ⏳
1. **Add Credential****Redis**
2. Заполни:
- **Name:** `CRM Redis`
- **Host:** `147.45.146.17`
- **Port:** `6379`
- **Password:** `CRM_Redis_Pass_2025_Secure!`
- **Database:** `0`
3. **Test** → ✅
4. **Save**
---
### Шаг 5: Настрой workflow ⏳
1. Открой импортированный workflow **"S3 File Monitor"**
2. Кликни на ноду **"List S3 Files"**
3. В поле **Credential for AWS** выбери `TWC S3`
4. В поле **Bucket Name:**
- Вариант 1: Впиши имя bucket из шага 1
- Вариант 2: Оставь `{{$env.S3_BUCKET}}` и настрой env variable
5. В поле **Prefix** (опционально):
- Оставь пустым для мониторинга всего bucket
- Или укажи папку, например: `nextcloud/data/admin/files/`
6. Кликни на ноду **"Publish to Redis"**
7. Выбери credential: `CRM Redis`
8. **Save** workflow
---
### Шаг 6: Активируй workflow ⏳
1. Переключи тумблер **Active** в ON (вверху справа)
2. Workflow начнёт работать!
**Первый запуск:**
- Создаст начальное состояние файлов
- **НЕ** сгенерирует события (это нормально!)
**Последующие запуски (каждые 30 сек):**
- Будут сравнивать с предыдущим состоянием
- Генерировать события при изменениях
---
### Шаг 7: Протестируй! 🧪
#### Тест 1: Загрузи файл в S3
Через **S3 Browser** или **aws-cli**:
```bash
echo "test content" > /tmp/test-file.txt
aws s3 cp /tmp/test-file.txt s3://ИМЯ-BUCKET/test-file.txt \
--endpoint-url https://s3.twcstorage.ru
```
#### Тест 2: Подожди 30 секунд
⏱️ Время для следующего polling цикла...
#### Тест 3: Проверь Redis
```bash
redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' \
--csv SUBSCRIBE crm:file:events
```
Должно прийти:
```
"subscribe","crm:file:events",1
"message","crm:file:events","{\"type\":\"file_created\",\"source\":\"s3_monitor\",...}"
```
#### Тест 4: Проверь n8n executions
1. В n8n → **S3 File Monitor** workflow
2. Нажми **Executions** (внизу)
3. Посмотри последние запуски
4. Должны быть ✅ зелёные
---
### Шаг 8: Настрой обработчик событий (опционально) ⏳
Создай новый workflow в n8n для обработки событий:
1. **Trigger:** Redis (Subscribe)
- Channel: `crm:file:events`
- Credential: `CRM Redis`
2. **Filter** по типу файла:
```javascript
// Обрабатываем только PDF
return $json.filename.endsWith('.pdf');
```
3. **Твоя логика:**
- Download from S3
- Process
- Upload to CRM
- Notify user
---
## 🔧 Troubleshooting
### ❌ Workflow не запускается
- Проверь что он **Active** (зелёная галочка)
- Проверь Executions → есть ли ошибки?
### ❌ S3 Connection failed
- Проверь credentials (Access Key, Secret)
- Проверь что включен `Force Path Style` ✅
- Проверь endpoint: `https://s3.twcstorage.ru`
### ❌ Redis Connection failed
- Проверь firewall: порт 6379 открыт?
- Проверь пароль: `CRM_Redis_Pass_2025_Secure!`
- Попробуй из терминала:
```bash
redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' PING
```
### ❌ События не приходят
- Убедись что файл добавлен **в правильный bucket**
- Убедись что файл **в нужной папке** (если указан Prefix)
- Подожди 30 секунд для polling цикла
- Проверь Static Data в workflow (сохраняется ли состояние?)
### ⚠️ Слишком много событий
- Уменьши область мониторинга через `Prefix`
- Увеличь интервал до 60-120 секунд
---
## 📊 Финальная проверка
После всех шагов у тебя должно быть:
- ✅ S3 credentials сохранены в n8n
- ✅ Redis credentials сохранены в n8n
- ✅ Workflow "S3 File Monitor" импортирован
- ✅ Workflow активирован (Active = ON)
- ✅ Тестовый файл загружен в S3
- ✅ Событие пришло в Redis канал `crm:file:events`
- ✅ Execution в n8n показывает ✅ success
---
## 🎉 Готово!
Теперь **любые** изменения в твоём S3 bucket будут автоматически:
1. Обнаруживаться (каждые 30 сек)
2. Публиковаться в Redis (`crm:file:events`)
3. Доступны для обработки в n8n, CRM, и других сервисах
---
## 📚 Дополнительные файлы
- `S3_MONITORING_GUIDE.md` - полное руководство
- `ARCHITECTURE.md` - схема архитектуры
- `n8n_s3_monitor_workflow.json` - workflow для импорта
- `get_s3_credentials.sh` - скрипт для получения credentials
---
## 🆘 Нужна помощь?
Если что-то не работает:
1. Проверь логи n8n executions
2. Проверь Redis подключение
3. Проверь S3 credentials
4. Перечитай `S3_MONITORING_GUIDE.md`
**Удачи!** 🚀

View File

@@ -0,0 +1,76 @@
#!/bin/bash
#
# Скрипт для запуска/проверки всех компонентов мониторинга файлов
#
echo "🚀 CRM File Monitoring System"
echo "════════════════════════════════════════════════════════════════════════════════"
echo ""
cd /var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage
# Проверка S3 Monitor
echo "1⃣ S3 Monitor (Docker):"
if docker ps | grep -q s3-monitor; then
echo " ✅ Работает"
docker ps --format " Статус: {{.Status}}" | grep s3-monitor
else
echo " ❌ НЕ работает"
echo " Запускаем..."
docker start s3-monitor 2>/dev/null || echo " ⚠️ Контейнер не существует, запустите вручную"
fi
echo ""
# Проверка Nextcloud Activity Monitor
echo "2⃣ Nextcloud Activity Monitor:"
if ps aux | grep -v grep | grep -q nextcloud_activity_monitor.js; then
PID=$(ps aux | grep -v grep | grep nextcloud_activity_monitor.js | awk '{print $2}')
echo " ✅ Работает (PID: $PID)"
else
echo " ❌ НЕ работает"
echo " Запускаем..."
nohup /usr/bin/nodejs nextcloud_activity_monitor.js > nextcloud_activity.log 2>&1 &
sleep 2
echo " ✅ Запущен (PID: $!)"
fi
echo ""
# Проверка Redis
echo "3⃣ Redis подключение:"
if redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' PING 2>&1 | grep -q PONG; then
echo " ✅ Redis доступен"
else
echo " ❌ Redis недоступен"
fi
echo ""
echo "════════════════════════════════════════════════════════════════════════════════"
echo ""
echo "📋 СТАТУС:"
echo ""
echo "Nextcloud Activity Monitor:"
tail -3 nextcloud_activity.log 2>/dev/null | tail -1
echo ""
echo "S3 Monitor:"
docker logs s3-monitor 2>&1 | tail -3 | tail -1
echo ""
echo "════════════════════════════════════════════════════════════════════════════════"
echo ""
echo "📚 Команды:"
echo ""
echo "Логи в реальном времени:"
echo " tail -f nextcloud_activity.log"
echo " docker logs s3-monitor -f"
echo ""
echo "Подписка на события:"
echo " redis-cli -h 147.45.146.17 -p 6379 -a 'CRM_Redis_Pass_2025_Secure!' SUBSCRIBE crm:file:events"
echo ""
echo "Документация:"
echo " cat README.md"
echo " cat PRODUCTION_READY.md"
echo ""

View File

@@ -0,0 +1,570 @@
<?php
/**
* Создание документа с текстом от AI
*
* Упрощенная версия - создает пустой DOCX, записывает в него текст от AI
* и возвращает ссылку на редактирование
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 1);
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
// Параметры - поддерживаем JSON POST и обычные POST/GET
$input = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$contentType = $_SERVER['CONTENT_TYPE'] ?? '';
if (strpos($contentType, 'application/json') !== false) {
$input = json_decode(file_get_contents('php://input'), true);
} else {
$input = $_POST;
}
}
$module = $input['module'] ?? $_GET['module'] ?? '';
$recordId = $input['recordId'] ?? $_GET['recordId'] ?? '';
$recordName = $input['recordName'] ?? $_GET['recordName'] ?? '';
$fileName = $input['fileName'] ?? $_GET['fileName'] ?? '';
$documentText = $input['documentText'] ?? $_GET['documentText'] ?? '';
$documentType = $input['documentType'] ?? $_GET['documentType'] ?? 'docx'; // docx, xlsx, pptx
if (empty($module) || empty($recordId) || empty($fileName) || empty($documentText)) {
die(json_encode([
'success' => false,
'error' => 'Не указаны обязательные параметры: module, recordId, fileName, documentText'
]));
}
// Определяем папку модуля
$moduleFolders = [
'Project' => 'Project',
'Contacts' => 'Contacts',
'Accounts' => 'Accounts',
'Invoice' => 'Invoice',
'Quotes' => 'Quotes',
'SalesOrder' => 'SalesOrder',
'PurchaseOrder' => 'PurchaseOrder',
'HelpDesk' => 'HelpDesk',
'Leads' => 'Leads',
'Potentials' => 'Potentials'
];
$moduleFolder = $moduleFolders[$module] ?? 'Other';
// Формируем имя папки записи (заменяем пробелы и спецсимволы на подчеркивания)
$recordName = preg_replace('/[\/\\\\:\*\?"<>\|\s]+/', '_', $recordName); // \s+ заменяет все пробелы на одно подчеркивание
$recordName = trim($recordName, '_'); // Убираем подчеркивания в начале и конце
$folderName = $recordName . '_' . $recordId;
// Путь к готовому документу (правильный формат: crm2/CRM_Active_Files/... без /crm/ в начале)
$fileExtension = $documentType === 'xlsx' ? 'xlsx' : ($documentType === 'pptx' ? 'pptx' : 'docx');
$ncPath = "crm2/CRM_Active_Files/Documents/{$moduleFolder}/{$folderName}/{$fileName}.{$fileExtension}";
error_log("=== CREATE DOCUMENT WITH TEXT ===");
error_log("Module: {$module}, RecordId: {$recordId}");
error_log("FileName: {$fileName}");
error_log("DocumentType: {$documentType}");
error_log("Text length: " . strlen($documentText));
// СОЗДАЕМ ДОКУМЕНТ С ТЕКСТОМ
try {
$documentContent = createDocumentWithText($documentText, $documentType);
if (empty($documentContent)) {
throw new Exception('Не удалось создать документ');
}
error_log("✅ Document created (" . strlen($documentContent) . " bytes)");
} catch (Exception $e) {
error_log("Failed to create document: " . $e->getMessage());
die(json_encode([
'success' => false,
'error' => 'Ошибка создания документа: ' . $e->getMessage()
]));
}
// СОХРАНЯЕМ В S3
$s3Path = $ncPath; // Путь уже без лишних слешей
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'ru-1',
'endpoint' => 'https://s3.twcstorage.ru',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => EnvLoader::getRequired('S3_ACCESS_KEY'),
'secret' => EnvLoader::getRequired('S3_SECRET_KEY')
],
'suppress_php_deprecation_warning' => true
]);
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
try {
$result = $s3Client->putObject([
'Bucket' => $bucket,
'Key' => $s3Path,
'Body' => $documentContent,
'ContentType' => getContentType($fileExtension)
]);
error_log("✅ File saved to S3: {$s3Path}");
} catch (Exception $e) {
error_log("Failed to save to S3: " . $e->getMessage());
die(json_encode([
'success' => false,
'error' => 'Ошибка сохранения: ' . $e->getMessage()
]));
}
// ПУБЛИКУЕМ СОБЫТИЕ В REDIS
try {
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'crm.clientright.ru',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!'
]);
$event = json_encode([
'type' => 'file_created',
'source' => 'ai_document_generator',
'path' => $s3Path,
'timestamp' => time()
]);
$redis->publish('crm:file:events', $event);
error_log("✅ Published event to Redis");
} catch (Exception $e) {
error_log("Redis publish failed: " . $e->getMessage());
}
// ФОРМИРУЕМ ССЫЛКУ НА РЕДАКТИРОВАНИЕ
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $s3Path;
$editUrl = 'https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=' . urlencode($recordId) . '&fileName=' . urlencode($s3Url);
// ВОЗВРАЩАЕМ РЕЗУЛЬТАТ
echo json_encode([
'success' => true,
'message' => 'Документ создан успешно',
'documentName' => $fileName . '.' . $fileExtension,
'documentUrl' => $s3Url,
'editUrl' => $editUrl,
'path' => $s3Path
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
/**
* Создает документ с текстом
*/
function createDocumentWithText($text, $type) {
if ($type === 'docx') {
return createDocxWithText($text);
} elseif ($type === 'xlsx') {
return createXlsxWithText($text);
} elseif ($type === 'pptx') {
return createPptxWithText($text);
} else {
throw new Exception("Неподдерживаемый тип документа: {$type}");
}
}
/**
* Создает DOCX с текстом и форматированием (поддержка Markdown)
*/
function createDocxWithText($text) {
// Проверяем наличие PHPWord
if (class_exists('\PhpOffice\PhpWord\PhpWord')) {
// Используем PHPWord если доступен
$phpWord = new \PhpOffice\PhpWord\PhpWord();
// Настройки документа
$phpWord->setDefaultFontName('Times New Roman');
$phpWord->setDefaultFontSize(12);
// Добавляем секцию
$section = $phpWord->addSection([
'marginTop' => 1134, // 2 см
'marginRight' => 1134, // 2 см
'marginBottom' => 1134, // 2 см
'marginLeft' => 1701 // 3 см
]);
// Парсим Markdown и создаем форматированный документ
parseMarkdownToPHPWord($section, $text);
// Сохраняем во временный файл
$tempFile = tempnam(sys_get_temp_dir(), 'docx_') . '.docx';
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
$writer->save($tempFile);
// Читаем содержимое
$content = file_get_contents($tempFile);
// Удаляем временный файл
unlink($tempFile);
return $content;
} else {
// Fallback: создаем простой DOCX через ZIP (DOCX это ZIP архив)
return createSimpleDocx($text);
}
}
/**
* Парсит Markdown и добавляет форматированный текст в PHPWord секцию
*
* Поддерживает:
* - Заголовки: # H1, ## H2, ### H3
* - Жирный: **текст** или __текст__
* - Курсив: *текст* или _текст_
* - Маркированные списки: - или *
* - Нумерованные списки: 1. 2. 3.
* - Выделение: `код`
*/
function parseMarkdownToPHPWord($section, $text) {
$lines = explode("\n", $text);
$inList = false;
$listType = null; // 'ul' или 'ol'
$listItems = [];
foreach ($lines as $line) {
$line = rtrim($line);
// Пустая строка
if (empty($line)) {
if ($inList) {
// Завершаем список
addListToSection($section, $listItems, $listType);
$listItems = [];
$inList = false;
$listType = null;
}
$section->addText('');
continue;
}
// Заголовки
if (preg_match('/^(#{1,3})\s+(.+)$/', $line, $matches)) {
if ($inList) {
addListToSection($section, $listItems, $listType);
$listItems = [];
$inList = false;
$listType = null;
}
$level = strlen($matches[1]);
$title = trim($matches[2]);
$fontSize = [1 => 18, 2 => 16, 3 => 14][$level] ?? 14;
$section->addText($title, [
'name' => 'Times New Roman',
'size' => $fontSize,
'bold' => true,
'color' => '000000'
], [
'spaceAfter' => 240,
'spaceBefore' => $level === 1 ? 480 : 240
]);
continue;
}
// Маркированный список
if (preg_match('/^[-*]\s+(.+)$/', $line, $matches)) {
if (!$inList || $listType !== 'ul') {
if ($inList && $listType === 'ol') {
addListToSection($section, $listItems, $listType);
$listItems = [];
}
$inList = true;
$listType = 'ul';
}
$listItems[] = trim($matches[1]);
continue;
}
// Нумерованный список
if (preg_match('/^\d+\.\s+(.+)$/', $line, $matches)) {
if (!$inList || $listType !== 'ol') {
if ($inList && $listType === 'ul') {
addListToSection($section, $listItems, $listType);
$listItems = [];
}
$inList = true;
$listType = 'ol';
}
$listItems[] = trim($matches[1]);
continue;
}
// Обычный текст
if ($inList) {
addListToSection($section, $listItems, $listType);
$listItems = [];
$inList = false;
$listType = null;
}
// Парсим форматирование в строке (жирный, курсив, код)
addFormattedText($section, $line);
}
// Если список не завершен
if ($inList && !empty($listItems)) {
addListToSection($section, $listItems, $listType);
}
}
/**
* Добавляет форматированный текст в секцию
*/
function addFormattedText($section, $text) {
// Парсим inline форматирование: **жирный**, *курсив*, `код`
$textRun = $section->addTextRun(['spaceAfter' => 240]);
// Разбиваем текст на части с форматированием
$parts = preg_split('/(\*\*.*?\*\*|__.*?__|\*.*?\*|_.*?_|`.*?`)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($parts as $part) {
if (empty($part)) continue;
// Жирный **текст** или __текст__
if (preg_match('/^\*\*(.+?)\*\*$/', $part, $m) || preg_match('/^__(.+?)__$/', $part, $m)) {
$textRun->addText($m[1], [
'name' => 'Times New Roman',
'size' => 12,
'bold' => true
]);
}
// Курсив *текст* или _текст_ (но не жирный)
elseif (preg_match('/^\*(.+?)\*$/', $part, $m) || (preg_match('/^_(.+?)_$/', $part, $m) && !preg_match('/^__/', $part))) {
$textRun->addText($m[1], [
'name' => 'Times New Roman',
'size' => 12,
'italic' => true
]);
}
// Код `текст`
elseif (preg_match('/^`(.+?)`$/', $part, $m)) {
$textRun->addText($m[1], [
'name' => 'Courier New',
'size' => 11,
'color' => '0066CC'
]);
}
// Обычный текст
else {
$textRun->addText($part, [
'name' => 'Times New Roman',
'size' => 12
]);
}
}
}
/**
* Добавляет список в секцию
*/
function addListToSection($section, $items, $type) {
if (empty($items)) return;
$fontStyle = [
'name' => 'Times New Roman',
'size' => 12
];
$paragraphStyle = [
'spaceAfter' => 120,
'indentation' => ['left' => 720] // 0.5 дюйма
];
foreach ($items as $index => $item) {
$prefix = $type === 'ol' ? ($index + 1) . '. ' : '• ';
// Парсим форматирование в элементе списка
$textRun = $section->addTextRun($paragraphStyle);
$textRun->addText($prefix, array_merge($fontStyle, ['bold' => true]));
// Добавляем текст элемента с форматированием
$formattedParts = preg_split('/(\*\*.*?\*\*|__.*?__|\*.*?\*|_.*?_|`.*?`)/', $item, -1, PREG_SPLIT_DELIM_CAPTURE);
foreach ($formattedParts as $part) {
if (empty($part)) continue;
if (preg_match('/^\*\*(.+?)\*\*$/', $part, $m) || preg_match('/^__(.+?)__$/', $part, $m)) {
$textRun->addText($m[1], array_merge($fontStyle, ['bold' => true]));
} elseif (preg_match('/^\*(.+?)\*$/', $part, $m) || (preg_match('/^_(.+?)_$/', $part, $m) && !preg_match('/^__/', $part))) {
$textRun->addText($m[1], array_merge($fontStyle, ['italic' => true]));
} elseif (preg_match('/^`(.+?)`$/', $part, $m)) {
$textRun->addText($m[1], array_merge($fontStyle, ['name' => 'Courier New', 'color' => '0066CC']));
} else {
$textRun->addText($part, $fontStyle);
}
}
}
// Пустая строка после списка
$section->addText('');
}
/**
* Создает простой DOCX без PHPWord (через ZIP архив)
*/
function createSimpleDocx($text) {
// Экранируем XML спецсимволы
$text = htmlspecialchars($text, ENT_XML1 | ENT_QUOTES, 'UTF-8');
// Разбиваем на параграфы
$paragraphs = explode("\n", $text);
$paragraphXml = '';
foreach ($paragraphs as $paragraph) {
$paragraph = trim($paragraph);
if (empty($paragraph)) {
$paragraphXml .= '<w:p><w:r><w:t></w:t></w:r></w:p>';
} else {
$paragraphXml .= '<w:p><w:r><w:t>' . $paragraph . '</w:t></w:r></w:p>';
}
}
// Минимальный DOCX структура (ZIP архив с XML файлами)
$tempDir = sys_get_temp_dir() . '/docx_' . uniqid();
mkdir($tempDir, 0755, true);
// [Content_Types].xml
file_put_contents($tempDir . '/[Content_Types].xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>
<Default Extension="xml" ContentType="application/xml"/>
<Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/>
</Types>');
// _rels/.rels
mkdir($tempDir . '/_rels', 0755, true);
file_put_contents($tempDir . '/_rels/.rels', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">
<Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>
</Relationships>');
// word/document.xml
mkdir($tempDir . '/word', 0755, true);
file_put_contents($tempDir . '/word/document.xml', '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">
<w:body>' . $paragraphXml . '</w:body>
</w:document>');
// Создаем ZIP архив
$zipFile = tempnam(sys_get_temp_dir(), 'docx_') . '.docx';
$zip = new ZipArchive();
if ($zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) === TRUE) {
$zip->addFile($tempDir . '/[Content_Types].xml', '[Content_Types].xml');
$zip->addFile($tempDir . '/_rels/.rels', '_rels/.rels');
$zip->addFile($tempDir . '/word/document.xml', 'word/document.xml');
$zip->close();
}
// Читаем содержимое
$content = file_get_contents($zipFile);
// Удаляем временные файлы
unlink($zipFile);
array_map('unlink', glob($tempDir . '/*/*'));
array_map('unlink', glob($tempDir . '/*'));
rmdir($tempDir . '/word');
rmdir($tempDir . '/_rels');
rmdir($tempDir);
return $content;
}
/**
* Создает XLSX с текстом (в первой ячейке)
*/
function createXlsxWithText($text) {
if (class_exists('\PhpOffice\PhpSpreadsheet\Spreadsheet')) {
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Записываем текст в первую ячейку
$sheet->setCellValue('A1', $text);
// Автоподбор ширины колонки
$sheet->getColumnDimension('A')->setAutoSize(true);
// Сохраняем во временный файл
$tempFile = tempnam(sys_get_temp_dir(), 'xlsx_') . '.xlsx';
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($tempFile);
// Читаем содержимое
$content = file_get_contents($tempFile);
// Удаляем временный файл
unlink($tempFile);
return $content;
} else {
throw new Exception('PhpSpreadsheet не установлен. Установите через composer: composer require phpoffice/phpspreadsheet');
}
}
/**
* Создает PPTX с текстом (на первом слайде)
*/
function createPptxWithText($text) {
if (class_exists('\PhpOffice\PhpPresentation\PhpPresentation')) {
$presentation = new \PhpOffice\PhpPresentation\PhpPresentation();
$slide = $presentation->getActiveSlide();
// Создаем текстовую фигуру
$shape = $slide->createRichTextShape()
->setHeight(400)
->setWidth(800)
->setOffsetX(100)
->setOffsetY(100);
// Разбиваем текст на параграфы
$paragraphs = explode("\n", $text);
foreach ($paragraphs as $paragraph) {
$paragraph = trim($paragraph);
if (!empty($paragraph)) {
$shape->createTextRun($paragraph);
$shape->createParagraph();
}
}
// Сохраняем во временный файл
$tempFile = tempnam(sys_get_temp_dir(), 'pptx_') . '.pptx';
$writer = \PhpOffice\PhpPresentation\IOFactory::createWriter($presentation, 'PowerPoint2007');
$writer->save($tempFile);
// Читаем содержимое
$content = file_get_contents($tempFile);
// Удаляем временный файл
unlink($tempFile);
return $content;
} else {
throw new Exception('PhpPresentation не установлен. Установите через composer: composer require phpoffice/phppresentation');
}
}
/**
* Определяет Content-Type для файла
*/
function getContentType($fileType) {
$types = [
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
];
return $types[$fileType] ?? 'application/octet-stream';
}

View File

@@ -0,0 +1,251 @@
<?php
/**
* Создание документа из шаблона Nextcloud
*
* Алгоритм:
* 1. Получаем шаблон из Nextcloud через WebDAV
* 2. Заполняем переменные через PHPWord
* 3. Сохраняем готовый документ в папку проекта
* 4. Открываем в OnlyOffice
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Параметры
$module = $_GET['module'] ?? '';
$recordId = $_GET['recordId'] ?? '';
$recordName = $_GET['recordName'] ?? '';
$fileName = $_GET['fileName'] ?? '';
$templateName = $_GET['templateName'] ?? ''; // Имя шаблона (например, "pretenziya.docx")
$variables = json_decode($_GET['variables'] ?? '{}', true); // Переменные для заполнения
if (empty($module) || empty($recordId) || empty($fileName) || empty($templateName)) {
die(json_encode(['success' => false, 'error' => 'Не указаны обязательные параметры']));
}
// Nextcloud credentials
$nextcloudUrl = 'https://office.clientright.ru:8443';
$username = 'admin';
$password = 'office';
// Определяем папку модуля
$moduleFolders = [
'Project' => 'Project',
'Contacts' => 'Contacts',
'Accounts' => 'Accounts',
'Invoice' => 'Invoice',
'Quotes' => 'Quotes',
'SalesOrder' => 'SalesOrder',
'PurchaseOrder' => 'PurchaseOrder',
'HelpDesk' => 'HelpDesk',
'Leads' => 'Leads',
'Potentials' => 'Potentials'
];
$moduleFolder = $moduleFolders[$module] ?? 'Other';
// Формируем имя папки записи
$recordName = preg_replace('/[\/\\\\:\*\?"<>\|]/', '_', $recordName);
$folderName = $recordName . '_' . $recordId;
// ONLYOFFICE хранит шаблоны в папке /Templates/ в корне пользователя
// Путь к шаблону в Nextcloud
$templatePath = "/Templates/{$templateName}";
$templateWebDAVUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $templatePath;
// Путь к готовому документу
$fileType = pathinfo($templateName, PATHINFO_EXTENSION);
$ncPath = "/crm/crm2/CRM_Active_Files/Documents/{$moduleFolder}/{$folderName}/{$fileName}.{$fileType}";
error_log("=== CREATE FROM TEMPLATE ===");
error_log("Template: {$templateName}");
error_log("Variables: " . json_encode($variables, JSON_UNESCAPED_UNICODE));
error_log("Output path: {$ncPath}");
// 1. СКАЧИВАЕМ ШАБЛОН ИЗ NEXTCLOUD
$ch = curl_init($templateWebDAVUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$templateContent = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 200 || empty($templateContent)) {
die(json_encode(['success' => false, 'error' => "Шаблон не найден: {$templateName}"]));
}
error_log("✅ Template downloaded (" . strlen($templateContent) . " bytes)");
// 2. ЗАПОЛНЯЕМ ПЕРЕМЕННЫЕ В ШАБЛОНЕ
$filledContent = fillTemplateVariables($templateContent, $variables, $fileType);
// 3. СОХРАНЯЕМ В S3
$s3Path = ltrim($ncPath, '/');
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'ru-1',
'endpoint' => 'https://s3.twcstorage.ru',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => EnvLoader::getRequired('S3_ACCESS_KEY'),
'secret' => EnvLoader::getRequired('S3_SECRET_KEY')
],
'suppress_php_deprecation_warning' => true
]);
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
try {
$result = $s3Client->putObject([
'Bucket' => $bucket,
'Key' => $s3Path,
'Body' => $filledContent,
'ContentType' => getContentType($fileType)
]);
error_log("✅ File saved to S3: {$s3Path}");
} catch (Exception $e) {
error_log("Failed to save to S3: " . $e->getMessage());
die(json_encode(['success' => false, 'error' => "Ошибка сохранения: " . $e->getMessage()]));
}
// 4. ПУБЛИКУЕМ СОБЫТИЕ В REDIS
try {
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => 'crm.clientright.ru',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!'
]);
$event = json_encode([
'type' => 'file_created',
'source' => 'crm_template',
'path' => $s3Path,
'timestamp' => time()
]);
$redis->publish('crm:file:events', $event);
error_log("✅ Published event to Redis");
} catch (Exception $e) {
error_log("Redis publish failed: " . $e->getMessage());
}
// 5. ОТКРЫВАЕМ В ONLYOFFICE
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $s3Path;
$redirectUrl = '/crm_extensions/file_storage/api/open_file_v2.php?recordId=' . urlencode($recordId) . '&fileName=' . urlencode($s3Url);
header('Location: ' . $redirectUrl);
exit;
/**
* Заполняет переменные в шаблоне
*
* Поддерживает два формата:
* 1. Простая замена {VARIABLE_NAME} → значение
* 2. PHPWord для сложных документов
*/
function fillTemplateVariables($content, $variables, $fileType) {
if ($fileType === 'docx') {
// Используем PHPWord для DOCX
return fillDocxTemplate($content, $variables);
} else {
// Для других форматов - простая замена
return fillSimpleTemplate($content, $variables);
}
}
/**
* Заполнение DOCX через PHPWord
*/
function fillDocxTemplate($content, $variables) {
// Сохраняем во временный файл
$tempFile = tempnam(sys_get_temp_dir(), 'template_') . '.docx';
file_put_contents($tempFile, $content);
try {
$phpWord = \PhpOffice\PhpWord\IOFactory::load($tempFile);
// Заменяем переменные во всех секциях
foreach ($phpWord->getSections() as $section) {
foreach ($section->getElements() as $element) {
if ($element instanceof \PhpOffice\PhpWord\Element\Text) {
$text = $element->getText();
$text = replaceVariables($text, $variables);
$element->setText($text);
} elseif ($element instanceof \PhpOffice\PhpWord\Element\TextRun) {
foreach ($element->getElements() as $textElement) {
if ($textElement instanceof \PhpOffice\PhpWord\Element\Text) {
$text = $textElement->getText();
$text = replaceVariables($text, $variables);
$textElement->setText($text);
}
}
}
}
}
// Сохраняем результат
$writer = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
$outputFile = tempnam(sys_get_temp_dir(), 'output_') . '.docx';
$writer->save($outputFile);
$result = file_get_contents($outputFile);
// Удаляем временные файлы
unlink($tempFile);
unlink($outputFile);
return $result;
} catch (Exception $e) {
error_log("PHPWord error: " . $e->getMessage());
// Fallback на простую замену
unlink($tempFile);
return fillSimpleTemplate($content, $variables);
}
}
/**
* Простая замена переменных {VAR} → значение
*/
function fillSimpleTemplate($content, $variables) {
foreach ($variables as $key => $value) {
$content = str_replace('{' . strtoupper($key) . '}', $value, $content);
$content = str_replace('{{' . strtoupper($key) . '}}', $value, $content);
}
return $content;
}
/**
* Универсальная замена переменных
*/
function replaceVariables($text, $variables) {
foreach ($variables as $key => $value) {
$text = str_replace('{' . strtoupper($key) . '}', $value, $text);
$text = str_replace('{{' . strtoupper($key) . '}}', $value, $text);
}
return $text;
}
/**
* Определяет Content-Type для файла
*/
function getContentType($fileType) {
$types = [
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
];
return $types[$fileType] ?? 'application/octet-stream';
}

View File

@@ -0,0 +1,233 @@
<?php
/**
* Создание нового файла в Nextcloud
* Создаёт пустой DOCX/XLSX/PPTX и открывает для редактирования
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Параметры
$module = $_GET['module'] ?? '';
$recordId = $_GET['recordId'] ?? '';
$recordName = $_GET['recordName'] ?? '';
$fileName = $_GET['fileName'] ?? '';
$fileType = $_GET['fileType'] ?? 'docx';
if (empty($module) || empty($recordId) || empty($fileName)) {
die("Не указаны обязательные параметры");
}
// Nextcloud credentials
$nextcloudUrl = 'https://office.clientright.ru:8443';
$username = 'admin';
$password = 'office';
// Определяем папку модуля
$moduleFolders = [
'Project' => 'Project',
'Contacts' => 'Contacts',
'Accounts' => 'Accounts',
'Invoice' => 'Invoice',
'Quotes' => 'Quotes',
'SalesOrder' => 'SalesOrder',
'PurchaseOrder' => 'PurchaseOrder',
'HelpDesk' => 'HelpDesk',
'Leads' => 'Leads',
'Potentials' => 'Potentials'
];
$moduleFolder = $moduleFolders[$module] ?? 'Other';
// Формируем имя папки записи
$recordName = preg_replace('/[\/\\\\:\*\?"<>\|]/', '_', $recordName); // Убираем недопустимые символы
$folderName = $recordName . '_' . $recordId;
// Формируем путь к файлу в Nextcloud
$ncPath = "/crm/crm2/CRM_Active_Files/Documents/{$moduleFolder}/{$folderName}/{$fileName}.{$fileType}";
$webdavUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $ncPath;
error_log("=== CREATE NEXTCLOUD FILE ===");
error_log("Module: " . $module);
error_log("Record ID: " . $recordId);
error_log("File name: " . $fileName);
error_log("File type: " . $fileType);
error_log("Nextcloud path: " . $ncPath);
error_log("WebDAV URL: " . $webdavUrl);
// СОЗДАЁМ ФАЙЛ ЧЕРЕЗ NEXTCLOUD OCS API (Direct Editing)
// Используем встроенный API Nextcloud для создания нового файла
$templateMap = [
'docx' => 'onlyoffice',
'xlsx' => 'onlyoffice',
'pptx' => 'onlyoffice'
];
$editorId = $templateMap[$fileType] ?? 'onlyoffice';
// Используем OCS API v2 для создания нового файла
$createUrl = $nextcloudUrl . '/ocs/v2.php/apps/files/api/v1/directEditing/create';
$postData = http_build_query([
'path' => $ncPath,
'editorId' => $editorId,
'templateId' => '',
'templateType' => $fileType
]);
error_log("Creating file via OCS API: " . $createUrl);
error_log("Post data: " . $postData);
// Вызываем API создания
$ch = curl_init($createUrl);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postData);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'OCS-APIRequest: true',
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json'
]);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
error_log("OCS API response code: " . $httpCode);
error_log("OCS API response: " . substr($response, 0, 500));
// Если API сработал - парсим ответ и получаем URL редактора
if ($httpCode === 200) {
$data = json_decode($response, true);
if (isset($data['ocs']['data']['url'])) {
$editorUrl = $data['ocs']['data']['url'];
error_log("Got editor URL from API: " . $editorUrl);
header('Location: ' . $nextcloudUrl . $editorUrl);
exit;
}
}
// Если API не сработал - создаём файл НАПРЯМУЮ В S3 и открываем через OnlyOffice!
error_log("OCS API failed, creating file directly in S3");
// Извлекаем S3 путь из Nextcloud пути
// /crm/crm2/CRM_Active_Files/... → crm2/CRM_Active_Files/...
$s3Path = ltrim($ncPath, '/');
// S3 credentials
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'ru-1',
'endpoint' => 'https://s3.twcstorage.ru',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => EnvLoader::getRequired('S3_ACCESS_KEY'),
'secret' => EnvLoader::getRequired('S3_SECRET_KEY')
],
'suppress_php_deprecation_warning' => true
]);
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
// Создаём минимальный пустой файл
$emptyContent = createEmptyFile($fileType);
error_log("Creating file in S3: " . $s3Path . " (" . strlen($emptyContent) . " bytes)");
// Загружаем файл в S3
try {
$result = $s3Client->putObject([
'Bucket' => $bucket,
'Key' => $s3Path,
'Body' => $emptyContent,
'ContentType' => getContentType($fileType)
]);
error_log("✅ File created in S3!");
} catch (Exception $e) {
error_log("Failed to create file in S3: " . $e->getMessage());
die("Не удалось создать файл в S3: " . $e->getMessage());
}
// Формируем S3 URL
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $s3Path;
error_log("S3 URL: " . $s3Url);
// Публикуем событие в Redis для индексации Nextcloud
try {
// Используем Predis (установлен через composer)
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => '147.45.146.17',
'port' => 6379,
'password' => 'CRM_Redis_Pass_2025_Secure!'
]);
$event = json_encode([
'type' => 'file_created',
'source' => 'crm_create_file',
'path' => $s3Path,
'timestamp' => time()
]);
$redis->publish('crm:file:events', $event);
error_log("✅ Published event to Redis");
} catch (Exception $e) {
error_log("Redis publish failed: " . $e->getMessage());
}
// Открываем файл НАПРЯМУЮ через OnlyOffice (быстро!)
$redirectUrl = '/crm_extensions/file_storage/api/open_file_v2.php?recordId=' . urlencode($recordId) . '&fileName=' . urlencode($s3Url);
error_log("Redirecting to OnlyOffice: " . $redirectUrl);
// Редирект
header('Location: ' . $redirectUrl);
exit;
/**
* Создаёт минимальное пустое содержимое для Office файла
*/
function createEmptyFile($fileType) {
// Используем готовые минимальные шаблоны
$templatePath = __DIR__ . '/../templates/empty.' . $fileType;
if (file_exists($templatePath)) {
$content = file_get_contents($templatePath);
error_log("Using template: " . $templatePath . " (" . strlen($content) . " bytes)");
return $content;
}
error_log("Template not found: " . $templatePath);
// Fallback: пустая строка (не будет работать, но хотя бы не упадёт)
return '';
}
/**
* Определяет Content-Type для файла
*/
function getContentType($fileType) {
$types = [
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
'doc' => 'application/msword',
'xls' => 'application/vnd.ms-excel',
'ppt' => 'application/vnd.ms-powerpoint'
];
return $types[$fileType] ?? 'application/octet-stream';
}
?>

View File

@@ -0,0 +1,23 @@
<?php
/**
* ПРОСТОЙ Прокси для скачивания файла из S3
* Просто отдаём публичный URL - S3 сам разберётся
*/
$s3Path = isset($_GET['path']) ? $_GET['path'] : '';
if (empty($s3Path)) {
http_response_code(400);
die('Missing path parameter');
}
// Формируем публичный S3 URL
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $s3Path;
error_log("download_s3_file.php: Redirecting to: $s3Url");
// РЕДИРЕКТИМ на S3 напрямую
header('Location: ' . $s3Url);
exit;
?>

View File

@@ -0,0 +1,40 @@
<?php
/**
* Быстрая индексация одного файла в Nextcloud
*/
header('Content-Type: application/json');
$input = json_decode(file_get_contents('php://input'), true);
$path = $input['path'] ?? '';
if (empty($path)) {
echo json_encode(['success' => false, 'error' => 'Missing path']);
exit;
}
// Формируем Nextcloud путь
$ncPath = '/admin/files/crm/' . $path;
// Запускаем сканирование
$command = "docker exec -u www-data nextcloud-fresh php occ files:scan --path=" . escapeshellarg($ncPath) . " 2>&1";
exec($command, $output, $returnCode);
if ($returnCode === 0) {
echo json_encode([
'success' => true,
'message' => 'File indexed successfully',
'output' => implode("\n", $output)
]);
} else {
echo json_encode([
'success' => false,
'error' => 'Indexing failed',
'output' => implode("\n", $output)
]);
}
?>

View File

@@ -0,0 +1,143 @@
<?php
/**
* Получение списка шаблонов из Nextcloud через WebDAV
*
* Использует WebDAV PROPFIND вместо несуществующего API endpoint
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
$nextcloudUrl = 'https://office.clientright.ru:8443';
$username = 'admin';
$password = 'office';
// ONLYOFFICE хранит шаблоны в папке /Templates/ в корне пользователя
$templatesPath = '/Templates/';
$webdavUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $templatesPath;
error_log("=== LIST TEMPLATES ===");
error_log("WebDAV URL: {$webdavUrl}");
// PROPFIND запрос для получения списка файлов
$propfindXml = '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:">
<d:prop>
<d:displayname/>
<d:getcontenttype/>
<d:getcontentlength/>
<d:getlastmodified/>
</d:prop>
</d:propfind>';
$ch = curl_init($webdavUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Depth: 1',
'Content-Type: application/xml'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, $propfindXml);
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
error_log("HTTP Code: {$httpCode}");
error_log("Response length: " . strlen($response));
if ($httpCode === 404) {
echo json_encode([
'success' => false,
'error' => 'Папка Templates не найдена. Создайте папку /crm/Templates/ в Nextcloud и загрузите туда шаблоны.',
'templates' => []
]);
exit;
}
if ($httpCode !== 207) { // 207 Multi-Status для PROPFIND
echo json_encode([
'success' => false,
'error' => "Ошибка WebDAV: HTTP {$httpCode}" . ($error ? " - {$error}" : ''),
'templates' => []
]);
exit;
}
// Парсим XML ответ
libxml_use_internal_errors(true);
$xml = @simplexml_load_string($response);
if ($xml === false) {
$errors = libxml_get_errors();
libxml_clear_errors();
error_log("XML Parse Error: " . print_r($errors, true));
echo json_encode([
'success' => false,
'error' => 'Ошибка парсинга XML ответа',
'templates' => []
]);
exit;
}
// Регистрируем namespace для XPath
$xml->registerXPathNamespace('d', 'DAV:');
$templates = [];
$basePath = rtrim($webdavUrl, '/');
foreach ($xml->xpath('//d:response') as $response) {
$href = (string)$response->xpath('.//d:href')[0];
$displayName = (string)$response->xpath('.//d:displayname')[0];
$contentType = (string)($response->xpath('.//d:getcontenttype')[0] ?? '');
$contentLength = (string)($response->xpath('.//d:getcontentlength')[0] ?? '0');
$lastModified = (string)($response->xpath('.//d:getlastmodified')[0] ?? '');
// Пропускаем саму папку
if (rtrim($href, '/') === $basePath) {
continue;
}
// Пропускаем подпапки (если есть)
if (empty($contentType)) {
continue;
}
// Только Office файлы
$isOfficeFile = (
strpos($contentType, 'officedocument') !== false ||
strpos($contentType, 'msword') !== false ||
strpos($contentType, 'spreadsheet') !== false ||
strpos($contentType, 'presentation') !== false ||
strpos($contentType, 'opendocument') !== false
);
if ($isOfficeFile) {
// Извлекаем имя файла из пути
$fileName = basename($href);
$templates[] = [
'name' => $displayName ?: $fileName,
'fileName' => $fileName,
'path' => $href,
'type' => $contentType,
'size' => (int)$contentLength,
'modified' => $lastModified
];
}
}
error_log("Found " . count($templates) . " templates");
echo json_encode([
'success' => true,
'templates' => $templates,
'count' => count($templates)
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);

View File

@@ -0,0 +1,110 @@
<?php
/**
* Открытие файла через Nextcloud (РАБОЧАЯ ВЕРСИЯ v2)
* Использует Redis индекс для быстрого получения FileID
*/
// Отключаем вывод ошибок в браузер
error_reporting(E_ALL);
ini_set('display_errors', 0);
// Отключаем buffering
if (ob_get_level()) ob_end_clean();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, must-revalidate');
$recordId = isset($_GET['recordId']) ? (int)$_GET['recordId'] : 0;
if ($recordId <= 0) {
echo json_encode(['success' => false, 'error' => 'Invalid recordId']);
exit;
}
try {
// 1. Получаем filename из БД
$db = new mysqli('localhost', 'ci20465_72new', 'EcY979Rn', 'ci20465_72new');
if ($db->connect_error) {
throw new Exception('DB connection failed');
}
$db->set_charset('utf8mb4');
$stmt = $db->prepare("SELECT filename FROM vtiger_notes WHERE notesid = ?");
$stmt->bind_param('i', $recordId);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$db->close();
if (!$row || empty($row['filename'])) {
throw new Exception('File not found in DB');
}
$fileName = $row['filename'];
// 2. Извлекаем S3 путь из URL
$bucketId = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$fileName = rawurldecode($fileName);
$pos = strpos($fileName, $bucketId . '/');
if ($pos === false) {
throw new Exception('Invalid S3 path in filename');
}
$s3Path = substr($fileName, $pos + strlen($bucketId) + 1);
// 3. Получаем FileID из Redis
$redis = new Redis();
if (!$redis->connect('crm.clientright.ru', 6379)) {
throw new Exception('Redis connection failed');
}
$redis->auth('CRM_Redis_Pass_2025_Secure!');
$redisKey = "crm:nc:fileid:" . $s3Path;
$cached = $redis->get($redisKey);
if (!$cached) {
$redis->close();
throw new Exception('FileID not found in Redis index. Key: ' . substr($redisKey, 0, 100));
}
$data = json_decode($cached, true);
$fileId = $data['fileId'] ?? null;
$redis->close();
if (!$fileId) {
throw new Exception('Invalid FileID data in Redis');
}
// 4. Формируем URL для Nextcloud
$nextcloudUrl = 'https://office.clientright.ru:8443';
$ncPath = '/crm/' . $s3Path;
$dirPath = dirname($ncPath);
$redirectUrl = $nextcloudUrl . '/apps/files/files/' . $fileId . '?dir=' . urlencode($dirPath) . '&openfile=true';
// 5. Возвращаем успешный ответ
echo json_encode([
'success' => true,
'fileId' => $fileId,
'redirectUrl' => $redirectUrl,
'source' => 'redis',
'recordId' => $recordId
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
'recordId' => $recordId
]);
}
exit;
?>

View File

@@ -0,0 +1,112 @@
<?php
/**
* Открытие файла через Nextcloud (РАБОЧАЯ ВЕРСИЯ v2)
* Использует Redis индекс для быстрого получения FileID
*/
// Отключаем вывод ошибок в браузер
error_reporting(E_ALL);
ini_set('display_errors', 0);
// Отключаем buffering
if (ob_get_level()) ob_end_clean();
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, must-revalidate');
$recordId = isset($_GET['recordId']) ? (int)$_GET['recordId'] : 0;
if ($recordId <= 0) {
echo json_encode(['success' => false, 'error' => 'Invalid recordId']);
exit;
}
try {
// 1. Получаем filename из БД
$db = new mysqli('localhost', 'ci20465_72new', 'EcY979Rn', 'ci20465_72new');
if ($db->connect_error) {
throw new Exception('DB connection failed');
}
$db->set_charset('utf8mb4');
$stmt = $db->prepare("SELECT filename FROM vtiger_notes WHERE notesid = ?");
$stmt->bind_param('i', $recordId);
$stmt->execute();
$result = $stmt->get_result();
$row = $result->fetch_assoc();
$db->close();
if (!$row || empty($row['filename'])) {
throw new Exception('File not found in DB');
}
$fileName = $row['filename'];
// 2. Извлекаем S3 путь из URL
$bucketId = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$fileName = rawurldecode($fileName);
$pos = strpos($fileName, $bucketId . '/');
if ($pos === false) {
throw new Exception('Invalid S3 path in filename');
}
$s3Path = substr($fileName, $pos + strlen($bucketId) + 1);
// 3. Получаем FileID из Redis
$redis = new Redis();
if (!$redis->connect('crm.clientright.ru', 6379)) {
throw new Exception('Redis connection failed');
}
$redis->auth('CRM_Redis_Pass_2025_Secure!');
$redisKey = "crm:nc:fileid:" . $s3Path;
$cached = $redis->get($redisKey);
if (!$cached) {
$redis->close();
throw new Exception('FileID not found in Redis index. Key: ' . substr($redisKey, 0, 100));
}
$data = json_decode($cached, true);
$fileId = $data['fileId'] ?? null;
$redis->close();
if (!$fileId) {
throw new Exception('Invalid FileID data in Redis');
}
// 4. Формируем URL для Nextcloud
$nextcloudUrl = 'https://office.clientright.ru:8443';
$ncPath = '/crm/' . $s3Path;
$dirPath = dirname($ncPath);
$redirectUrl = $nextcloudUrl . '/apps/files/files/' . $fileId . '?dir=' . urlencode($dirPath) . '&openfile=true';
// 5. Возвращаем успешный ответ
echo json_encode([
'success' => true,
'fileId' => $fileId,
'redirectUrl' => $redirectUrl,
'source' => 'redis',
'recordId' => $recordId
]);
} catch (Exception $e) {
http_response_code(500);
echo json_encode([
'success' => false,
'error' => $e->getMessage(),
'recordId' => $recordId
]);
}
exit;
?>

View File

@@ -0,0 +1,130 @@
<?php
/**
* OnlyOffice Callback для сохранения файлов в S3
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/onlyoffice_callback.log');
// Логируем все запросы
$input = file_get_contents('php://input');
$data = json_decode($input, true);
error_log("=== ONLYOFFICE CALLBACK ===");
error_log("Method: " . $_SERVER['REQUEST_METHOD']);
error_log("Body: " . $input);
// OnlyOffice отправляет POST с JSON данными
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($data)) {
$status = $data['status'] ?? 0;
$key = $data['key'] ?? 'unknown';
error_log("Callback Status: $status, Key: $key");
// Status 2 = файл сохранён, нужно скачать и загрузить в S3
if ($status == 2 && isset($data['url'])) {
$downloadUrl = $data['url'];
error_log("File saved! Download URL: " . $downloadUrl);
try {
// Скачиваем изменённый файл от OnlyOffice
$fileContent = file_get_contents($downloadUrl);
if ($fileContent === false) {
error_log("Failed to download file from OnlyOffice");
http_response_code(500);
echo json_encode(['error' => 1]);
exit;
}
error_log("Downloaded file: " . strlen($fileContent) . " bytes");
// Получаем оригинальный путь файла из query параметра
$s3Path = $_GET['s3Path'] ?? null;
if (!$s3Path) {
error_log("ERROR: s3Path not provided in callback URL!");
// Fallback: сохраняем во временную папку
$s3Path = 'onlyoffice_saved/' . $key . '_' . date('Y-m-d_H-i-s') . '.docx';
error_log("Using fallback path: " . $s3Path);
} else {
error_log("Saving to original path: " . $s3Path);
}
// Инициализируем S3 клиент
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'ru-1',
'endpoint' => 'https://s3.twcstorage.ru',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => EnvLoader::getRequired('S3_ACCESS_KEY'),
'secret' => EnvLoader::getRequired('S3_SECRET_KEY')
],
'suppress_php_deprecation_warning' => true
]);
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
// Сохраняем в ОРИГИНАЛЬНОЕ место!
$savedPath = $s3Path;
// Определяем Content-Type на основе расширения файла
$contentType = getContentType($savedPath);
error_log("Content-Type: " . $contentType);
$result = $s3Client->putObject([
'Bucket' => $bucket,
'Key' => $savedPath,
'Body' => $fileContent,
'ContentType' => $contentType
]);
error_log("File saved to S3: " . $savedPath);
error_log("S3 Response: " . json_encode($result->toArray()));
http_response_code(200);
echo json_encode(['error' => 0]);
exit;
} catch (Exception $e) {
error_log("Error saving file to S3: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 1, 'message' => $e->getMessage()]);
exit;
}
}
// Другие статусы (1 = открыт, 4 = закрыт и т.д.)
http_response_code(200);
echo json_encode(['error' => 0]);
exit;
}
// Для всех остальных запросов - 200 OK
http_response_code(200);
echo json_encode(['error' => 0]);
/**
* Определяет Content-Type на основе расширения файла
*/
function getContentType($filename) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$types = [
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
];
return $types[$ext] ?? 'application/octet-stream';
}
?>

View File

@@ -0,0 +1,121 @@
<?php
/**
* OnlyOffice Callback v2 с сохранением в оригинальный файл S3
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 0);
$input = file_get_contents('php://input');
$data = json_decode($input, true);
error_log("=== ONLYOFFICE CALLBACK V2 ===");
error_log("Body: " . $input);
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($data)) {
$status = $data['status'] ?? 0;
$key = $data['key'] ?? 'unknown';
error_log("Status: $status, Key: $key");
// Status 2 = файл сохранён, нужно скачать и загрузить в S3
if ($status == 2 && isset($data['url'])) {
$downloadUrl = $data['url'];
error_log("File saved! Downloading from: " . $downloadUrl);
try {
// Подключаемся к Redis чтобы узнать оригинальный путь
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => EnvLoader::getRequired('REDIS_HOST'),
'port' => (int)EnvLoader::getRequired('REDIS_PORT'),
'password' => EnvLoader::getRequired('REDIS_PASSWORD')
]);
// Получаем маппинг
$mapping = $redis->get("crm:onlyoffice:key:$key");
if (!$mapping) {
error_log("ERROR: No mapping found for key $key");
// Сохраняем в резервную папку
$s3Path = 'onlyoffice_saved/' . $key . '_' . date('Y-m-d_H-i-s') . '.docx';
} else {
$mappingData = json_decode($mapping, true);
$s3Path = $mappingData['s3_path'];
error_log("Found mapping: $key$s3Path");
}
// Скачиваем файл от OnlyOffice
$fileContent = file_get_contents($downloadUrl);
if ($fileContent === false) {
throw new Exception("Failed to download file from OnlyOffice");
}
error_log("Downloaded: " . strlen($fileContent) . " bytes");
// Инициализируем S3 клиент
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'ru-1',
'endpoint' => 'https://s3.twcstorage.ru',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => EnvLoader::getRequired('S3_ACCESS_KEY'),
'secret' => EnvLoader::getRequired('S3_SECRET_KEY')
],
'suppress_php_deprecation_warning' => true
]);
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
// Загружаем в S3 (ПЕРЕЗАПИСЫВАЕМ оригинальный файл!)
$result = $s3Client->putObject([
'Bucket' => $bucket,
'Key' => $s3Path,
'Body' => $fileContent,
'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'Metadata' => [
'saved_by' => 'onlyoffice',
'saved_at' => date('Y-m-d H:i:s')
]
]);
error_log("✅ File saved to S3: $s3Path");
// Публикуем событие в Redis
$redis->publish('crm:file:events', json_encode([
'type' => 'file_modified',
'source' => 'onlyoffice',
'timestamp' => date('c'),
'path' => $s3Path,
'size' => strlen($fileContent),
'action' => 'updated_via_onlyoffice'
]));
http_response_code(200);
echo json_encode(['error' => 0]);
exit;
} catch (Exception $e) {
error_log("ERROR: " . $e->getMessage());
http_response_code(500);
echo json_encode(['error' => 1, 'message' => $e->getMessage()]);
exit;
}
}
// Другие статусы
http_response_code(200);
echo json_encode(['error' => 0]);
exit;
}
http_response_code(200);
echo json_encode(['error' => 0]);
?>

View File

@@ -1,110 +1,237 @@
<?php
/**
* Простой редирект на файл в Nextcloud БЕЗ CSRF проверок
* Использует FilePathManager для новой структуры файлов
* ФИНАЛ: OnlyOffice + Pre-signed S3 URL
* Теперь с CORS и правильными настройками!
*/
// Включаем отображение ошибок
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 1);
// Подключаем конфигурацию и FilePathManager
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/file_storage/FilePathManager.php';
// Получаем параметры
$fileName = isset($_GET['fileName']) ? $_GET['fileName'] : '';
$recordId = isset($_GET['recordId']) ? $_GET['recordId'] : '';
// Если fileName содержит полный URL S3, извлекаем путь к файлу
$ncPath = '';
if (empty($fileName)) {
die("❌ fileName не указан");
}
// Извлекаем S3 путь
$s3Path = '';
if (strpos($fileName, 'http') === 0) {
// Декодируем URL
$fileName = urldecode($fileName);
// Извлекаем путь после bucket ID
// Формат: https://s3.twcstorage.ru/BUCKET_ID/crm2/CRM_Active_Files/...
$bucketId = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$pos = strpos($fileName, $bucketId . '/');
if ($pos !== false) {
$s3Path = substr($fileName, $pos + strlen($bucketId) + 1);
// Nextcloud путь = /crm/ + s3_path
$ncPath = '/crm/' . $s3Path;
}
}
if (empty($ncPath)) {
die(" Ошибка: Не удалось извлечь путь из URL: $fileName");
if (empty($s3Path)) {
die("Не удалось извлечь путь из URL");
}
// Настройки Nextcloud
$nextcloudUrl = 'https://office.clientright.ru:8443';
$username = 'admin';
$password = 'office';
// Извлекаем расширение файла
$ext = strtolower(pathinfo($s3Path, PATHINFO_EXTENSION));
// Вспомогательная функция: кодирование пути по сегментам (WebDAV)
$encodePath = function(array $segments) {
return implode('/', array_map('rawurlencode', $segments));
};
// ПРЯМОЙ S3 URL (bucket публичный, CORS настроен!)
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $s3Path;
// Получаем fileId через WebDAV PROPFIND
$fileId = null;
$propfindUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $ncPath;
// Генерируем версию и ключ документа
$version = time();
// СЛУЧАЙНЫЙ ключ при каждом запросе, чтобы OnlyOffice не использовал кеш!
$documentKey = md5($s3Path . '_' . $version);
error_log("Nextcloud Editor: PROPFIND -> {$propfindUrl}");
// ПРЯМОЙ S3 URL (bucket публичный, поэтому pre-signed URL не нужен!)
// Bucket поддерживает Range requests и CORS из коробки
$fileUrl = $s3Url;
// XML запрос для получения fileid
$xmlRequest = '<?xml version="1.0"?>
<d:propfind xmlns:d="DAV:" xmlns:oc="http://owncloud.org/ns">
<d:prop>
<oc:fileid/>
</d:prop>
</d:propfind>';
// ОТЛАДКА: Логируем все параметры
error_log("=== OPEN FILE DEBUG ===");
error_log("S3 Path: " . $s3Path);
error_log("File URL: " . $fileUrl);
error_log("File extension: " . $ext);
error_log("Document Key (unique): " . $documentKey);
error_log("Version: " . $version);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $propfindUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_USERPWD, $username . ':' . $password);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
curl_setopt($ch, CURLOPT_POSTFIELDS, $xmlRequest);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Depth: 0',
'Content-Type: application/xml'
]);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
$fileBasename = basename($s3Path);
$fileType = getFileType($ext);
$officeFormats = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
if ($response === false) {
error_log("Nextcloud Editor: Ошибка cURL: " . $curlError);
} else {
error_log("Nextcloud Editor: HTTP код: {$httpCode}");
if ($httpCode === 207 && preg_match('/<oc:fileid>(\d+)<\/oc:fileid>/', $response, $matches)) {
$fileId = (int)$matches[1];
error_log("Nextcloud Editor: Получен fileId: {$fileId}");
} else {
error_log("Nextcloud Editor: Файл не найден по пути: {$ncPath} (HTTP {$httpCode})");
}
if (!in_array($ext, $officeFormats)) {
header('Location: ' . $s3Url);
exit;
}
if (!$fileId) {
$errorMsg = "❌ Ошибка: Не удалось получить fileId для файла {$fileName}";
error_log("Nextcloud Editor ERROR: " . $errorMsg);
die($errorMsg);
}
// Формируем URL для Nextcloud
// РАБОЧИЙ ФОРМАТ - редирект на файл с автооткрытием редактора!
$redirectUrl = $nextcloudUrl . '/apps/files/files/' . $fileId . '?dir=/&editing=true&openfile=true';
// Логирование
error_log("Nextcloud Editor: Redirect to $redirectUrl for file (ID: $fileId)");
// Делаем редирект
header('Location: ' . $redirectUrl);
exit;
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title><?php echo htmlspecialchars($fileBasename); ?></title>
<script src="https://office.clientright.ru:9443/web-apps/apps/api/documents/api.js?v=<?php echo time(); ?>"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
margin: 0;
padding: 0;
}
#editor {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="editor"></div>
<script>
// Отладка в консоль
console.log('📁 Файл:', <?php echo json_encode($fileBasename); ?>);
console.log('🔗 S3 URL:', <?php echo json_encode($fileUrl); ?>);
console.log('🔑 Document Key (unique):', <?php echo json_encode($documentKey); ?>);
console.log('✅ Standalone OnlyOffice (9443) + Direct S3 URL!');
new DocsAPI.DocEditor("editor", {
"documentType": "<?php echo $fileType; ?>",
"document": {
"fileType": "<?php echo $ext; ?>",
"key": "<?php echo $documentKey; ?>",
"title": <?php echo json_encode($fileBasename); ?>,
"url": <?php echo json_encode($fileUrl); ?>,
"permissions": {
"comment": true,
"download": true,
"edit": true,
"print": true,
"review": true
}
},
"editorConfig": {
"mode": "edit",
"lang": "ru",
"callbackUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/onlyoffice_callback.php?s3Path=<?php echo urlencode($s3Path); ?>",
"user": {
"id": "user_<?php echo $recordId ?? 'guest'; ?>",
"name": "CRM User"
},
"customization": {
"autosave": true,
"chat": false,
"comments": true,
"compactHeader": false,
"compactToolbar": false,
"help": true,
"hideRightMenu": false,
"logo": {
"image": "https://crm.clientright.ru/layouts/v7/skins/images/logo.png",
"imageEmbedded": "https://crm.clientright.ru/layouts/v7/skins/images/logo.png"
},
"zoom": 100
}
},
"height": "100%",
"width": "100%",
"type": "desktop",
"events": {
"onReady": function() {
console.log('✅ Editor ready!');
},
"onDocumentReady": function() {
console.log('✅ Document loaded!');
},
"onError": function(event) {
console.error('❌ OnlyOffice Error FULL:', JSON.stringify(event, null, 2));
console.error('Event data:', event.data);
console.error('Error code:', event.data.errorCode);
console.error('Error description:', event.data.errorDescription);
// Тестируем доступность URL из браузера
console.log('🧪 Testing S3 URL from browser...');
fetch(<?php echo json_encode($fileUrl); ?>, { method: 'HEAD' })
.then(response => {
console.log('✅ Browser can access S3:', response.status);
})
.catch(error => {
console.error('❌ Browser CANNOT access S3:', error);
});
alert('Ошибка загрузки документа:\n\n' +
'Code: ' + event.data.errorCode + '\n' +
'Description: ' + event.data.errorDescription + '\n\n' +
'Используется Pre-signed URL из S3\n\n' +
'Смотри консоль браузера (F12) для деталей!');
},
"onWarning": function(event) {
console.warn('⚠️ OnlyOffice Warning:', event);
}
}
});
</script>
</body>
</html>
<?php
function getFileType($ext) {
if (in_array($ext, ['doc', 'docx'])) return 'word';
if (in_array($ext, ['xls', 'xlsx'])) return 'cell';
if (in_array($ext, ['ppt', 'pptx'])) return 'slide';
return 'word';
}
function generatePresignedUrl($s3Key, $expirationSeconds) {
try {
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
$s3Client = new Aws\S3\S3Client([
'version' => 'latest',
'region' => 'ru-1',
'endpoint' => 'https://s3.twcstorage.ru',
'use_path_style_endpoint' => true,
'credentials' => [
'key' => EnvLoader::getRequired('S3_ACCESS_KEY'),
'secret' => EnvLoader::getRequired('S3_SECRET_KEY')
],
'suppress_php_deprecation_warning' => true
]);
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
// КЛЮЧ: Минимальные параметры = правильная подпись!
$cmd = $s3Client->getCommand('GetObject', [
'Bucket' => $bucket,
'Key' => $s3Key
]);
$request = $s3Client->createPresignedRequest($cmd, "+{$expirationSeconds} seconds");
return (string)$request->getUri();
} catch (Exception $e) {
error_log("Pre-signed URL error: " . $e->getMessage());
return null;
}
}
function getContentType($filename) {
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
$types = [
'doc' => 'application/msword',
'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls' => 'application/vnd.ms-excel',
'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'ppt' => 'application/vnd.ms-powerpoint',
'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
];
return $types[$ext] ?? 'application/octet-stream';
}
?>

View File

@@ -0,0 +1,58 @@
<?php
/**
* Открытие файлов через Collabora (вместо OnlyOffice)
* Collabora работает через Nextcloud WebDAV
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 1);
$fileName = isset($_GET['fileName']) ? $_GET['fileName'] : '';
$recordId = isset($_GET['recordId']) ? $_GET['recordId'] : '';
if (empty($fileName)) {
die("❌ fileName не указан");
}
// Извлекаем S3 путь
$s3Path = '';
if (strpos($fileName, 'http') === 0) {
$fileName = urldecode($fileName);
$bucketId = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$pos = strpos($fileName, $bucketId . '/');
if ($pos !== false) {
$s3Path = substr($fileName, $pos + strlen($bucketId) + 1);
}
}
if (empty($s3Path)) {
die("Не удалось извлечь путь из URL");
}
// Nextcloud path (убираем 'crm2/')
$ncPath = str_replace('crm2/', '', $s3Path);
$fileBasename = basename($ncPath);
// Nextcloud URL
$nextcloudUrl = 'https://office.clientright.ru:8443';
$webdavPath = '/crm/crm2/' . $ncPath;
// Открываем напрямую в Nextcloud Files - Collabora откроется автоматически!
$dirPath = dirname($webdavPath);
$redirectUrl = $nextcloudUrl . '/apps/files/?dir=' . urlencode($dirPath) . '&openfile=' . urlencode($fileBasename);
error_log("=== COLLABORA OPEN ===");
error_log("S3 Path: " . $s3Path);
error_log("NC WebDAV Path: " . $webdavPath);
error_log("Redirect URL: " . $redirectUrl);
// Редиректим в Nextcloud
header('Location: ' . $redirectUrl);
exit;
?>

View File

@@ -0,0 +1,169 @@
<?php
/**
* OnlyOffice Standalone + S3 Direct URL
* С сохранением обратно в S3
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 1);
$fileName = isset($_GET['fileName']) ? $_GET['fileName'] : '';
$recordId = isset($_GET['recordId']) ? $_GET['recordId'] : 'unknown';
if (empty($fileName)) {
die("❌ fileName не указан");
}
// Извлекаем S3 путь
$s3Path = '';
if (strpos($fileName, 'http') === 0) {
$fileName = urldecode($fileName);
$bucketId = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$pos = strpos($fileName, $bucketId . '/');
if ($pos !== false) {
$s3Path = substr($fileName, $pos + strlen($bucketId) + 1);
}
}
if (empty($s3Path)) {
die("Не удалось извлечь путь из URL");
}
// Извлекаем расширение файла
$ext = strtolower(pathinfo($s3Path, PATHINFO_EXTENSION));
// ПРЯМОЙ S3 URL (bucket публичный)
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $s3Path;
// Генерируем уникальный ключ документа
$version = time();
$documentKey = md5($s3Path . '_' . $version);
// ВАЖНО: Сохраняем маппинг documentKey → S3 путь в Redis
try {
$redis = new Predis\Client([
'scheme' => 'tcp',
'host' => EnvLoader::getRequired('REDIS_HOST'),
'port' => (int)EnvLoader::getRequired('REDIS_PORT'),
'password' => EnvLoader::getRequired('REDIS_PASSWORD')
]);
// Сохраняем на 24 часа (TTL = срок действия документа)
$redis->setex("crm:onlyoffice:key:$documentKey", 86400, json_encode([
's3_path' => $s3Path,
'record_id' => $recordId,
'created_at' => time()
]));
error_log("Redis: Saved mapping for key $documentKey$s3Path");
} catch (Exception $e) {
error_log("Redis error: " . $e->getMessage());
}
$fileBasename = basename($s3Path);
$fileType = getFileType($ext);
$officeFormats = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
if (!in_array($ext, $officeFormats)) {
header('Location: ' . $s3Url);
exit;
}
?>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
<title><?php echo htmlspecialchars($fileBasename); ?></title>
<script src="https://office.clientright.ru:9443/web-apps/apps/api/documents/api.js?v=<?php echo time(); ?>"></script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
#editor {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="editor"></div>
<script>
console.log('📁 Файл:', <?php echo json_encode($fileBasename); ?>);
console.log('🔗 S3 URL:', <?php echo json_encode($s3Url); ?>);
console.log('🔑 Document Key:', <?php echo json_encode($documentKey); ?>);
console.log('✅ Standalone OnlyOffice + Redis mapping!');
new DocsAPI.DocEditor("editor", {
"documentType": "<?php echo $fileType; ?>",
"document": {
"fileType": "<?php echo $ext; ?>",
"key": "<?php echo $documentKey; ?>",
"title": <?php echo json_encode($fileBasename); ?>,
"url": <?php echo json_encode($s3Url); ?>,
"permissions": {
"comment": true,
"download": true,
"edit": true,
"print": true,
"review": true
}
},
"editorConfig": {
"mode": "edit",
"lang": "ru",
"callbackUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/onlyoffice_callback_v2.php",
"user": {
"id": "user_<?php echo $recordId; ?>",
"name": "CRM User"
},
"customization": {
"autosave": true,
"compactHeader": false,
"compactToolbar": false
}
},
"height": "100%",
"width": "100%",
"type": "desktop",
"events": {
"onReady": function() {
console.log('✅ Editor ready!');
},
"onDocumentReady": function() {
console.log('✅ Document loaded!');
},
"onError": function(event) {
console.error('❌ Error:', event.data);
alert('Ошибка: ' + event.data.errorDescription);
}
}
});
</script>
</body>
</html>
<?php
function getFileType($ext) {
if (in_array($ext, ['doc', 'docx'])) return 'word';
if (in_array($ext, ['xls', 'xlsx'])) return 'cell';
if (in_array($ext, ['ppt', 'pptx'])) return 'slide';
return 'word';
}
?>

View File

@@ -0,0 +1,121 @@
<?php
/**
* S3 Proxy для OnlyOffice
* Проксирует запросы к S3, чтобы OnlyOffice мог загружать файлы
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
error_reporting(E_ALL);
ini_set('display_errors', 0);
$path = isset($_GET['path']) ? $_GET['path'] : '';
if (empty($path)) {
http_response_code(400);
die('Path parameter is required');
}
// CORS preflight
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, HEAD, OPTIONS');
header('Access-Control-Allow-Headers: *');
header('Access-Control-Max-Age: 3600');
http_response_code(200);
exit;
}
// Для HEAD запросов - только headers, без body
$isHeadRequest = ($_SERVER['REQUEST_METHOD'] === 'HEAD');
// Формируем URL к S3
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$s3Url = 'https://s3.twcstorage.ru/' . $bucket . '/' . $path;
// Проверяем Range header (для OnlyOffice partial requests)
$rangeHeader = isset($_SERVER['HTTP_RANGE']) ? $_SERVER['HTTP_RANGE'] : '';
error_log("S3 Proxy: Request from: " . ($_SERVER['REMOTE_ADDR'] ?? 'unknown'));
error_log("S3 Proxy: Downloading: " . $s3Url);
if ($rangeHeader) {
error_log("S3 Proxy: Range request: " . $rangeHeader);
}
// СНАЧАЛА скачиваем в буфер
$ch = curl_init($s3Url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // ← ВАЖНО: В БУФЕР!
curl_setopt($ch, CURLOPT_HEADER, true); // ← Получаем headers
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 120);
// Для HEAD запросов - только headers
if ($isHeadRequest) {
curl_setopt($ch, CURLOPT_NOBODY, true);
}
// Если есть Range header - передаём его в S3!
if ($rangeHeader) {
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Range: ' . $rangeHeader]);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$error = curl_error($ch);
curl_close($ch);
// Проверяем ПРЕЖДЕ чем отправлять что-либо
// 200 = полный файл, 206 = частичный (Range request)
if ($response === false || ($httpCode !== 200 && $httpCode !== 206)) {
error_log("S3 Proxy ERROR: HTTP $httpCode, cURL error: $error");
header('Access-Control-Allow-Origin: *');
http_response_code($httpCode ?: 500);
die('Failed to fetch file from S3');
}
// Разделяем headers и body
$headersText = substr($response, 0, $headerSize);
$body = $isHeadRequest ? '' : substr($response, $headerSize); // Для HEAD body пустой
// Парсим headers
$headers = explode("\r\n", $headersText);
foreach ($headers as $header) {
if (strpos($header, ':') !== false) {
list($name, $value) = explode(':', $header, 2);
$name = strtolower(trim($name));
$value = trim($value);
// Пробрасываем нужные headers
if (in_array($name, ['content-type', 'content-length', 'content-range', 'accept-ranges', 'etag', 'last-modified'])) {
header($name . ': ' . $value);
}
}
}
// CORS headers
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, HEAD, OPTIONS');
header('Access-Control-Allow-Headers: *');
header('Access-Control-Expose-Headers: Content-Range, Accept-Ranges');
// Устанавливаем правильный HTTP код (206 для partial content)
if ($httpCode === 206) {
http_response_code(206);
} else {
http_response_code(200);
}
// Отправляем body только для GET запросов (не для HEAD)
if (!$isHeadRequest) {
echo $body;
error_log("S3 Proxy: Success! Sent " . strlen($body) . " bytes");
} else {
error_log("S3 Proxy: HEAD request completed");
}
?>

View File

@@ -0,0 +1,36 @@
<?php
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8mb4",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
echo "ПРОВЕРКА ПУТЕЙ ПОСЛЕ ИСПРАВЛЕНИЯ:\n\n";
$sql = "SELECT notesid, s3_key FROM vtiger_notes n
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
WHERE snr.crmid = 396447 AND n.s3_key IS NOT NULL
ORDER BY notesid";
$stmt = $pdo->query($sql);
$docs = $stmt->fetchAll(PDO::FETCH_ASSOC);
$allCorrect = true;
foreach ($docs as $doc) {
$hasPrefix = strpos($doc['s3_key'], 'crm2/CRM_Active_Files') === 0;
$status = $hasPrefix ? '❌ С ПРЕФИКСОМ' : '✅ БЕЗ ПРЕФИКСА';
echo sprintf("ID %-8s | %s\n", $doc['notesid'], $status);
if ($hasPrefix) {
$allCorrect = false;
}
}
echo "\n";
if ($allCorrect) {
echo "ВСЕ ДОКУМЕНТЫ ИМЕЮТ ЕДИНООБРАЗНЫЙ ФОРМАТ ПУТИ!\n";
} else {
echo "⚠️ ЕСТЬ ДОКУМЕНТЫ С ПРЕФИКСОМ\n";
}

View File

@@ -0,0 +1,128 @@
<?php
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$projectId = 396447;
// Получаем информацию о проекте
$sqlProject = "SELECT projectid, projectname, projectstatus FROM vtiger_project WHERE projectid = ?";
$stmtProject = $pdo->prepare($sqlProject);
$stmtProject->execute([$projectId]);
$project = $stmtProject->fetch(PDO::FETCH_ASSOC);
if (!$project) {
die("❌ Проект $projectId не найден!\n");
}
echo "📋 ПРОЕКТ: {$project['projectname']}\n";
echo " ID: {$project['projectid']}\n";
echo " Статус: {$project['projectstatus']}\n";
echo "\n" . str_repeat("=", 80) . "\n\n";
// Получаем документы проекта
$sql = "SELECT
n.notesid,
n.title,
n.filename,
n.filelocationtype,
n.foldername,
n.s3_key,
n.nc_path,
n.filesize,
e.createdtime,
e.modifiedtime,
u.user_name,
e.deleted
FROM vtiger_notes n
INNER JOIN vtiger_crmentity e ON e.crmid = n.notesid
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
LEFT JOIN vtiger_users u ON u.id = e.smownerid
WHERE snr.crmid = ? AND e.deleted = 0
ORDER BY e.createdtime DESC";
$stmt = $pdo->prepare($sql);
$stmt->execute([$projectId]);
$documents = $stmt->fetchAll(PDO::FETCH_ASSOC);
$count = count($documents);
echo "📄 НАЙДЕНО ДОКУМЕНТОВ: $count\n\n";
if ($count == 0) {
echo "⚠️ Документы не найдены!\n";
exit;
}
$totalSize = 0;
$s3Count = 0;
$localCount = 0;
$brokenCount = 0;
foreach ($documents as $i => $doc) {
$num = $i + 1;
echo "[$num] ID: " . ($doc['notesid'] ?? 'N/A') . "\n";
echo " Название: " . ($doc['title'] ?? 'не указано') . "\n";
echo " Файл: " . ($doc['filename'] ?? 'не указано') . "\n";
echo " Расположение: " . ($doc['filelocationtype'] ?: 'не указано') . "\n";
if (!empty($doc['s3_key'])) {
echo " S3 Key: " . $doc['s3_key'] . "\n";
$s3Count++;
}
if (!empty($doc['nc_path'])) {
echo " Nextcloud Path: " . $doc['nc_path'] . "\n";
}
if (!empty($doc['foldername'])) {
echo " Папка CRM: " . $doc['foldername'] . "\n";
}
if (!empty($doc['filesize']) && $doc['filesize'] > 0) {
$sizeKB = round($doc['filesize'] / 1024, 2);
$sizeMB = round($doc['filesize'] / 1024 / 1024, 2);
if ($sizeMB >= 1) {
echo " Размер: {$sizeMB} MB\n";
} else {
echo " Размер: {$sizeKB} KB\n";
}
$totalSize += $doc['filesize'];
} else {
echo " Размер: не указан\n";
}
echo " Создан: " . ($doc['createdtime'] ?? 'не указано') . "\n";
echo " Изменён: " . ($doc['modifiedtime'] ?? 'не указано') . "\n";
if (!empty($doc['user_name'])) {
echo " Владелец: " . $doc['user_name'] . "\n";
}
// Проверка на битые файлы
if (empty($doc['filename']) && empty($doc['s3_key'])) {
echo " ВНИМАНИЕ: Файл без имени и пути!\n";
$brokenCount++;
}
echo "\n";
}
echo str_repeat("=", 80) . "\n";
echo "📊 СТАТИСТИКА:\n";
echo " Всего документов: $count\n";
echo " В S3: $s3Count\n";
echo " Локальных: " . ($count - $s3Count) . "\n";
if ($brokenCount > 0) {
echo " ⚠️ Битых (без файла): $brokenCount\n";
}
if ($totalSize > 0) {
$totalMB = round($totalSize / 1024 / 1024, 2);
echo " Общий размер: {$totalMB} MB\n";
}
echo "\n";

View File

@@ -0,0 +1,39 @@
<?php
/**
* Создание минимальных пустых шаблонов Office файлов
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/vendor/autoload.php';
use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpPresentation\PhpPresentation;
$templatesDir = __DIR__ . '/templates/';
// Создаём Word документ
$phpWord = new PhpWord();
$section = $phpWord->addSection();
$section->addText('');
$objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007');
$objWriter->save($templatesDir . 'empty.docx');
echo "✅ Created empty.docx\n";
// Создаём Excel таблицу
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setCellValue('A1', '');
$writer = new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($spreadsheet);
$writer->save($templatesDir . 'empty.xlsx');
echo "✅ Created empty.xlsx\n";
// Создаём PowerPoint презентацию
$presentation = new PhpPresentation();
$slide = $presentation->getActiveSlide();
$writer = \PhpOffice\PhpPresentation\IOFactory::createWriter($presentation, 'PowerPoint2007');
$writer->save($templatesDir . 'empty.pptx');
echo "✅ Created empty.pptx\n";
echo "\nВсе шаблоны созданы!\n";
?>

View File

@@ -0,0 +1,31 @@
version: '3.8'
services:
onlyoffice-standalone:
image: onlyoffice/documentserver:latest
container_name: onlyoffice-standalone
restart: unless-stopped
ports:
- "127.0.0.1:8083:80" # Новый порт (8081 и 8082 заняты)
environment:
- JWT_ENABLED=false
- JWT_SECRET=
- WOPI_ENABLED=false
volumes:
- onlyoffice-standalone-data:/var/www/onlyoffice/Data
- onlyoffice-standalone-logs:/var/log/onlyoffice
- onlyoffice-standalone-fonts:/usr/share/fonts/truetype/custom
- onlyoffice-standalone-forgotten:/var/lib/onlyoffice/documentserver/App_Data/cache/files/forgotten
networks:
- crm-network
volumes:
onlyoffice-standalone-data:
onlyoffice-standalone-logs:
onlyoffice-standalone-fonts:
onlyoffice-standalone-forgotten:
networks:
crm-network:
name: crm-network

View File

@@ -0,0 +1,440 @@
# 📄 Как AI Ассистент создает документы из шаблонов
**Дата:** 2025-01-XX
**Статус:** ✅ Полное описание процесса
## 🎯 Общий процесс (пошагово)
### ШАГ 1: Пользователь просит создать документ
**Пример запроса:**
```
Пользователь: "Создай претензию по заливу квартиры. Ущерб 400 тысяч рублей,
ответчик УК Жилищник, клиент Иванов Иван Иванович"
```
**Что происходит:**
- Пользователь вводит запрос в AI Drawer
- AI Drawer отправляет запрос в n8n через `/aiassist/n8n_proxy.php`
---
### ШАГ 2: AI Drawer отправляет запрос в n8n
**Код в `ai-drawer-simple.js`:**
```javascript
// Пользователь нажал "Отправить"
sendMessage() {
const message = this.chatInput.value;
this.sendToN8N(message);
}
// Отправка в n8n
async sendToN8N(message) {
const context = this.getCurrentContext(); // Получаем данные проекта из CRM
const response = await fetch('/aiassist/n8n_proxy.php', {
method: 'POST',
body: JSON.stringify({
message: message,
context: context, // { projectId, module, userId, ... }
sessionId: this.sessionId
})
});
const data = await response.json();
// data.task_id - уникальный ID задачи
// Подписываемся на SSE для получения ответа
this.startSSEListener(data.task_id);
}
```
**Что отправляется в n8n:**
```json
{
"message": "Создай претензию по заливу квартиры...",
"context": {
"projectId": "123456",
"module": "Project",
"userId": "42",
"projectName": "Дело Иванова"
},
"sessionId": "ai-drawer-session-1234567890",
"taskId": "task-691209e225894-1762789858",
"redisChannel": "ai:response:task-691209e225894-1762789858"
}
```
---
### ШАГ 3: n8n обрабатывает запрос
**Workflow в n8n:**
```
1. Webhook (получает запрос)
2. AI Node (GPT-4) - анализирует запрос
3. Определение типа документа
4. Поиск данных в CRM (если нужно)
5. Генерация текста документа
6. Формирование переменных для шаблона
7. HTTP Request → вызов API создания документа
8. Публикация результата в Redis
```
**Пример обработки в n8n:**
**3.1. AI анализирует запрос:**
```
Промпт для GPT:
"Пользователь просит создать претензию.
Проанализируй запрос и определи:
- Тип документа (претензия/иск/жалоба)
- Данные клиента
- Данные ответчика
- Сумму ущерба
- Описание ситуации"
```
**Ответ AI:**
```json
{
"document_type": "pretenziya",
"client_name": "Иванов Иван Иванович",
"respondent_name": "УК Жилищник",
"amount": "400000",
"situation": "Залив квартиры от стояка ХВС",
"claim_text": "УК отказывается возмещать ущерб..."
}
```
**3.2. Определение шаблона:**
```javascript
// В n8n workflow
const templateMap = {
'pretenziya': 'pretenziya.docx',
'isk': 'iskovoe_zayavlenie.docx',
'zhaloba': 'zhaloba.docx'
};
const templateName = templateMap[aiResponse.document_type];
// templateName = "pretenziya.docx"
```
**3.3. Формирование переменных:**
```javascript
// В n8n workflow
const variables = {
CLIENT_NAME: aiResponse.client_name,
RESPONDENT_NAME: aiResponse.respondent_name,
DATE: new Date().toLocaleDateString('ru-RU'),
AMOUNT: aiResponse.amount,
CLAIM_TEXT: aiResponse.claim_text,
SITUATION: aiResponse.situation
};
```
---
### ШАГ 4: n8n вызывает API создания документа
**HTTP Request в n8n:**
```javascript
// URL
https://crm.clientright.ru/crm_extensions/file_storage/api/create_from_template.php
// Метод: GET
// Параметры:
{
module: "Project",
recordId: "123456",
recordName: ело_Иванова",
fileName: ретензия_УК_Жилищник",
templateName: "pretenziya.docx",
variables: JSON.stringify({
CLIENT_NAME: "Иванов Иван Иванович",
RESPONDENT_NAME: "УК Жилищник",
DATE: "15.01.2025",
AMOUNT: "400000",
CLAIM_TEXT: "УК отказывается возмещать ущерб..."
})
}
```
**Полный URL:**
```
https://crm.clientright.ru/crm_extensions/file_storage/api/create_from_template.php?
module=Project&
recordId=123456&
recordName=Дело_Иванова&
fileName=Претензия_УК_Жилищник&
templateName=pretenziya.docx&
variables={"CLIENT_NAME":"Иванов Иван Иванович","DATE":"15.01.2025","AMOUNT":"400000",...}
```
---
### ШАГ 5: API создает документ
**Что делает `create_from_template.php`:**
**5.1. Скачивает шаблон из Nextcloud:**
```php
// WebDAV запрос к Nextcloud
$templateWebDAVUrl = 'https://office.clientright.ru:8443/remote.php/dav/files/admin/Templates/pretenziya.docx';
$ch = curl_init($templateWebDAVUrl);
curl_setopt($ch, CURLOPT_USERPWD, "admin:office");
$templateContent = curl_exec($ch);
// Получили содержимое DOCX файла
```
**5.2. Заполняет переменные:**
```php
// Шаблон содержит:
// "Кому: {RESPONDENT_NAME}"
// "От: {CLIENT_NAME}"
// "Сумма: {AMOUNT} рублей"
// PHPWord заменяет переменные:
$filledContent = fillDocxTemplate($templateContent, $variables);
// Результат:
// "Кому: УК Жилищник"
// "От: Иванов Иван Иванович"
// "Сумма: 400000 рублей"
```
**5.3. Сохраняет готовый документ:**
```php
// Сохраняет в S3
$s3Path = "crm2/CRM_Active_Files/Documents/Project/Дело_Иванова_123456/Претензия_УК_Жилищник.docx";
$s3Client->putObject([
'Bucket' => '...',
'Key' => $s3Path,
'Body' => $filledContent
]);
```
**5.4. Открывает документ в OnlyOffice:**
```php
// Редирект на открытие файла
header('Location: /crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=...');
```
---
### ШАГ 6: n8n публикует результат в Redis
**После создания документа:**
```javascript
// В n8n workflow
const result = {
success: true,
message: "Документ создан успешно",
documentUrl: "https://s3.twcstorage.ru/.../Претензия_УК_Жилищник.docx",
documentName: ретензия_УК_Жилищник.docx"
};
// Публикация в Redis
redis.publish('ai:response:task-691209e225894-1762789858', JSON.stringify(result));
```
---
### ШАГ 7: AI Drawer получает ответ
**SSE слушает Redis:**
```javascript
// В ai-drawer-simple.js
startSSEListener(taskId) {
const eventSource = new EventSource(`/aiassist/ai_sse.php?task_id=${taskId}`);
eventSource.addEventListener('response', (event) => {
const data = JSON.parse(event.data);
if (data.success) {
// Показываем сообщение пользователю
this.addMessage('assistant', `✅ Документ создан: ${data.documentName}`);
// Можно добавить кнопку для открытия документа
this.addDocumentLink(data.documentUrl);
}
});
}
```
**Пользователь видит:**
```
✅ Документ создан: Претензия_УК_Жилищник.docx
[Открыть документ] ← кнопка
```
---
## 📊 Визуальная схема процесса
```
┌─────────────┐
│ Пользователь│
│ AI Drawer │
└──────┬──────┘
│ "Создай претензию..."
┌──────────────────┐
│ n8n_proxy.php │
│ (генерирует │
│ task_id) │
└──────┬───────────┘
│ POST {message, context}
┌──────────────────┐
│ n8n Workflow │
│ │
│ 1. AI анализирует│
│ 2. Определяет тип│
│ 3. Генерирует данные│
│ 4. Вызывает API │
└──────┬───────────┘
│ GET /create_from_template.php
┌──────────────────┐
│ create_from_ │
│ template.php │
│ │
│ 1. Скачивает │
│ шаблон │
│ 2. Заполняет │
│ переменные │
│ 3. Сохраняет │
│ в S3 │
│ 4. Открывает │
│ в OnlyOffice │
└──────┬───────────┘
│ Результат
┌──────────────────┐
│ Redis Pub/Sub │
│ ai:response: │
│ {taskId} │
└──────┬───────────┘
│ SSE событие
┌──────────────────┐
│ AI Drawer (SSE) │
│ Показывает │
│ результат │
└──────────────────┘
```
---
## 🔧 Пример полного запроса
### Запрос пользователя:
```
"Создай претензию по заливу квартиры. Ущерб 400 тысяч рублей,
ответчик УК Жилищник, клиент Иванов Иван Иванович"
```
### Что происходит:
1. **AI Drawer → n8n:**
```json
{
"message": "Создай претензию...",
"context": {"projectId": "123456", "module": "Project"}
}
```
2. **n8n → AI (GPT-4):**
```
"Проанализируй запрос и определи тип документа и данные"
```
3. **AI → n8n:**
```json
{
"document_type": "pretenziya",
"client_name": "Иванов Иван Иванович",
"respondent_name": "УК Жилищник",
"amount": "400000"
}
```
4. **n8n → API создания документа:**
```
GET /create_from_template.php?
module=Project&
recordId=123456&
fileName=Претензия_УК_Жилищник&
templateName=pretenziya.docx&
variables={"CLIENT_NAME":"Иванов Иван Иванович",...}
```
5. **API → Nextcloud:**
```
WebDAV GET /Templates/pretenziya.docx
```
6. **API → PHPWord:**
```
Заменяет {CLIENT_NAME} → "Иванов Иван Иванович"
Заменяет {AMOUNT} → "400000"
...
```
7. **API → S3:**
```
PUT crm2/CRM_Active_Files/Documents/Project/.../Претензия_УК_Жилищник.docx
```
8. **n8n → Redis:**
```
PUBLISH ai:response:task-xxx {"success": true, "documentUrl": "..."}
```
9. **SSE → AI Drawer:**
```
Показывает: "✅ Документ создан: Претензия_УК_Жилищник.docx"
```
---
## 💡 Ключевые моменты
1. **AI не создает документ напрямую** - он только анализирует запрос и генерирует данные
2. **n8n координирует процесс** - вызывает API создания документа
3. **API работает с шаблонами** - скачивает, заполняет, сохраняет
4. **Результат возвращается через SSE** - пользователь видит ответ в реальном времени
## 🎯 Преимущества такого подхода
**Разделение ответственности:**
- AI анализирует и генерирует данные
- n8n координирует процесс
- API работает с файлами
**Гибкость:**
- Легко добавить новые типы документов
- Легко изменить шаблоны
- Легко добавить новые источники данных
**Надежность:**
- Каждый компонент можно тестировать отдельно
- Ошибки изолированы
- Легко отлаживать

View File

@@ -0,0 +1,205 @@
# 🛠️ Инструмент для AI: Создание документов
**Дата:** 2025-01-XX
**Статус:** ✅ Готово к использованию
## 🎯 Назначение
Простой инструмент для AI Ассистента, который:
1. Создает пустой DOCX/XLSX/PPTX файл
2. Записывает в него текст, сгенерированный AI
3. Сохраняет в папку проекта
4. Возвращает ссылку на редактирование
## 📍 Endpoint
```
POST /crm_extensions/file_storage/api/create_document_with_text.php
```
## 📥 Параметры запроса
### Обязательные:
- `module` - модуль CRM (Project, Contacts, Accounts, etc.)
- `recordId` - ID записи (проекта, контакта и т.д.)
- `recordName` - название записи (для формирования папки)
- `fileName` - имя создаваемого файла (без расширения)
- `documentText` - текст документа, который нужно записать
### Опциональные:
- `documentType` - тип документа: `docx` (по умолчанию), `xlsx`, `pptx`
## 📤 Ответ
### Успешный ответ:
```json
{
"success": true,
"message": "Документ создан успешно",
"documentName": ретензия_УК_Жилищник.docx",
"documentUrl": "https://s3.twcstorage.ru/.../Претензия_УК_Жилищник.docx",
"editUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=...",
"path": "crm2/CRM_Active_Files/Documents/Project/Дело_Иванова_123456/Претензия_УК_Жилищник.docx"
}
```
### Ошибка:
```json
{
"success": false,
"error": "Не указаны обязательные параметры: module, recordId, fileName, documentText"
}
```
## 🔧 Использование в n8n
### Пример HTTP Request узла в n8n:
**URL:**
```
https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php
```
**Method:** `POST`
**Body (JSON):**
```json
{
"module": "{{ $json.body.context.module }}",
"recordId": "{{ $json.body.context.projectId }}",
"recordName": "{{ $json.body.context.projectName }}",
"fileName": "{{ $json.body.documentName }}",
"documentText": "{{ $json.body.generatedText }}",
"documentType": "docx"
}
```
**Или через Query Parameters (GET):**
```
https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php?
module={{ $json.body.context.module }}&
recordId={{ $json.body.context.projectId }}&
recordName={{ $json.body.context.projectName }}&
fileName={{ $json.body.documentName }}&
documentText={{ $json.body.generatedText }}&
documentType=docx
```
## 📋 Пример полного workflow в n8n
```
1. Webhook (получает запрос от AI Drawer)
2. AI Node (GPT-4) - генерирует текст документа
3. HTTP Request → create_document_with_text.php
Body: {
module: "Project",
recordId: "123456",
recordName: "Дело Иванова",
fileName: "Претензия_УК_Жилищник",
documentText: "ПРЕТЕНЗИЯ\n\nКому: УК Жилищник\nОт: Иванов Иван Иванович\n\n..."
}
4. Получаем ответ:
{
success: true,
documentName: "Претензия_УК_Жилищник.docx",
editUrl: "https://..."
}
5. Формируем сообщение для пользователя:
"✅ Документ создан: Претензия_УК_Жилищник.docx\n[Открыть для редактирования]"
6. Публикуем в Redis: ai:response:{taskId}
```
## 💬 Пример ответа AI пользователю
**После создания документа:**
```
✅ Документ создан: Претензия_УК_Жилищник.docx
Документ сохранен в папку проекта и готов к редактированию.
Вы можете открыть его для просмотра и внесения изменений.
[Открыть документ] ← ссылка на editUrl
```
## 🎨 Форматирование текста
### DOCX:
- Текст разбивается на параграфы по переносам строк (`\n`)
- Каждый параграф отделяется пустой строкой
- Шрифт: Times New Roman, 12pt
- Поля: 2 см сверху/справа/снизу, 3 см слева
### XLSX:
- Весь текст записывается в ячейку A1
- Автоподбор ширины колонки
### PPTX:
- Текст размещается на первом слайде
- Разбивается на параграфы
## 🔍 Примеры использования
### Пример 1: Создание претензии
**Запрос в n8n:**
```json
{
"module": "Project",
"recordId": "123456",
"recordName": "Дело Иванова",
"fileName": ретензия_УК_Жилищник",
"documentText": "ПРЕТЕНЗИЯ\n\nКому: УК \"Жилищник\"\nОт: Иванов Иван Иванович\n\nДата: 15.01.2025\n\nТекст претензии:\nУК отказывается возмещать ущерб от залива квартиры...\n\nТребования:\n1. Возместить ущерб в размере 400000 рублей\n2. Провести экспертизу\n\nС уважением,\nИванов Иван Иванович"
}
```
**Результат:**
- Создан файл ретензия_УК_Жилищник.docx`
- Сохранен в папку проекта
- Возвращена ссылка на редактирование
### Пример 2: Создание иска
**Запрос:**
```json
{
"module": "Project",
"recordId": "123456",
"recordName": "Дело Иванова",
"fileName": сковоеаявление",
"documentText": "ИСКОВОЕ ЗАЯВЛЕНИЕ\n\nВ суд: ...\n\nИстец: Иванов Иван Иванович\nОтветчик: УК \"Жилищник\"\n\n...",
"documentType": "docx"
}
```
## ⚠️ Ограничения
1. **Максимальный размер текста:** Ограничен памятью PHP (обычно 128MB+)
2. **Форматирование:** Базовое форматирование (параграфы, переносы строк)
3. **Таблицы/изображения:** Не поддерживаются в упрощенной версии
## 🚀 Следующие шаги
После MVP можно добавить:
1. Поддержку шаблонов (заполнение переменных)
2. Расширенное форматирование (жирный, курсив, списки)
3. Таблицы и изображения
4. Автоматическое определение типа документа
## 📝 Примечания
- Файл сохраняется в S3
- Событие публикуется в Redis для индексации
- Документ сразу доступен для редактирования в OnlyOffice
- Путь формируется автоматически: `{module}/{recordName}_{recordId}/{fileName}.{ext}`

View File

@@ -0,0 +1,200 @@
# 📄 Инструмент создания документов для AI Ассистента
## Описание
Создает документ (DOCX/XLSX/PPTX) с текстом, сгенерированным AI, и сохраняет его в папку проекта в CRM. Документ сразу доступен для редактирования в OnlyOffice.
**Процесс:**
1. Создает пустой документ выбранного типа (DOCX по умолчанию)
2. Записывает в него текст, сгенерированный AI
3. Сохраняет в S3 в папку проекта: `{module}/{recordName}_{recordId}/{fileName}.{ext}`
4. Публикует событие в Redis для индексации
5. Возвращает ссылку на редактирование в OnlyOffice
**Форматирование:**
- DOCX: текст разбивается на параграфы по переносам строк (`\n`), шрифт Times New Roman 12pt, стандартные поля
- XLSX: весь текст записывается в ячейку A1
- PPTX: текст размещается на первом слайде
## Входные параметры
**URL:** `https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php`
**Method:** `POST`
**Content-Type:** `application/json`
### Обязательные параметры:
- `module` (string) — модуль CRM, где создается документ:
- `"Project"` — для проектов
- `"Contacts"` — для контактов
- `"Accounts"` — для организаций
- `"Invoice"`, `"Quotes"`, `"SalesOrder"`, `"PurchaseOrder"`, `"HelpDesk"`, `"Leads"`, `"Potentials"` — для других модулей
- `recordId` (string) — ID записи в CRM (проекта, контакта и т.д.), к которой привязывается документ
- `recordName` (string) — название записи (используется для формирования имени папки). Спецсимволы будут заменены на подчеркивания
- `fileName` (string) — имя создаваемого файла без расширения (например: `"Претензия_УК_Жилищник"`). Расширение добавится автоматически
- `documentText` (string) — текст документа, который нужно записать. **Поддерживается Markdown форматирование:**
- Заголовки: `# H1`, `## H2`, `### H3`
- Жирный: `**текст**` или `__текст__`
- Курсив: `*текст*` или `_текст_`
- Код: `` `текст` ``
- Маркированные списки: `- пункт` или `* пункт`
- Нумерованные списки: `1. пункт`
- Поддерживаются переносы строк (`\n`) для разделения на параграфы
### Опциональные параметры:
- `documentType` (string, по умолчанию `"docx"`) — тип документа:
- `"docx"` — Word документ (рекомендуется для текстовых документов)
- `"xlsx"` — Excel таблица (для табличных данных)
- `"pptx"` — PowerPoint презентация (для презентаций)
## Что возвращает
### Успешный ответ:
```json
{
"success": true,
"message": "Документ создан успешно",
"documentName": ретензия_УК_Жилищник.docx",
"documentUrl": "https://s3.twcstorage.ru/bucket/path/to/file.docx",
"editUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=https://s3.twcstorage.ru/...",
"path": "crm2/CRM_Active_Files/Documents/Project/Дело_Иванова_123456/Претензия_УК_Жилищник.docx"
}
```
**Поля ответа:**
- `success` (boolean) — `true` если документ создан успешно
- `message` (string) — сообщение о результате
- `documentName` (string) — имя созданного файла с расширением
- `documentUrl` (string) — прямой URL файла в S3
- `editUrl` (string) — URL для открытия документа в OnlyOffice (используй эту ссылку для пользователя)
- `path` (string) — путь к файлу в S3 (для внутреннего использования)
### Ошибка:
```json
{
"success": false,
"error": "Не указаны обязательные параметры: module, recordId, fileName, documentText"
}
```
## Когда использовать
**Используй этот инструмент когда:**
1. Пользователь просит создать документ (претензию, иск, жалобу, ходатайство и т.д.)
2. Ты уже сгенерировал текст документа и готов его сохранить
3. Нужно сохранить документ в папку проекта в CRM
4. Пользователь должен иметь возможность редактировать документ в OnlyOffice
**Примеры запросов пользователя:**
- "Создай претензию по заливу квартиры"
- "Подготовь исковое заявление"
- "Сформируй жалобу в прокуратуру"
- "Напиши ходатайство о приостановлении дела"
**Не используй когда:**
- Пользователь просто спрашивает информацию (без создания документа)
- Нужно только показать текст без сохранения
- Документ должен быть создан из шаблона с переменными (используй другой инструмент)
## Пример использования
### Запрос пользователя:
```
"Создай претензию по заливу квартиры. Ущерб 400 тысяч рублей,
ответчик УК Жилищник, клиент Иванов Иван Иванович"
```
### Твой ответ (после генерации текста):
**1. Вызываешь инструмент:**
```json
POST /crm_extensions/file_storage/api/create_document_with_text.php
{
"module": "Project",
"recordId": "123456",
"recordName": "Дело Иванова",
"fileName": ретензия_УК_Жилищник",
"documentText": "# ПРЕТЕНЗИЯ\n\nКому: **УК \\\"Жилищник\\\"**\nОт: *Иванов Иван Иванович*\n\nДата: 15.01.2025\n\n## Текст претензии\n\nУК отказывается возмещать ущерб от залива квартиры от стояка ХВС. Ущерб составляет `400000` рублей.\n\n## Требования:\n\n1. Возместить ущерб в размере **400000 рублей**\n2. Провести экспертизу для оценки ущерба\n\nС уважением,\n**Иванов Иван Иванович**"
}
```
**2. Получаешь ответ:**
```json
{
"success": true,
"documentName": ретензия_УК_Жилищник.docx",
"editUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=..."
}
```
**3. Сообщаешь пользователю:**
```
✅ Документ создан: Претензия_УК_Жилищник.docx
Документ сохранен в папку проекта и готов к редактированию.
Вы можете открыть его для просмотра и внесения изменений.
[Открыть документ](editUrl)
```
## Важные замечания
1. **Всегда используй `editUrl`** для ссылки пользователю — это откроет документ в OnlyOffice
2. **Имя файла должно быть уникальным** — если файл с таким именем уже существует, он будет перезаписан
3. **Текст документа** должен быть готовым к использованию — инструмент не редактирует текст, только записывает его
4. **Переносы строк** (`\n`) в `documentText` создают новые параграфы в DOCX
5. **Документ сохраняется сразу** — откатить операцию нельзя, убедись что данные корректны
## Поддержка форматирования
**API поддерживает Markdown форматирование:**
-**Заголовки**: `# H1`, `## H2`, `### H3` — автоматически форматируются как заголовки
-**Жирный текст**: `**текст**` или `__текст__` — выделение важной информации
-**Курсив**: `*текст*` или `_текст_` — акценты
-**Код**: `` `текст` `` — статьи, суммы, технические данные (Courier New, синий)
-**Маркированные списки**: `- пункт` или `* пункт` — автоматические отступы
-**Нумерованные списки**: `1. пункт` — автоматическая нумерация
**Пример использования:**
```markdown
# ПРЕТЕНЗИЯ
Кому: **УК "Жилищник"**
От: *Иванов Иван Иванович*
## Требования:
1. Возместить ущерб **400000 рублей**
2. Провести экспертизу
- Дополнительно
- Еще пункт
```
## Ограничения
- Максимальный размер текста ограничен памятью PHP (обычно 128MB+)
- Таблицы не поддерживаются (можно использовать списки)
- Изображения не поддерживаются
- Вложенные списки не поддерживаются (только один уровень)
- Ссылки не поддерживаются (можно использовать код `` `текст` ``)
## Следующие шаги после создания
После создания документа пользователь может:
1. Открыть документ по ссылке `editUrl` в OnlyOffice
2. Редактировать текст, форматирование, добавлять таблицы и изображения
3. Сохранить изменения (автоматически сохраняется в S3)
4. Экспортировать в PDF через OnlyOffice

View File

@@ -0,0 +1,252 @@
# 📝 Поддержка форматирования Markdown в документах
**Дата:** 2025-01-XX
**Статус:** ✅ Реализовано
## 🎯 Обзор
API создания документов теперь поддерживает **Markdown форматирование**! AI может использовать стандартный Markdown синтаксис для создания красиво оформленных документов.
## ✨ Поддерживаемые элементы форматирования
### 1. Заголовки
```markdown
# Заголовок 1 уровня (H1) - размер 18pt, жирный
## Заголовок 2 уровня (H2) - размер 16pt, жирный
### Заголовок 3 уровня (H3) - размер 14pt, жирный
```
**Пример:**
```markdown
# ПРЕТЕНЗИЯ
## Текст претензии
### Требования
```
### 2. Жирный текст
```markdown
**жирный текст**
__жирный текст__
```
**Пример:**
```markdown
Кому: **УК "Жилищник"**
Сумма: __400000 рублей__
```
### 3. Курсив
```markdown
*курсив*
урсив_
```
**Пример:**
```markdown
От: *Иванов Иван Иванович*
Дата: _15.01.2025_
```
### 4. Выделение кода
```markdown
`код`
```
**Пример:**
```markdown
Сумма ущерба: `400000` рублей
Статья: `ст. 1064 ГК РФ`
```
### 5. Маркированные списки
```markdown
- Первый пункт
- Второй пункт
- Третий пункт
* Альтернативный маркер
* Еще один пункт
```
**Пример:**
```markdown
Требования:
- Возместить ущерб
- Провести экспертизу
- Подготовить документы
```
### 6. Нумерованные списки
```markdown
1. Первый пункт
2. Второй пункт
3. Третий пункт
```
**Пример:**
```markdown
Порядок действий:
1. Подать претензию
2. Дождаться ответа
3. При необходимости обратиться в суд
```
## 📋 Пример полного документа с форматированием
```markdown
# ПРЕТЕНЗИЯ
## Заголовок раздела
Кому: **УК "Жилищник"**
От: *Иванов Иван Иванович*
Дата: 15.01.2025
### Текст претензии
УК отказывается возмещать ущерб от залива квартиры от стояка ХВС.
Ущерб составляет `400000` рублей.
### Требования:
1. Возместить ущерб в размере **400000 рублей**
2. Провести экспертизу для оценки ущерба
3. Возместить моральный вред
### Дополнительно:
- Провести экспертизу
- Оценить ущерб
- Подготовить документы
С уважением,
**Иванов Иван Иванович**
```
## 🎨 Как это выглядит в документе
### Заголовки:
- **H1** (#) — крупный заголовок, 18pt, жирный, отступ сверху
- **H2** (##) — средний заголовок, 16pt, жирный
- **H3** (###) — маленький заголовок, 14pt, жирный
### Текст:
- **Жирный** — выделение важной информации
- *Курсив* — акценты, названия
- `Код` — статьи, суммы, технические данные (Courier New, синий цвет)
### Списки:
- Маркированные — с символом •, отступ слева
- Нумерованные — с автоматической нумерацией, отступ слева
## 💡 Рекомендации для AI
### Когда использовать форматирование:
1. **Заголовки** — для структурирования документа:
```markdown
# ПРЕТЕНЗИЯ
## Текст претензии
## Требования
## Приложения
```
2. **Жирный текст** — для важной информации:
```markdown
Кому: **УК "Жилищник"**
Сумма: **400000 рублей**
```
3. **Списки** — для перечислений:
```markdown
Требования:
1. Возместить ущерб
2. Провести экспертизу
```
4. **Код** — для статей, сумм, ссылок:
```markdown
Ссылка на право: `ст. 1064 ГК РФ`
Сумма: `400000` рублей
```
### Пример использования в AI ответе:
```markdown
# ПРЕТЕНЗИЯ
Кому: **УК "Жилищник"**
От: *Иванов Иван Иванович*
Дата: 15.01.2025
## Текст претензии
УК отказывается возмещать ущерб от залива квартиры от стояка ХВС.
Ущерб составляет `400000` рублей.
## Требования:
1. Возместить ущерб в размере **400000 рублей**
2. Провести экспертизу для оценки ущерба
3. Возместить моральный вред
## Ссылки на право:
- `ст. 1064 ГК РФ` - общие основания ответственности за вред
- `ст. 15 ГК РФ` - возмещение убытков
С уважением,
**Иванов Иван Иванович**
```
## ⚠️ Ограничения
1. **Вложенные списки** — не поддерживаются (только один уровень)
2. **Таблицы** — не поддерживаются (можно использовать списки)
3. **Изображения** — не поддерживаются
4. **Ссылки** — не поддерживаются (можно использовать код `[текст](url)`)
5. **Комбинированное форматирование** — `**жирный *курсив* текст**` работает частично
## 🔧 Технические детали
- Парсинг выполняется построчно
- Поддерживается комбинирование форматирования в одном параграфе
- Списки автоматически завершаются при появлении обычного текста
- Пустые строки создают отступы между блоками
## 📚 Справочник Markdown для AI
Используй эти элементы при генерации документов:
| Элемент | Синтаксис | Пример |
|---------|-----------|--------|
| Заголовок H1 | `# Текст` | `# ПРЕТЕНЗИЯ` |
| Заголовок H2 | `## Текст` | `## Требования` |
| Заголовок H3 | `### Текст` | `### Дополнительно` |
| Жирный | `**текст**` | `**400000 рублей**` |
| Курсив | `*текст*` | `*Иванов Иван Иванович*` |
| Код | `` `текст` `` | `` `ст. 1064 ГК РФ` `` |
| Маркированный список | `- пункт` | `- Первый пункт` |
| Нумерованный список | `1. пункт` | `1. Первый пункт` |
## ✅ Преимущества
1. **Стандартный синтаксис** — Markdown понимают все AI модели
2. **Читаемость** — легко читать и редактировать
3. **Гибкость** — можно комбинировать элементы
4. **Автоматическое форматирование** — документ получается красивым без ручной правки

View File

@@ -0,0 +1,160 @@
# 🔧 cURL для n8n HTTP Request ноды
**Дата:** 2025-01-XX
**Назначение:** Тестирование и настройка HTTP Request ноды в n8n
## 📍 Endpoint
```
POST https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php
```
## 🔧 cURL команда для тестирования
### Базовый пример:
```bash
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
-H "Content-Type: application/json" \
-d '{
"module": "Project",
"recordId": "123456",
"recordName": "Тестовый проект",
"fileName": "Тестовый_документ",
"documentText": "ПРЕТЕНЗИЯ\n\nКому: УК Жилищник\nОт: Иванов Иван Иванович\n\nДата: 15.01.2025\n\nТекст претензии:\nУК отказывается возмещать ущерб от залива квартиры.\n\nТребования:\n1. Возместить ущерб в размере 400000 рублей\n2. Провести экспертизу\n\nС уважением,\nИванов Иван Иванович"
}'
```
### С documentType:
```bash
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
-H "Content-Type: application/json" \
-d '{
"module": "Project",
"recordId": "123456",
"recordName": "Тестовый проект",
"fileName": "Тестовый_документ",
"documentText": "ПРЕТЕНЗИЯ\n\nКому: УК Жилищник\nОт: Иванов Иван Иванович\n\n...",
"documentType": "docx"
}'
```
## 📋 Для импорта в n8n HTTP Request ноду
### Настройки ноды:
**Method:** `POST`
**URL:**
```
https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php
```
**Authentication:** None
**Headers:**
```
Content-Type: application/json
```
**Body (JSON):**
```json
{
"module": "{{ $json.body.context.module }}",
"recordId": "{{ $json.body.context.projectId }}",
"recordName": "{{ $json.body.context.projectName }}",
"fileName": "{{ $json.body.documentName }}",
"documentText": "{{ $json.body.generatedText }}",
"documentType": "docx"
}
```
## 🎯 Примеры с реальными данными
### Пример 1: Претензия
```bash
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
-H "Content-Type: application/json" \
-d '{
"module": "Project",
"recordId": "390657",
"recordName": "Дело Иванова",
"fileName": "Претензия_УК_Жилищник",
"documentText": "ПРЕТЕНЗИЯ\n\nКому: УК \"Жилищник\"\nОт: Иванов Иван Иванович\n\nДата: 15.01.2025\n\nТекст претензии:\nУК отказывается возмещать ущерб от залива квартиры от стояка ХВС. Ущерб составляет 400000 рублей.\n\nТребования:\n1. Возместить ущерб в размере 400000 рублей\n2. Провести экспертизу для оценки ущерба\n3. Возместить моральный вред\n\nС уважением,\nИванов Иван Иванович"
}'
```
### Пример 2: Исковое заявление
```bash
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
-H "Content-Type: application/json" \
-d '{
"module": "Project",
"recordId": "390657",
"recordName": "Дело Иванова",
"fileName": "Исковоеаявление",
"documentText": "ИСКОВОЕ ЗАЯВЛЕНИЕ\n\nВ суд: Районный суд г. Москвы\n\nИстец: Иванов Иван Иванович\nОтветчик: УК \"Жилищник\"\n\nЦена иска: 400000 рублей\n\nИсковые требования:\n1. Взыскать с ответчика 400000 рублей в счет возмещения ущерба\n2. Взыскать госпошлину\n\nОбстоятельства дела:\n..."
}'
```
## 🔍 Проверка ответа
### Успешный ответ:
```json
{
"success": true,
"message": "Документ создан успешно",
"documentName": ретензия_УК_Жилищник.docx",
"documentUrl": "https://s3.twcstorage.ru/.../Претензия_УК_Жилищник.docx",
"editUrl": "https://crm.clientright.ru/crm_extensions/file_storage/api/open_file_v2.php?recordId=123456&fileName=...",
"path": "crm2/CRM_Active_Files/Documents/Project/Дело_Иванова_123456/Претензия_УК_Жилищник.docx"
}
```
### Ошибка:
```json
{
"success": false,
"error": "Не указаны обязательные параметры: module, recordId, fileName, documentText"
}
```
## 📝 n8n Expression для Body
Если используете выражения n8n в Body:
```json
{
"module": "{{ $json.body.context.module || 'Project' }}",
"recordId": "{{ $json.body.context.projectId }}",
"recordName": "{{ $json.body.context.projectName || 'Проект' }}",
"fileName": "{{ $json.body.documentName || 'Документ_' + Date.now() }}",
"documentText": "{{ $json.body.generatedText }}",
"documentType": "{{ $json.body.documentType || 'docx' }}"
}
```
## 🚀 Быстрый тест
```bash
# Минимальный тест
curl -X POST "https://crm.clientright.ru/crm_extensions/file_storage/api/create_document_with_text.php" \
-H "Content-Type: application/json" \
-d '{
"module": "Project",
"recordId": "123456",
"recordName": "Тест",
"fileName": "Тест",
"documentText": "Тестовый документ\n\nЭто тест создания документа через API."
}'
```

View File

@@ -0,0 +1,219 @@
# 📋 Настройка шаблонов документов в Nextcloud
**Дата:** 2025-01-XX
**Статус:** ✅ Готово к использованию
## 🎯 Обзор
Для генерации документов из шаблонов используется гибридный подход:
1. **Шаблоны хранятся в Nextcloud** в папке `/crm/Templates/`
2. **Заполнение переменных** происходит через PHPWord
3. **Готовый документ** сохраняется в папку проекта и открывается в OnlyOffice
## 📁 Структура шаблонов
### 1. Создание папки для шаблонов
В Nextcloud создайте папку:
```
/crm/Templates/
```
**Как создать:**
1. Зайдите в Nextcloud: `https://office.clientright.ru:8443`
2. Перейдите в папку `/crm/`
3. Создайте папку `Templates`
4. Загрузите туда типовые документы
### 2. Формат шаблонов
#### Формат переменных в шаблонах:
**Вариант 1: Простые переменные**
```
{CLIENT_NAME}
{DATE}
{AMOUNT}
```
**Вариант 2: Двойные фигурные скобки**
```
{{CLIENT_NAME}}
{{DATE}}
{{AMOUNT}}
```
#### Пример шаблона претензии:
```docx
ПРЕТЕНЗИЯ
Кому: {RESPONDENT_NAME}
От: {CLIENT_NAME}
Дата: {DATE}
Текст претензии:
{CLAIM_TEXT}
Требования:
1. Возместить ущерб в размере {AMOUNT} рублей
2. {OTHER_REQUIREMENTS}
С уважением,
{CLIENT_NAME}
```
## 🔧 Использование API
### Endpoint: `/crm_extensions/file_storage/api/create_from_template.php`
**Параметры:**
- `module` - модуль CRM (Project, Contacts, etc.)
- `recordId` - ID записи
- `recordName` - название записи
- `fileName` - имя создаваемого файла
- `templateName` - имя шаблона из Nextcloud (например, `pretenziya.docx`)
- `variables` - JSON объект с переменными для заполнения
**Пример запроса:**
```javascript
const url = `/crm_extensions/file_storage/api/create_from_template.php?` +
`module=Project&` +
`recordId=123456&` +
`recordName=Проект_1&` +
`fileName=Претензия_УК&` +
`templateName=pretenziya.docx&` +
`variables=${encodeURIComponent(JSON.stringify({
CLIENT_NAME: 'Иванов Иван Иванович',
DATE: '15.01.2025',
AMOUNT: '400000',
RESPONDENT_NAME: 'УК "Жилищник"',
CLAIM_TEXT: 'УК отказывается возмещать ущерб от залива квартиры...',
OTHER_REQUIREMENTS: 'Провести экспертизу'
}))}`;
window.location.href = url;
```
## 🚀 Интеграция с AI Drawer
### Пример использования в n8n:
```javascript
// После генерации текста AI
const aiResponse = {
document_type: 'pretenziya',
client_name: 'Иванов Иван Иванович',
amount: '400000',
claim_text: '...',
// ... другие данные
};
// Определяем шаблон по типу документа
const templateMap = {
'pretenziya': 'pretenziya.docx',
'isk': 'iskovoe_zayavlenie.docx',
'zhaloba': 'zhaloba.docx',
'hodataystvo': 'hodataystvo.docx'
};
const templateName = templateMap[aiResponse.document_type] || 'pretenziya.docx';
// Формируем переменные
const variables = {
CLIENT_NAME: aiResponse.client_name,
DATE: new Date().toLocaleDateString('ru-RU'),
AMOUNT: aiResponse.amount,
CLAIM_TEXT: aiResponse.claim_text,
// ... другие переменные
};
// Вызываем API создания документа
const createUrl = `https://crm.clientright.ru/crm_extensions/file_storage/api/create_from_template.php?` +
`module=Project&` +
`recordId=${projectId}&` +
`recordName=${projectName}&` +
`fileName=${fileName}&` +
`templateName=${templateName}&` +
`variables=${encodeURIComponent(JSON.stringify(variables))}`;
// Открываем документ
return { url: createUrl };
```
## 📝 Создание шаблонов
### Рекомендации по созданию шаблонов:
1. **Используйте стандартные названия:**
- `pretenziya.docx` - Претензия
- `iskovoe_zayavlenie.docx` - Исковое заявление
- `zhaloba.docx` - Жалоба
- `hodataystvo.docx` - Ходатайство
2. **Структура документа:**
- Шапка (кому, от кого, дата)
- Фабула (описание ситуации)
- Требования
- Ссылки на право
- Приложения
3. **Переменные:**
- Используйте понятные названия: `CLIENT_NAME`, `AMOUNT`, `DATE`
- Все переменные в верхнем регистре
- Обрамляйте фигурными скобками: `{VAR}` или `{{VAR}}`
## 🔍 Отладка
### Логи:
```bash
tail -f /var/log/apache2/error.log | grep "CREATE FROM TEMPLATE"
```
### Проверка шаблона:
```bash
# Проверить наличие шаблона в Nextcloud
curl -u admin:office "https://office.clientright.ru:8443/remote.php/dav/files/admin/crm/Templates/pretenziya.docx" -k -I
```
## ⚠️ Ограничения
1. **PHPWord** работает только с DOCX файлами
2. Для XLSX и PPTX используется простая замена текста
3. Сложное форматирование (таблицы, изображения) может не сохраниться при простой замене
## 🎯 Альтернативные подходы
### Вариант A: Использование DOCX шаблонов с закладками
Вместо переменных `{VAR}` можно использовать закладки Word:
1. В Word: Вставка → Закладка
2. Создать закладку с именем переменной
3. PHPWord может заполнять закладки
### Вариант B: Использование только локальных шаблонов
Если не нужна синхронизация через Nextcloud:
1. Хранить шаблоны в `/crm_extensions/file_storage/templates/`
2. Использовать напрямую без WebDAV
### Вариант C: Генерация через PDFMaker
Если документ должен быть в PDF:
1. Создать DOCX из шаблона
2. Конвертировать через PDFMaker
3. Сохранить PDF в проект
## 📚 Полезные ссылки
- [PHPWord Documentation](https://phpword.readthedocs.io/)
- [Nextcloud WebDAV API](https://docs.nextcloud.com/server/latest/user_manual/files/webdav.html)
- [OnlyOffice Integration](https://api.onlyoffice.com/)

View File

@@ -0,0 +1,254 @@
# 🔍 Анализ проблемы с API шаблонов Nextcloud
**Дата:** 2025-01-XX
**Статус:** ✅ Проблема найдена и решена
## 🎯 Проблема
При попытке получить список шаблонов через API:
```bash
curl "https://office.clientright.ru:8443/ocs/v2.php/apps/files/api/v1/directEditing/templates"
```
Получаем ошибку:
```xml
<status>failure</status>
<statuscode>998</statuscode>
<message>Invalid query, please check the syntax.</message>
```
## 🔬 Диагностика
### 1. Проверка основного API Direct Editing
**Запрос:**
```bash
curl -u admin:office "https://office.clientright.ru:8443/ocs/v2.php/apps/files/api/v1/directEditing" \
-H "OCS-APIRequest: true" -k
```
**Результат:** ✅ API работает, возвращает список редакторов и создателей
**Структура ответа:**
```xml
<editors>
<onlyoffice>
<id>onlyoffice</id>
<name>ONLYOFFICE</name>
<mimetypes>...</mimetypes>
<optionalMimetypes>...</optionalMimetypes>
</onlyoffice>
</editors>
<creators>
<onlyoffice_docx>
<id>onlyoffice_docx</id>
<editor>onlyoffice</editor>
<name>Новый документ</name>
<extension>docx</extension>
<templates></templates> ← ПУСТОЙ!
<mimetype>application/vnd.openxmlformats-officedocument.wordprocessingml.document</mimetype>
</onlyoffice_docx>
</creators>
```
### 2. Выводы
**Проблема №1: Endpoint `/templates` не существует**
- Nextcloud Direct Editing API не имеет отдельного endpoint `/templates`
- Шаблоны должны возвращаться внутри основного ответа `/directEditing`
- Тег `<templates></templates>` присутствует, но **пустой**
**Проблема №2: Шаблоны не настроены в Nextcloud**
- Тег `<templates></templates>` пустой означает, что шаблоны не найдены
- Nextcloud ищет шаблоны в специальной папке, но она либо не существует, либо пустая
## 📚 Как работает система шаблонов в Nextcloud
### Где Nextcloud ищет шаблоны:
1. **Системные шаблоны OnlyOffice:**
- Хранятся в конфигурации OnlyOffice
- Недоступны через Nextcloud API напрямую
2. **Пользовательские шаблоны:**
- Должны быть в специальной папке пользователя
- Путь зависит от конфигурации Nextcloud
- Обычно: `/Templates/` в корне пользователя
3. **Глобальные шаблоны:**
- Могут быть настроены администратором
- Путь настраивается в `config.php`
## ✅ Решение
### Вариант 1: Использовать WebDAV для получения списка шаблонов (РЕКОМЕНДУЕТСЯ)
Вместо несуществующего API endpoint, используем WebDAV PROPFIND:
```php
// Получаем список файлов из папки Templates
$templatesUrl = 'https://office.clientright.ru:8443/remote.php/dav/files/admin/crm/Templates/';
$ch = curl_init($templatesUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Depth: 1']);
curl_setopt($ch, CURLOPT_USERPWD, "admin:office");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// Парсим XML ответ и извлекаем имена файлов
```
**Преимущества:**
- ✅ Работает всегда (WebDAV - стандартный протокол)
-Не зависит от версии Nextcloud
- ✅ Можно получить метаданные файлов (размер, дата изменения)
### Вариант 2: Настроить шаблоны в Nextcloud (если нужно)
Если хотите использовать встроенную систему шаблонов:
1. **Создать папку Templates:**
```
/admin/Templates/ (в корне пользователя admin)
```
2. **Загрузить шаблоны:**
- Загрузить DOCX файлы через веб-интерфейс
- Или через WebDAV
3. **Проверить конфигурацию:**
```php
// config/config.php
'direct_editing' => [
'templates' => [
'path' => '/admin/Templates/',
],
],
```
**Проблема:** Не все версии Nextcloud поддерживают это из коробки.
### Вариант 3: Использовать наш подход (ТЕКУЩИЙ)
Мы уже реализовали решение через WebDAV:
- Шаблоны хранятся в `/crm/Templates/`
- Получаем список через WebDAV PROPFIND
- Скачиваем шаблон через WebDAV GET
- Заполняем переменные через PHPWord
- Сохраняем готовый документ
**Преимущества:**
- ✅ Работает независимо от версии Nextcloud
- ✅ Полный контроль над процессом
- ✅ Можно использовать сложную логику заполнения
## 🔧 Реализация получения списка шаблонов
Создадим endpoint для получения списка шаблонов:
```php
// /crm_extensions/file_storage/api/list_templates.php
<?php
require_once '/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/shared/EnvLoader.php';
EnvLoader::load('/var/www/fastuser/data/www/crm.clientright.ru/crm_extensions/.env');
header('Content-Type: application/json; charset=utf-8');
$nextcloudUrl = 'https://office.clientright.ru:8443';
$username = 'admin';
$password = 'office';
$templatesPath = '/crm/Templates/';
$webdavUrl = $nextcloudUrl . '/remote.php/dav/files/' . $username . $templatesPath;
$ch = curl_init($webdavUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PROPFIND');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Depth: 1',
'Content-Type: application/xml'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, '<?xml version="1.0"?><d:propfind xmlns:d="DAV:"><d:prop><d:displayname/><d:getcontenttype/><d:getcontentlength/></d:prop></d:propfind>');
curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode !== 207) { // 207 Multi-Status для PROPFIND
echo json_encode(['success' => false, 'error' => 'Failed to get templates']);
exit;
}
// Парсим XML
$xml = simplexml_load_string($response);
$xml->registerXPathNamespace('d', 'DAV:');
$templates = [];
foreach ($xml->xpath('//d:response') as $response) {
$href = (string)$response->xpath('.//d:href')[0];
$displayName = (string)$response->xpath('.//d:displayname')[0];
$contentType = (string)$response->xpath('.//d:getcontenttype')[0];
// Пропускаем саму папку
if (rtrim($href, '/') === rtrim($webdavUrl, '/')) {
continue;
}
// Только Office файлы
if (strpos($contentType, 'officedocument') !== false ||
strpos($contentType, 'msword') !== false ||
strpos($contentType, 'spreadsheet') !== false ||
strpos($contentType, 'presentation') !== false) {
$templates[] = [
'name' => $displayName,
'path' => $href,
'type' => $contentType
];
}
}
echo json_encode(['success' => true, 'templates' => $templates]);
```
## 📊 Сравнение подходов
| Подход | Работает | Сложность | Гибкость |
|--------|----------|-----------|----------|
| API `/templates` | ❌ Не существует | - | - |
| WebDAV PROPFIND | ✅ Да | Средняя | Высокая |
| Настройка Nextcloud | ⚠️ Зависит от версии | Высокая | Низкая |
| Наш подход (WebDAV + PHPWord) | ✅ Да | Средняя | Очень высокая |
## 🎯 Рекомендация
**Использовать WebDAV PROPFIND** для получения списка шаблонов:
- ✅ Надежно работает
- ✅ Не зависит от версии Nextcloud
- ✅ Можно получить метаданные
- ✅ Стандартный протокол
**Не использовать** несуществующий endpoint `/templates`:
- ❌ Его нет в API Nextcloud
- ❌ Возвращает ошибку 998 (Invalid query)
## 📝 Выводы
1. **Проблема:** Endpoint `/ocs/v2.php/apps/files/api/v1/directEditing/templates` не существует в Nextcloud API
2. **Причина:** Nextcloud не предоставляет отдельный endpoint для получения шаблонов
3. **Решение:** Использовать WebDAV PROPFIND для получения списка файлов из папки Templates
4. **Статус:** Наш текущий подход (WebDAV + PHPWord) является правильным и оптимальным решением

View File

@@ -0,0 +1,141 @@
# 🔍 Анализ системы шаблонов ONLYOFFICE в Nextcloud
**Дата:** 2025-01-XX
**Статус:** ✅ Найдено решение
## 🎯 Проблема
В настройках ONLYOFFICE видно раздел "Общие шаблоны" с шаблоном:
- `Соглашение_№_71_06_об_оказании_юридической_помощи_от_01_10_2025_года.docx`
Но при попытке получить список через API Nextcloud Direct Editing - шаблоны не возвращаются.
## 🔬 Диагностика
### 1. Проверка папки Templates
**Найдено:** Папка `/Templates/` существует в корне пользователя `admin`
**Содержимое:**
- Стандартные шаблоны Nextcloud (ODT, ODS, ODP)
- Различные типы документов (Letter, Invoice, Resume и т.д.)
**WebDAV путь:**
```
https://office.clientright.ru:8443/remote.php/dav/files/admin/Templates/
```
### 2. Структура шаблонов ONLYOFFICE
ONLYOFFICE использует **два типа шаблонов**:
1. **Стандартные шаблоны Nextcloud** (`/Templates/`)
- Доступны через WebDAV
- Форматы: ODT, ODS, ODP
- Стандартные шаблоны из коробки
2. **Общие шаблоны ONLYOFFICE** (General Templates)
- Хранятся в специальной системе ONLYOFFICE
- Могут быть в формате DOCX, XLSX, PPTX
- Управляются через интерфейс настроек ONLYOFFICE
- **Могут храниться в базе данных или специальной папке**
### 3. Где хранятся "Общие шаблоны" ONLYOFFICE?
**Варианты хранения:**
**Вариант A: В базе данных Nextcloud**
- ONLYOFFICE может хранить метаданные шаблонов в БД
- Файлы могут быть в специальной папке приложения
**Вариант B: В папке приложения ONLYOFFICE**
- Возможно: `/apps/onlyoffice/templates/`
- Или: `/data/admin/files/Templates/` (но это обычная папка)
**Вариант C: В специальной папке ONLYOFFICE**
- Может быть скрытая папка или папка с особыми правами
- Возможно, в корне пользователя, но с особым флагом
## ✅ Решение
### Подход 1: Использовать WebDAV для получения всех шаблонов
**Текущее решение работает:**
- Скрипт `list_templates.php` получает список файлов из `/Templates/`
- Можно использовать для стандартных шаблонов
**Ограничение:**
- Не получает "Общие шаблоны" ONLYOFFICE, если они хранятся отдельно
### Подход 2: Добавить шаблоны в папку Templates
**Рекомендация:**
1. Скачать шаблон "Соглашение..." из настроек ONLYOFFICE
2. Загрузить его в папку `/Templates/` через WebDAV или веб-интерфейс
3. Теперь он будет доступен через наш API
**Преимущества:**
- ✅ Единая точка доступа ко всем шаблонам
- ✅ Работает через WebDAV (стандартный протокол)
-Не зависит от внутренней структуры ONLYOFFICE
### Подход 3: Использовать API ONLYOFFICE напрямую (если доступен)
**Проверка:**
```bash
# Попытка получить шаблоны через ONLYOFFICE API
curl "https://office.clientright.ru:8443/index.php/apps/onlyoffice/ajax/templates"
```
**Статус:** Не работает (возвращает 404)
## 📝 Рекомендации
### Для использования шаблонов:
1. **Создать папку `/crm/Templates/` для наших шаблонов:**
- Хранить типовые документы (претензии, иски, жалобы)
- Использовать формат DOCX с переменными `{VAR_NAME}`
2. **Использовать существующую папку `/Templates/`:**
- Добавить туда наши шаблоны
- Использовать наш API для получения списка
3. **Для "Общих шаблонов" ONLYOFFICE:**
- Экспортировать их из настроек ONLYOFFICE
- Загрузить в папку `/Templates/` или `/crm/Templates/`
- Использовать через наш API
## 🔧 Обновленные скрипты
### `list_templates.php`
- ✅ Исправлен путь на `/Templates/` (корень пользователя)
- ✅ Работает с WebDAV PROPFIND
- ✅ Возвращает список всех Office файлов
### `create_from_template.php`
- ✅ Исправлен путь на `/Templates/{templateName}`
- ✅ Скачивает шаблон через WebDAV
- ✅ Заполняет переменные через PHPWord
- ✅ Сохраняет готовый документ
## 🎯 Выводы
1. **Шаблоны ONLYOFFICE хранятся в папке `/Templates/`** в корне пользователя
2. **"Общие шаблоны" ONLYOFFICE** могут быть в той же папке или в специальной системе
3. **Наш подход через WebDAV работает** для всех шаблонов в папке `/Templates/`
4. **Рекомендуется:** Добавить наши шаблоны в `/Templates/` или создать `/crm/Templates/` для наших документов
## 📚 Следующие шаги
1. Проверить, есть ли шаблон "Соглашение..." в папке `/Templates/`
2. Если нет - экспортировать из настроек ONLYOFFICE и загрузить в папку
3. Протестировать получение списка через `list_templates.php`
4. Использовать шаблоны через `create_from_template.php`

View File

@@ -0,0 +1,92 @@
<?php
/**
* Исправление пути документа 397340 в проекте 396447
*
* Проблема: документ 397340 имеет путь с префиксом 'crm2/CRM_Active_Files/',
* а остальные документы проекта имеют путь БЕЗ этого префикса.
*
* Решение: убрать префикс 'crm2/CRM_Active_Files/' из s3_key для единообразия.
*/
require_once '/var/www/fastuser/data/www/crm.clientright.ru/config.inc.php';
$pdo = new PDO(
"mysql:host={$dbconfig['db_server']};port=3306;dbname={$dbconfig['db_name']};charset=utf8mb4",
$dbconfig['db_username'],
$dbconfig['db_password'],
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]
);
$notesId = 397340;
$dryRun = false; // Изменить на false для реального исправления
echo "=== ИСПРАВЛЕНИЕ ПУТИ ДОКУМЕНТА 397340 ===\n\n";
// Получаем текущие данные документа
$sql = "SELECT notesid, title, s3_key, s3_bucket, filename FROM vtiger_notes WHERE notesid = ?";
$stmt = $pdo->prepare($sql);
$stmt->execute([$notesId]);
$doc = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$doc) {
die("❌ Документ $notesId не найден!\n");
}
echo "📄 Документ: {$doc['title']}\n";
echo " ID: {$doc['notesid']}\n";
echo " Текущий s3_key: {$doc['s3_key']}\n\n";
// Проверяем, есть ли префикс
if (strpos($doc['s3_key'], 'crm2/CRM_Active_Files/') === 0) {
// Убираем префикс
$newS3Key = str_replace('crm2/CRM_Active_Files/', '', $doc['s3_key']);
echo "✅ Найден префикс 'crm2/CRM_Active_Files/'\n";
echo " Новый s3_key: $newS3Key\n\n";
// Проверяем остальные документы проекта для сравнения
$sql2 = "SELECT notesid, s3_key FROM vtiger_notes n
INNER JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
WHERE snr.crmid = 396447 AND n.notesid != ? AND n.s3_key IS NOT NULL
LIMIT 3";
$stmt2 = $pdo->prepare($sql2);
$stmt2->execute([$notesId]);
$others = $stmt2->fetchAll(PDO::FETCH_ASSOC);
echo "📊 Сравнение с другими документами проекта:\n";
foreach ($others as $other) {
echo " ID {$other['notesid']}: {$other['s3_key']}\n";
}
echo "\n";
// Обновляем filename тоже (если там есть полный URL)
$newFilename = $doc['filename'];
if (strpos($doc['filename'], 'crm2/CRM_Active_Files/') !== false) {
$newFilename = str_replace('crm2/CRM_Active_Files/', '', $doc['filename']);
// Если это полный URL, пересобираем его
if (strpos($newFilename, 'https://') === false && $doc['s3_bucket']) {
$newFilename = "https://s3.twcstorage.ru/{$doc['s3_bucket']}/" . rawurlencode($newS3Key);
}
}
if (!$dryRun) {
echo "🔧 ПРИМЕНЯЕМ ИСПРАВЛЕНИЕ...\n\n";
$updateSql = "UPDATE vtiger_notes SET s3_key = ?, filename = ? WHERE notesid = ?";
$updateStmt = $pdo->prepare($updateSql);
$updateStmt->execute([$newS3Key, $newFilename, $notesId]);
echo "✅ Документ обновлён!\n";
echo " Новый s3_key: $newS3Key\n";
echo " Новый filename: " . substr($newFilename, 0, 100) . "...\n";
} else {
echo "⚠️ РЕЖИМ ПРОВЕРКИ (dry-run)\n";
echo " Для применения изменений установите \$dryRun = false\n";
}
} else {
echo " Префикс 'crm2/CRM_Active_Files/' не найден в пути.\n";
echo " Документ уже в правильном формате.\n";
}
echo "\n=== ГОТОВО ===\n";

View File

@@ -0,0 +1,48 @@
#!/bin/bash
echo "🔑 S3 Credentials для n8n"
echo "════════════════════════════════════════════════════════════════════════════════"
echo ""
# Получаем credentials из Nextcloud
docker exec nextcloud-fresh php occ files_external:list --output=json 2>/dev/null | jq -r '
.[0].configuration |
"📦 Bucket: \(.bucket)
🔑 Access Key: \(.key)
🔐 Secret Key: \(.secret)
🌐 Endpoint: \(.hostname)
🗺️ Region: \(.region)
════════════════════════════════════════════════════════════════════════════════
📋 Для n8n AWS S3 Credentials:
────────────────────────────────────────────────────────────────────────────────
Access Key ID: \(.key)
Secret Access Key: \(.secret)
Region: \(.region)
Custom Endpoints: ✅ ВКЛЮЧИТЬ
S3 Endpoint: https://\(.hostname)
Force Path Style: ✅ ВКЛЮЧИТЬ
────────────────────────────────────────────────────────────────────────────────
📋 Для n8n Environment Variables:
────────────────────────────────────────────────────────────────────────────────
S3_BUCKET=\(.bucket)
S3_ENDPOINT=https://\(.hostname)
S3_REGION=\(.region)
────────────────────────────────────────────────────────────────────────────────
"
'
echo ""
echo "📋 Redis Credentials для n8n:"
echo "────────────────────────────────────────────────────────────────────────────────"
echo "Host: 147.45.146.17"
echo "Port: 6379"
echo "Password: CRM_Redis_Pass_2025_Secure!"
echo "Database: 0"
echo "────────────────────────────────────────────────────────────────────────────────"
echo ""
echo "✅ Готово! Скопируй эти данные в n8n"

View File

@@ -0,0 +1,166 @@
<?php
/**
* Простая миграция одного проекта в новую структуру
* Использование: php migrate_single_project.php PROJECT_ID
*/
require_once(__DIR__ . '/../../config.inc.php');
require_once(__DIR__ . '/../../include/database/PearDatabase.php');
$adb = PearDatabase::getInstance();
// Получаем ID проекта
$projectId = isset($argv[1]) ? (int)$argv[1] : null;
if (!$projectId) {
echo "❌ Укажите ID проекта!\n";
echo "Использование: php migrate_single_project.php PROJECT_ID\n";
exit(1);
}
echo "🔄 МИГРАЦИЯ ПРОЕКТА $projectId\n";
echo "==========================================\n\n";
// Получаем информацию о проекте
$result = $adb->pquery("SELECT p.projectname FROM vtiger_project p WHERE p.projectid = ?", [$projectId]);
if ($adb->num_rows($result) == 0) {
echo "❌ Проект не найден!\n";
exit(1);
}
$projectName = $adb->query_result($result, 0, 'projectname');
echo "📁 Проект: $projectName\n\n";
// Функция очистки имени файла
function sanitizeName($name) {
// Транслитерация
$translitMap = [
'А' => 'A', 'Б' => 'B', 'В' => 'V', 'Г' => 'G', 'Д' => 'D',
'Е' => 'E', 'Ё' => 'E', 'Ж' => 'Zh', 'З' => 'Z', 'И' => 'I',
'Й' => 'Y', 'К' => 'K', 'Л' => 'L', 'М' => 'M', 'Н' => 'N',
'О' => 'O', 'П' => 'P', 'Р' => 'R', 'С' => 'S', 'Т' => 'T',
'У' => 'U', 'Ф' => 'F', 'Х' => 'H', 'Ц' => 'Ts', 'Ч' => 'Ch',
'Ш' => 'Sh', 'Щ' => 'Sch', 'Ъ' => '', 'Ы' => 'Y', 'Ь' => '',
'Э' => 'E', 'Ю' => 'Yu', 'Я' => 'Ya',
'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd',
'е' => 'e', 'ё' => 'e', 'ж' => 'zh', 'з' => 'z', 'и' => 'i',
'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n',
'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't',
'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'ts', 'ч' => 'ch',
'ш' => 'sh', 'щ' => 'sch', 'ъ' => '', 'ы' => 'y', 'ь' => '',
'э' => 'e', 'ю' => 'yu', 'я' => 'ya'
];
$name = strtr($name, $translitMap);
$name = preg_replace('/[^a-zA-Z0-9_\-]/', '_', $name);
$name = preg_replace('/_+/', '_', $name);
$name = trim($name, '_');
if (strlen($name) > 100) {
$name = substr($name, 0, 100);
}
return $name;
}
$sanitizedName = sanitizeName($projectName);
$newFolder = "Documents/Project/{$sanitizedName}_{$projectId}";
echo "📁 Новая папка: $newFolder\n\n";
// Получаем все документы проекта
$result = $adb->pquery(
"SELECT n.notesid, n.title, n.filename, n.s3_key, n.filelocationtype
FROM vtiger_notes n
JOIN vtiger_senotesrel snr ON snr.notesid = n.notesid
JOIN vtiger_crmentity e ON e.crmid = n.notesid
WHERE snr.crmid = ? AND e.deleted = 0 AND n.filelocationtype = 'E'
ORDER BY n.notesid",
[$projectId]
);
$total = $adb->num_rows($result);
echo "📊 Найдено документов: $total\n\n";
if ($total == 0) {
echo "✅ Нет документов для миграции\n";
exit(0);
}
$migrated = 0;
$errors = 0;
while ($doc = $adb->fetch_array($result)) {
$notesid = $doc['notesid'];
$title = $doc['title'];
$oldS3Key = $doc['s3_key'];
echo "[$migrated/$total] Документ: $title (ID: $notesid)\n";
echo " Старый s3_key: $oldS3Key\n";
// Извлекаем имя файла и расширение
$oldFilename = basename($oldS3Key);
$pathInfo = pathinfo($oldFilename);
$extension = isset($pathInfo['extension']) ? '.' . $pathInfo['extension'] : '';
// Новое имя файла: file_docID_название.ext
$newFilename = "file_{$notesid}_{$pathInfo['filename']}{$extension}";
// Новый путь в S3
$newS3Key = "{$newFolder}/{$newFilename}";
$newNcPath = "/crm/crm2/CRM_Active_Files/{$newFolder}/{$newFilename}";
// Новый URL
$bucket = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
$newUrl = "https://s3.twcstorage.ru/{$bucket}/crm2/CRM_Active_Files/{$newS3Key}";
echo " Новый s3_key: $newS3Key\n";
echo " Новый nc_path: $newNcPath\n";
// Копируем файл в S3 (используем aws s3 cp)
$oldKey = "crm2/CRM_Active_Files/" . $oldS3Key;
$newKey = "crm2/CRM_Active_Files/" . $newS3Key;
$copyCmd = "aws s3 cp " .
"s3://{$bucket}/{$oldKey} " .
"s3://{$bucket}/{$newKey} " .
"--endpoint-url https://s3.twcstorage.ru " .
"--region ru-1 2>&1";
echo " Копирование в S3...\n";
exec($copyCmd, $output, $returnCode);
if ($returnCode !== 0) {
echo " ❌ ОШИБКА копирования: " . implode("\n", $output) . "\n";
$errors++;
continue;
}
echo " ✅ Скопировано в S3\n";
// Обновляем БД
$updateResult = $adb->pquery(
"UPDATE vtiger_notes
SET s3_key = ?, nc_path = ?, filename = ?
WHERE notesid = ?",
[$newS3Key, $newNcPath, $newUrl, $notesid]
);
if ($updateResult) {
echo " ✅ БД обновлена\n";
$migrated++;
} else {
echo " ❌ ОШИБКА обновления БД\n";
$errors++;
}
echo "\n";
}
echo "==========================================\n";
echo "✅ МИГРАЦИЯ ЗАВЕРШЕНА!\n";
echo "Обработано: $total\n";
echo "Мигрировано: $migrated\n";
echo "Ошибок: $errors\n";

View File

@@ -0,0 +1,170 @@
{
"name": "Nextcloud Activity Monitor",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "seconds",
"secondsInterval": 30
}
]
}
},
"id": "schedule",
"name": "Каждые 30 секунд",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [250, 300]
},
{
"parameters": {
"url": "https://office.clientright.ru:8443/ocs/v2.php/apps/activity/api/v2/activity",
"authentication": "genericCredentialType",
"genericAuthType": "httpBasicAuth",
"sendQuery": true,
"queryParameters": {
"parameters": [
{
"name": "format",
"value": "json"
},
{
"name": "limit",
"value": "100"
}
]
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "OCS-APIRequest",
"value": "true"
}
]
},
"options": {}
},
"id": "http-request",
"name": "Get Nextcloud Activities",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4,
"position": [450, 300],
"credentials": {
"httpBasicAuth": {
"id": "3",
"name": "Nextcloud Admin"
}
}
},
{
"parameters": {
"jsCode": "// Получаем последний обработанный activity_id из Static Data\nconst staticData = getWorkflowStaticData('node');\nlet lastProcessedId = staticData.lastActivityId || 0;\n\nconst activities = $input.item.json.ocs.data;\nconst fileEvents = [];\n\n// Типы событий которые нас интересуют\nconst relevantTypes = ['file_created', 'file_changed', 'file_deleted', 'file_restored'];\n\n// Фильтруем и обрабатываем события\nfor (const activity of activities) {\n // Пропускаем уже обработанные\n if (activity.activity_id <= lastProcessedId) {\n continue;\n }\n \n // Только файловые события\n if (!relevantTypes.includes(activity.type)) {\n continue;\n }\n \n // РАЗБИВАЕМ агрегированные события!\n // Если есть objects - создаём событие для каждого файла\n if (activity.objects && typeof activity.objects === 'object') {\n const fileIds = Object.keys(activity.objects);\n \n for (const fileId of fileIds) {\n const filePath = activity.objects[fileId];\n \n fileEvents.push({\n json: {\n type: activity.type,\n source: 'nextcloud_activity',\n timestamp: activity.datetime,\n file_id: parseInt(fileId),\n path: filePath,\n filename: filePath ? filePath.split('/').pop() : null,\n user: activity.user,\n activity_id: activity.activity_id,\n action: activity.type.replace('file_', '')\n }\n });\n }\n } else {\n // Одиночное событие\n fileEvents.push({\n json: {\n type: activity.type,\n source: 'nextcloud_activity',\n timestamp: activity.datetime,\n file_id: activity.object_id,\n path: activity.object_name,\n filename: activity.object_name ? activity.object_name.split('/').pop() : null,\n user: activity.user,\n activity_id: activity.activity_id,\n action: activity.type.replace('file_', '')\n }\n });\n }\n \n // Обновляем последний ID\n if (activity.activity_id > lastProcessedId) {\n lastProcessedId = activity.activity_id;\n }\n}\n\n// Сохраняем последний обработанный ID\nstaticData.lastActivityId = lastProcessedId;\n\n// Если новых событий нет - возвращаем пустой массив\nif (fileEvents.length === 0) {\n return [];\n}\n\nreturn fileEvents;"
},
"id": "process-activities",
"name": "Process & Filter Events",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [650, 300]
},
{
"parameters": {
"conditions": {
"string": [
{
"value1": "={{$json.filename}}",
"operation": "isNotEmpty"
}
]
}
},
"id": "filter",
"name": "Есть новые события?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [850, 300]
},
{
"parameters": {
"operation": "publish",
"channel": "crm:file:events",
"value": "={{JSON.stringify($json)}}",
"keyType": "automatic"
},
"id": "redis-publish",
"name": "Publish to Redis",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [1050, 200],
"credentials": {
"redis": {
"id": "2",
"name": "CRM Redis"
}
}
},
{
"parameters": {
"content": "=📢 Nextcloud Event:\n**Type:** {{$json.type}}\n**File:** {{$json.filename}}\n**Path:** {{$json.path}}\n**User:** {{$json.user}}\n**Time:** {{$json.timestamp}}",
"options": {}
},
"id": "logger",
"name": "Log Event (optional)",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [1050, 400]
}
],
"connections": {
"schedule": {
"main": [
[
{
"node": "http-request",
"type": "main",
"index": 0
}
]
]
},
"http-request": {
"main": [
[
{
"node": "process-activities",
"type": "main",
"index": 0
}
]
]
},
"process-activities": {
"main": [
[
{
"node": "filter",
"type": "main",
"index": 0
}
]
]
},
"filter": {
"main": [
[
{
"node": "redis-publish",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}

View File

@@ -0,0 +1,174 @@
{
"name": "S3 Event Processor",
"nodes": [
{
"parameters": {
"operation": "subscribe",
"channel": "crm:file:events"
},
"id": "redis-subscribe",
"name": "Redis Subscribe",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [250, 300],
"credentials": {
"redis": {
"id": "CRM_REDIS_ID",
"name": "CRM Redis"
}
}
},
{
"parameters": {
"jsCode": "// Парсим JSON из Redis message\nconst items = [];\n\nfor (const item of $input.all()) {\n const message = item.json.message;\n \n // Если message это строка - парсим\n let parsedMessage = message;\n if (typeof message === 'string') {\n parsedMessage = JSON.parse(message);\n }\n \n items.push({\n json: parsedMessage\n });\n}\n\nreturn items;"
},
"id": "parse-json",
"name": "Parse Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [450, 300]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.type }}",
"rightValue": "file_created",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
}
},
"id": "filter-created",
"name": "Только новые файлы",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [650, 300]
},
{
"parameters": {
"conditions": {
"options": {},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.filename }}",
"rightValue": ".xlsx",
"operator": {
"type": "string",
"operation": "endsWith"
}
}
],
"combinator": "or"
}
},
"id": "filter-xlsx",
"name": "Только XLSX",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [850, 200]
},
{
"parameters": {
"conditions": {
"options": {},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.filename }}",
"rightValue": ".pdf",
"operator": {
"type": "string",
"operation": "endsWith"
}
}
],
"combinator": "or"
}
},
"id": "filter-pdf",
"name": "Только PDF",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [850, 400]
},
{
"parameters": {
"content": "=📊 **XLSX файл обнаружен!**\n\n**Имя:** {{ $json.filename }}\n**Путь:** {{ $json.path }}\n**Размер:** {{ $json.size }} bytes\n**Время:** {{ $json.timestamp }}\n\n---\n\nЗдесь можно:\n- Скачать файл из S3\n- Обработать данные\n- Загрузить в CRM\n- Отправить уведомление",
"height": 312,
"width": 389
},
"id": "process-xlsx",
"name": "Обработка XLSX",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [1050, 80]
},
{
"parameters": {
"content": "=📄 **PDF файл обнаружен!**\n\n**Имя:** {{ $json.filename }}\n**Путь:** {{ $json.path }}\n**Размер:** {{ $json.size }} bytes\n**Время:** {{ $json.timestamp }}\n\n---\n\nЗдесь можно:\n- Скачать PDF из S3\n- OCR распознавание\n- Извлечь данные\n- Создать документ в CRM",
"height": 312,
"width": 389
},
"id": "process-pdf",
"name": "Обработка PDF",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [1050, 320]
}
],
"connections": {
"redis-subscribe": {
"main": [
[
{
"node": "parse-json",
"type": "main",
"index": 0
}
]
]
},
"parse-json": {
"main": [
[
{
"node": "filter-created",
"type": "main",
"index": 0
}
]
]
},
"filter-created": {
"main": [
[
{
"node": "filter-xlsx",
"type": "main",
"index": 0
},
{
"node": "filter-pdf",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}

View File

@@ -0,0 +1,179 @@
{
"name": "S3 File Monitor",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "seconds",
"secondsInterval": 30
}
]
}
},
"id": "schedule",
"name": "Каждые 30 секунд",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1,
"position": [250, 300]
},
{
"parameters": {
"operation": "getAll",
"bucketName": "={{$env.S3_BUCKET}}",
"returnAll": true,
"options": {
"prefix": ""
}
},
"id": "s3-list",
"name": "List S3 Files",
"type": "n8n-nodes-base.awsS3",
"typeVersion": 1,
"position": [450, 300],
"credentials": {
"aws": {
"id": "1",
"name": "TWC S3"
}
}
},
{
"parameters": {
"functionCode": "// Храним состояние файлов в Static Data\nconst currentState = $node[\"s3-list\"].json;\nconst previousState = $getWorkflowStaticData('node') || {};\nconst events = [];\n\n// Проверяем новые и изменённые файлы\nfor (const file of currentState) {\n const key = file.Key;\n const etag = file.ETag;\n \n if (!previousState[key]) {\n // Новый файл\n events.push({\n type: 'file_created',\n source: 's3_monitor',\n timestamp: new Date().toISOString(),\n path: key,\n filename: key.split('/').pop(),\n size: file.Size,\n etag: etag,\n last_modified: file.LastModified\n });\n } else if (previousState[key].ETag !== etag) {\n // Файл изменён\n events.push({\n type: 'file_modified',\n source: 's3_monitor',\n timestamp: new Date().toISOString(),\n path: key,\n filename: key.split('/').pop(),\n size: file.Size,\n etag: etag,\n last_modified: file.LastModified\n });\n }\n \n previousState[key] = { ETag: etag, Size: file.Size };\n}\n\n// Проверяем удалённые файлы\nconst currentKeys = currentState.map(f => f.Key);\nfor (const key in previousState) {\n if (!currentKeys.includes(key)) {\n events.push({\n type: 'file_deleted',\n source: 's3_monitor',\n timestamp: new Date().toISOString(),\n path: key,\n filename: key.split('/').pop()\n });\n delete previousState[key];\n }\n}\n\n// Сохраняем новое состояние\n$setWorkflowStaticData(previousState);\n\nreturn events.map(event => ({ json: event }));"
},
"id": "detect-changes",
"name": "Detect Changes",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [650, 300]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.type }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty"
}
}
],
"combinator": "and"
}
},
"id": "filter",
"name": "Есть изменения?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [850, 300]
},
{
"parameters": {
"operation": "publish",
"channel": "crm:file:events",
"value": "={{JSON.stringify($json)}}",
"keyType": "automatic",
"expire": false,
"expireAfter": 86400
},
"id": "redis-publish",
"name": "Publish to Redis",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [1050, 200],
"credentials": {
"redis": {
"id": "2",
"name": "CRM Redis"
}
}
},
{
"parameters": {
"url": "={{$env.WEBHOOK_URL}}",
"options": {
"bodyContentType": "json"
},
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "event",
"value": "={{$json}}"
}
]
}
},
"id": "webhook",
"name": "Send Webhook (опционально)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [1050, 400]
}
],
"connections": {
"schedule": {
"main": [
[
{
"node": "s3-list",
"type": "main",
"index": 0
}
]
]
},
"s3-list": {
"main": [
[
{
"node": "detect-changes",
"type": "main",
"index": 0
}
]
]
},
"detect-changes": {
"main": [
[
{
"node": "filter",
"type": "main",
"index": 0
}
]
]
},
"filter": {
"main": [
[
{
"node": "redis-publish",
"type": "main",
"index": 0
},
{
"node": "webhook",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}

View File

@@ -0,0 +1,249 @@
#!/usr/bin/env node
/**
* Nextcloud Activity Monitor
* Мониторит события файлов через Activity API и публикует в Redis
*/
const https = require('https');
const Redis = require('ioredis');
const CONFIG = {
nextcloud: {
host: 'office.clientright.ru',
port: 8443,
username: 'admin',
password: 'tGHKS-3cC9m-7Hggb-65Awk-zxWQE',
auth: Buffer.from('admin:tGHKS-3cC9m-7Hggb-65Awk-zxWQE').toString('base64')
},
redis: {
host: '147.45.146.17',
port: 6379,
password: 'CRM_Redis_Pass_2025_Secure!'
},
pollInterval: 30000, // 30 секунд
stateKey: 'crm:nextcloud:activity:state'
};
const redis = new Redis({
host: CONFIG.redis.host,
port: CONFIG.redis.port,
password: CONFIG.redis.password
});
// Хранилище последнего обработанного activity_id
let lastActivityId = 0;
// Загрузка состояния из Redis
async function loadState() {
try {
const data = await redis.get(CONFIG.stateKey);
if (data) {
const state = JSON.parse(data);
lastActivityId = state.lastActivityId || 0;
console.log(`📥 Последний обработанный activity_id: ${lastActivityId}`);
} else {
console.log('📥 Состояние пустое (первый запуск)');
}
} catch (err) {
console.error('⚠️ Ошибка загрузки состояния:', err.message);
}
}
// Сохранение состояния в Redis
async function saveState() {
try {
await redis.set(CONFIG.stateKey, JSON.stringify({ lastActivityId }));
} catch (err) {
console.error('⚠️ Ошибка сохранения состояния:', err.message);
}
}
// Получение активностей из Nextcloud API
function getActivities(limit = 100) {
return new Promise((resolve, reject) => {
const options = {
hostname: CONFIG.nextcloud.host,
port: CONFIG.nextcloud.port,
path: `/ocs/v2.php/apps/activity/api/v2/activity?format=json&limit=${limit}`,
method: 'GET',
headers: {
'Authorization': `Basic ${CONFIG.nextcloud.auth}`,
'OCS-APIRequest': 'true'
},
rejectUnauthorized: false
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => {
data += chunk;
});
res.on('end', () => {
try {
const json = JSON.parse(data);
resolve(json.ocs.data || []);
} catch (err) {
reject(new Error('Ошибка парсинга JSON: ' + err.message));
}
});
});
req.on('error', (err) => {
reject(err);
});
req.setTimeout(10000, () => {
req.destroy();
reject(new Error('Timeout'));
});
req.end();
});
}
// Публикация события в Redis
async function publishEvent(event) {
console.log(`\n 📢 ${event.type.toUpperCase()}: ${event.filename}`);
console.log(` 🆔 file_id: ${event.file_id}`);
console.log(` 👤 user: ${event.user}`);
try {
await redis.publish('crm:file:events', JSON.stringify(event));
console.log(` ✅ Опубликовано в Redis`);
} catch (err) {
console.error(` ❌ Ошибка публикации:`, err.message);
}
}
// Сканирование новых активностей
async function scanActivities() {
try {
console.log(`\n🔍 Проверка новых событий... (${new Date().toISOString()})`);
const activities = await getActivities(100);
const relevantTypes = ['file_created', 'file_changed', 'file_deleted', 'file_restored'];
let newEvents = 0;
let totalFiles = 0;
// Обрабатываем активности (от новых к старым)
for (const activity of activities) {
// Пропускаем уже обработанные
if (activity.activity_id <= lastActivityId) {
continue;
}
// Только файловые события
if (!relevantTypes.includes(activity.type)) {
continue;
}
newEvents++;
// РАЗБИВАЕМ агрегированные события на отдельные файлы!
if (activity.objects && typeof activity.objects === 'object') {
// Множественное событие - разбиваем
const fileIds = Object.keys(activity.objects);
totalFiles += fileIds.length;
console.log(` 📦 Агрегированное событие: ${fileIds.length} файлов`);
for (const fileId of fileIds) {
const filePath = activity.objects[fileId];
const event = {
type: activity.type,
source: 'nextcloud_activity',
timestamp: activity.datetime,
file_id: parseInt(fileId),
path: filePath,
filename: filePath ? filePath.split('/').pop().replace(/^\//, '') : null,
user: activity.user,
activity_id: activity.activity_id,
action: activity.type.replace('file_', '')
};
await publishEvent(event);
}
} else {
// Одиночное событие
totalFiles++;
const event = {
type: activity.type,
source: 'nextcloud_activity',
timestamp: activity.datetime,
file_id: activity.object_id,
path: activity.object_name,
filename: activity.object_name ? activity.object_name.split('/').pop().replace(/^\//, '') : null,
user: activity.user,
activity_id: activity.activity_id,
action: activity.type.replace('file_', '')
};
await publishEvent(event);
}
// Обновляем последний ID
if (activity.activity_id > lastActivityId) {
lastActivityId = activity.activity_id;
}
}
// Сохраняем состояние
await saveState();
console.log(`\n✅ Сканирование завершено:`);
console.log(` 📊 Новых активностей: ${newEvents}`);
console.log(` 📁 Файлов обработано: ${totalFiles}`);
console.log(` 🆔 Последний activity_id: ${lastActivityId}`);
} catch (err) {
console.error('❌ Ошибка сканирования:', err.message);
}
}
// Запуск
async function start() {
console.log('🚀 Nextcloud Activity Monitor');
console.log('════════════════════════════════════════════════════════════════════════════════');
console.log(`📡 Nextcloud: ${CONFIG.nextcloud.host}:${CONFIG.nextcloud.port}`);
console.log(`📡 Redis: ${CONFIG.redis.host}:${CONFIG.redis.port}`);
console.log(`🔄 Интервал: ${CONFIG.pollInterval / 1000}с`);
console.log('════════════════════════════════════════════════════════════════════════════════\n');
await loadState();
console.log('👂 Начинаем мониторинг...\n');
// Первое сканирование
await scanActivities();
// Периодическое сканирование
setInterval(scanActivities, CONFIG.pollInterval);
}
// Запуск при подключении к Redis
redis.on('connect', () => {
console.log('✅ Подключились к Redis\n');
start();
});
redis.on('error', (err) => {
console.error('❌ Redis ошибка:', err.message);
});
process.on('SIGINT', () => {
console.log('\n\n⛔ Остановка мониторинга...');
redis.disconnect();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n\n⛔ Получен сигнал SIGTERM, останавливаемся...');
redis.disconnect();
process.exit(0);
});

View File

@@ -0,0 +1,124 @@
#!/usr/bin/env node
/**
* Nextcloud Cache Updater
*
* Подписывается на Redis канал crm:file:events
* При новом файле - обновляет кеш Nextcloud для этого файла
* БЕЗ полного сканирования всей папки!
*/
const Redis = require('ioredis');
const { exec } = require('child_process');
const util = require('util');
const execPromise = util.promisify(exec);
const CONFIG = {
redis: {
host: '147.45.146.17',
port: 6379,
password: 'CRM_Redis_Pass_2025_Secure!'
},
channel: 'crm:file:events',
nextcloudContainer: 'nextcloud-fresh'
};
const redis = new Redis(CONFIG.redis);
console.log('🔄 Nextcloud Cache Updater');
console.log('==========================================');
console.log(`📡 Подписка на: ${CONFIG.channel}`);
console.log(`🐳 Nextcloud: ${CONFIG.nextcloudContainer}`);
console.log('');
// Подписка на канал
redis.subscribe(CONFIG.channel, (err, count) => {
if (err) {
console.error('❌ Ошибка подписки:', err);
process.exit(1);
}
console.log(`✅ Подписка активна (${count} каналов)`);
console.log('⏳ Ожидание событий...\n');
});
// Обработка событий
redis.on('message', async (channel, message) => {
try {
const event = JSON.parse(message);
// Логируем событие
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] 📥 Событие:`);
console.log(` Type: ${event.type}`);
console.log(` Source: ${event.source}`);
console.log(` Path: ${event.path || event.filename}`);
// Обрабатываем только создание/изменение файлов
if (!['file_created', 'file_modified', 'file_update'].includes(event.type)) {
console.log(` ⏭️ Пропуск (не файловое событие)\n`);
return;
}
// Извлекаем путь файла
let filePath = event.path || event.filename;
// Для событий из S3 Monitor - путь уже правильный
// Для событий из Nextcloud - может быть без префикса
// Формируем путь для Nextcloud
let ncPath = filePath;
if (!ncPath.startsWith('/')) {
ncPath = '/admin/files/crm/' + ncPath;
}
console.log(` 🔄 Обновление кеша Nextcloud...`);
console.log(` Путь: ${ncPath}`);
// Обновляем кеш только для этого файла
const command = `docker exec -u www-data ${CONFIG.nextcloudContainer} php occ files:scan --path="${ncPath}" 2>&1`;
try {
const { stdout, stderr } = await execPromise(command);
if (stderr && !stderr.includes('Starting scan')) {
console.log(` ⚠️ Предупреждение: ${stderr}`);
}
console.log(` ✅ Кеш обновлён`);
// Дополнительно очищаем statcache для этой папки
const dirname = ncPath.substring(0, ncPath.lastIndexOf('/'));
const clearCommand = `docker exec -u www-data ${CONFIG.nextcloudContainer} php occ files:scan --path="${dirname}" --shallow 2>&1`;
await execPromise(clearCommand);
console.log(` ✅ Родительская папка обновлена\n`);
} catch (execError) {
console.error(` ❌ Ошибка обновления: ${execError.message}\n`);
}
} catch (error) {
console.error(`❌ Ошибка обработки события:`, error.message);
console.error(` Сообщение:`, message.substring(0, 200));
console.log('');
}
});
// Обработка ошибок
redis.on('error', (err) => {
console.error('❌ Redis ошибка:', err);
});
// Graceful shutdown
process.on('SIGINT', () => {
console.log('\n\n🛑 Остановка...');
redis.disconnect();
process.exit(0);
});
process.on('SIGTERM', () => {
console.log('\n\n🛑 Остановка...');
redis.disconnect();
process.exit(0);
});

View File

@@ -0,0 +1,130 @@
#!/usr/bin/env node
/**
* Nextcloud FileID Indexer
*
* Индексирует fileId из Nextcloud БД в Redis для быстрого доступа
* Структура: crm:nc:fileid:{path} => fileId
*/
const mysql = require('mysql2/promise');
const Redis = require('ioredis');
const CONFIG = {
nextcloud_db: {
host: '192.168.128.3',
user: 'nextcloud',
password: 'nextcloud_password',
database: 'nextcloud'
},
redis: {
host: '147.45.146.17',
port: 6379,
password: 'CRM_Redis_Pass_2025_Secure!'
},
// Индексируем только файлы из этих папок
pathPrefixes: [
'crm2/CRM_Active_Files/', // ИСПРАВЛЕНО: без 'files/' префикса!
'erv_app/'
],
indexInterval: 60000 // Обновляем индекс каждую минуту
};
const redis = new Redis(CONFIG.redis);
let connection = null;
async function connectDB() {
try {
connection = await mysql.createConnection(CONFIG.nextcloud_db);
console.log('✅ Подключились к БД Nextcloud');
} catch (err) {
console.error('❌ Ошибка подключения к БД:', err.message);
process.exit(1);
}
}
async function indexFiles() {
try {
console.log(`\n🔍 Индексация файлов... (${new Date().toISOString()})`);
let totalIndexed = 0;
for (const prefix of CONFIG.pathPrefixes) {
// Получаем все файлы из этой папки
const [rows] = await connection.execute(
'SELECT fileid, path, name, size, mtime FROM oc_filecache WHERE path LIKE ? AND mimetype != 2',
[prefix + '%']
);
console.log(` 📁 ${prefix}: найдено ${rows.length} файлов`);
// Индексируем в Redis
const pipeline = redis.pipeline();
for (const row of rows) {
const key = `crm:nc:fileid:${row.path}`;
const value = JSON.stringify({
fileId: row.fileid,
name: row.name,
size: row.size,
mtime: row.mtime
});
// Храним 24 часа (обновляется каждую минуту)
pipeline.setex(key, 86400, value);
totalIndexed++;
}
await pipeline.exec();
}
console.log(`✅ Проиндексировано: ${totalIndexed} файлов`);
} catch (err) {
console.error('❌ Ошибка индексации:', err.message);
console.error(err.stack);
}
}
async function start() {
console.log('🚀 Nextcloud FileID Indexer');
console.log('════════════════════════════════════════════════════════════════════════════════');
console.log(`🗄️ Nextcloud DB: ${CONFIG.nextcloud_db.host}/${CONFIG.nextcloud_db.database}`);
console.log(`📡 Redis: ${CONFIG.redis.host}:${CONFIG.redis.port}`);
console.log(`🔄 Интервал: ${CONFIG.indexInterval / 1000}с`);
console.log('════════════════════════════════════════════════════════════════════════════════\n');
await connectDB();
console.log('👂 Начинаем индексацию...\n');
// Первая индексация
await indexFiles();
// Периодическая индексация
setInterval(indexFiles, CONFIG.indexInterval);
}
// Запуск
redis.on('connect', () => {
console.log('✅ Подключились к Redis\n');
start();
});
redis.on('error', (err) => {
console.error('❌ Redis ошибка:', err.message);
});
process.on('SIGINT', async () => {
console.log('\n\n⛔ Остановка индексатора...');
if (connection) await connection.end();
redis.disconnect();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('\n\n⛔ Остановка индексатора...');
if (connection) await connection.end();
redis.disconnect();
process.exit(0);
});

1
crm_extensions/file_storage/node_modules/.bin/fxparser generated vendored Symbolic link
View File

@@ -0,0 +1 @@
../fast-xml-parser/src/cli/cli.js

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,100 @@
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [5.2.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v5.1.0...v5.2.0) (2023-10-16)
### Features
- support ESM artifacts in all packages ([#752](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/752)) ([e930ffb](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/e930ffba5cfef66dd242049e7d514ced232c1e3b))
# [5.1.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v5.0.0...v5.1.0) (2023-09-22)
### Bug Fixes
- Update tsc to 2.x ([#735](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/735)) ([782e0de](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/782e0de9f5fef41f694130580a69d940894b6b8c))
### Features
- Use @smithy/util-utf8 ([#730](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/730)) ([00fb851](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/00fb851ca3559d5a1f370f9256814de1210826b8)), closes [#699](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/699)
# [5.0.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v4.0.1...v5.0.0) (2023-07-13)
**Note:** Version bump only for package @aws-crypto/crc32
# [4.0.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v3.0.0...v4.0.0) (2023-02-20)
**Note:** Version bump only for package @aws-crypto/crc32
# [3.0.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v2.0.2...v3.0.0) (2023-01-12)
- feat!: replace Hash implementations with Checksum interface (#492) ([da43dc0](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/da43dc0fdf669d9ebb5bfb1b1f7c79e46c4aaae1)), closes [#492](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/492)
### BREAKING CHANGES
- All classes that implemented `Hash` now implement `Checksum`.
## [2.0.2](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v2.0.1...v2.0.2) (2022-09-07)
### Bug Fixes
- **#337:** update @aws-sdk/types ([#373](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/373)) ([b26a811](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/b26a811a392f5209c7ec7e57251500d4d78f97ff)), closes [#337](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/337)
## [2.0.1](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v2.0.0...v2.0.1) (2021-12-09)
**Note:** Version bump only for package @aws-crypto/crc32
# [2.0.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v1.2.2...v2.0.0) (2021-10-25)
**Note:** Version bump only for package @aws-crypto/crc32
## [1.2.2](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v1.2.1...v1.2.2) (2021-10-12)
### Bug Fixes
- **crc32c:** ie11 does not support Array.from ([#221](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/221)) ([5f49547](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/5f495472ab8988cf203e0f2a70a51f7e1fcd7e60))
## [1.2.1](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v1.2.0...v1.2.1) (2021-09-17)
**Note:** Version bump only for package @aws-crypto/crc32
# [1.2.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/v1.1.1...v1.2.0) (2021-09-17)
### Features
- Add AwsCrc32 Hash ([f5d7e81](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/f5d7e815fcbe0f8da1edb855fea3bd33eb1edc15))
# [1.1.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/@aws-crypto/crc32@1.0.0...@aws-crypto/crc32@1.1.0) (2021-08-11)
### Features
- Create CRC-32C implementation ([#201](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/201)) ([e43c7ec](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/e43c7ecd30d6499fa696f5839ecc30502a34b8b6))
# [1.0.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/@aws-crypto/crc32@1.0.0-alpha.0...@aws-crypto/crc32@1.0.0) (2020-10-22)
**Note:** Version bump only for package @aws-crypto/crc32
# [1.0.0-alpha.0](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/@aws-crypto/crc32@0.1.0-preview.4...@aws-crypto/crc32@1.0.0-alpha.0) (2020-02-07)
**Note:** Version bump only for package @aws-crypto/crc32
# [0.1.0-preview.4](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/@aws-crypto/crc32@0.1.0-preview.2...@aws-crypto/crc32@0.1.0-preview.4) (2020-01-16)
### Bug Fixes
- Changed package.json files to point to the right Git repo ([#9](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/9)) ([028245d](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/028245d72e642ca98d82226afb300eb154503c4a)), closes [#8](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/8)
- lerna version maintains package-lock ([#14](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/14)) ([2ef29e1](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/2ef29e13779703a5c9b32e93d18918fcb33b7272)), closes [#13](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/13)
# [0.1.0-preview.3](https://github.com/aws/aws-sdk-js-crypto-helpers/compare/@aws-crypto/crc32@0.1.0-preview.2...@aws-crypto/crc32@0.1.0-preview.3) (2019-11-15)
### Bug Fixes
- Changed package.json files to point to the right Git repo ([#9](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/9)) ([028245d](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/028245d72e642ca98d82226afb300eb154503c4a)), closes [#8](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/8)
- lerna version maintains package-lock ([#14](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/14)) ([2ef29e1](https://github.com/aws/aws-sdk-js-crypto-helpers/commit/2ef29e13779703a5c9b32e93d18918fcb33b7272)), closes [#13](https://github.com/aws/aws-sdk-js-crypto-helpers/issues/13)
# [0.1.0-preview.2](https://github.com/aws/aws-javascript-crypto-helpers/compare/@aws-crypto/crc32@0.1.0-preview.1...@aws-crypto/crc32@0.1.0-preview.2) (2019-10-30)
### Bug Fixes
- remove /src/ from .npmignore (for sourcemaps) ([#5](https://github.com/aws/aws-javascript-crypto-helpers/issues/5)) ([ec52056](https://github.com/aws/aws-javascript-crypto-helpers/commit/ec52056))

View File

@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,16 @@
# @aws-crypto/crc32
Pure JS implementation of CRC32 https://en.wikipedia.org/wiki/Cyclic_redundancy_check
## Usage
```
import { Crc32 } from '@aws-crypto/crc32';
const crc32Digest = (new Crc32).update(buffer).digest()
```
## Test
`npm test`

View File

@@ -0,0 +1,7 @@
import { SourceData, Checksum } from "@aws-sdk/types";
export declare class AwsCrc32 implements Checksum {
private crc32;
update(toHash: SourceData): void;
digest(): Promise<Uint8Array>;
reset(): void;
}

View File

@@ -0,0 +1,31 @@
"use strict";
// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
Object.defineProperty(exports, "__esModule", { value: true });
exports.AwsCrc32 = void 0;
var tslib_1 = require("tslib");
var util_1 = require("@aws-crypto/util");
var index_1 = require("./index");
var AwsCrc32 = /** @class */ (function () {
function AwsCrc32() {
this.crc32 = new index_1.Crc32();
}
AwsCrc32.prototype.update = function (toHash) {
if ((0, util_1.isEmptyData)(toHash))
return;
this.crc32.update((0, util_1.convertToBuffer)(toHash));
};
AwsCrc32.prototype.digest = function () {
return tslib_1.__awaiter(this, void 0, void 0, function () {
return tslib_1.__generator(this, function (_a) {
return [2 /*return*/, (0, util_1.numToUint8)(this.crc32.digest())];
});
});
};
AwsCrc32.prototype.reset = function () {
this.crc32 = new index_1.Crc32();
};
return AwsCrc32;
}());
exports.AwsCrc32 = AwsCrc32;
//# sourceMappingURL=aws_crc32.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"aws_crc32.js","sourceRoot":"","sources":["../../src/aws_crc32.ts"],"names":[],"mappings":";AAAA,oEAAoE;AACpE,sCAAsC;;;;AAGtC,yCAA4E;AAC5E,iCAAgC;AAEhC;IAAA;QACU,UAAK,GAAG,IAAI,aAAK,EAAE,CAAC;IAe9B,CAAC;IAbC,yBAAM,GAAN,UAAO,MAAkB;QACvB,IAAI,IAAA,kBAAW,EAAC,MAAM,CAAC;YAAE,OAAO;QAEhC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAA,sBAAe,EAAC,MAAM,CAAC,CAAC,CAAC;IAC7C,CAAC;IAEK,yBAAM,GAAZ;;;gBACE,sBAAO,IAAA,iBAAU,EAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,EAAC;;;KACxC;IAED,wBAAK,GAAL;QACE,IAAI,CAAC,KAAK,GAAG,IAAI,aAAK,EAAE,CAAC;IAC3B,CAAC;IACH,eAAC;AAAD,CAAC,AAhBD,IAgBC;AAhBY,4BAAQ"}

View File

@@ -0,0 +1,7 @@
export declare function crc32(data: Uint8Array): number;
export declare class Crc32 {
private checksum;
update(data: Uint8Array): this;
digest(): number;
}
export { AwsCrc32 } from "./aws_crc32";

View File

@@ -0,0 +1,108 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AwsCrc32 = exports.Crc32 = exports.crc32 = void 0;
var tslib_1 = require("tslib");
var util_1 = require("@aws-crypto/util");
function crc32(data) {
return new Crc32().update(data).digest();
}
exports.crc32 = crc32;
var Crc32 = /** @class */ (function () {
function Crc32() {
this.checksum = 0xffffffff;
}
Crc32.prototype.update = function (data) {
var e_1, _a;
try {
for (var data_1 = tslib_1.__values(data), data_1_1 = data_1.next(); !data_1_1.done; data_1_1 = data_1.next()) {
var byte = data_1_1.value;
this.checksum =
(this.checksum >>> 8) ^ lookupTable[(this.checksum ^ byte) & 0xff];
}
}
catch (e_1_1) { e_1 = { error: e_1_1 }; }
finally {
try {
if (data_1_1 && !data_1_1.done && (_a = data_1.return)) _a.call(data_1);
}
finally { if (e_1) throw e_1.error; }
}
return this;
};
Crc32.prototype.digest = function () {
return (this.checksum ^ 0xffffffff) >>> 0;
};
return Crc32;
}());
exports.Crc32 = Crc32;
// prettier-ignore
var a_lookUpTable = [
0x00000000, 0x77073096, 0xEE0E612C, 0x990951BA,
0x076DC419, 0x706AF48F, 0xE963A535, 0x9E6495A3,
0x0EDB8832, 0x79DCB8A4, 0xE0D5E91E, 0x97D2D988,
0x09B64C2B, 0x7EB17CBD, 0xE7B82D07, 0x90BF1D91,
0x1DB71064, 0x6AB020F2, 0xF3B97148, 0x84BE41DE,
0x1ADAD47D, 0x6DDDE4EB, 0xF4D4B551, 0x83D385C7,
0x136C9856, 0x646BA8C0, 0xFD62F97A, 0x8A65C9EC,
0x14015C4F, 0x63066CD9, 0xFA0F3D63, 0x8D080DF5,
0x3B6E20C8, 0x4C69105E, 0xD56041E4, 0xA2677172,
0x3C03E4D1, 0x4B04D447, 0xD20D85FD, 0xA50AB56B,
0x35B5A8FA, 0x42B2986C, 0xDBBBC9D6, 0xACBCF940,
0x32D86CE3, 0x45DF5C75, 0xDCD60DCF, 0xABD13D59,
0x26D930AC, 0x51DE003A, 0xC8D75180, 0xBFD06116,
0x21B4F4B5, 0x56B3C423, 0xCFBA9599, 0xB8BDA50F,
0x2802B89E, 0x5F058808, 0xC60CD9B2, 0xB10BE924,
0x2F6F7C87, 0x58684C11, 0xC1611DAB, 0xB6662D3D,
0x76DC4190, 0x01DB7106, 0x98D220BC, 0xEFD5102A,
0x71B18589, 0x06B6B51F, 0x9FBFE4A5, 0xE8B8D433,
0x7807C9A2, 0x0F00F934, 0x9609A88E, 0xE10E9818,
0x7F6A0DBB, 0x086D3D2D, 0x91646C97, 0xE6635C01,
0x6B6B51F4, 0x1C6C6162, 0x856530D8, 0xF262004E,
0x6C0695ED, 0x1B01A57B, 0x8208F4C1, 0xF50FC457,
0x65B0D9C6, 0x12B7E950, 0x8BBEB8EA, 0xFCB9887C,
0x62DD1DDF, 0x15DA2D49, 0x8CD37CF3, 0xFBD44C65,
0x4DB26158, 0x3AB551CE, 0xA3BC0074, 0xD4BB30E2,
0x4ADFA541, 0x3DD895D7, 0xA4D1C46D, 0xD3D6F4FB,
0x4369E96A, 0x346ED9FC, 0xAD678846, 0xDA60B8D0,
0x44042D73, 0x33031DE5, 0xAA0A4C5F, 0xDD0D7CC9,
0x5005713C, 0x270241AA, 0xBE0B1010, 0xC90C2086,
0x5768B525, 0x206F85B3, 0xB966D409, 0xCE61E49F,
0x5EDEF90E, 0x29D9C998, 0xB0D09822, 0xC7D7A8B4,
0x59B33D17, 0x2EB40D81, 0xB7BD5C3B, 0xC0BA6CAD,
0xEDB88320, 0x9ABFB3B6, 0x03B6E20C, 0x74B1D29A,
0xEAD54739, 0x9DD277AF, 0x04DB2615, 0x73DC1683,
0xE3630B12, 0x94643B84, 0x0D6D6A3E, 0x7A6A5AA8,
0xE40ECF0B, 0x9309FF9D, 0x0A00AE27, 0x7D079EB1,
0xF00F9344, 0x8708A3D2, 0x1E01F268, 0x6906C2FE,
0xF762575D, 0x806567CB, 0x196C3671, 0x6E6B06E7,
0xFED41B76, 0x89D32BE0, 0x10DA7A5A, 0x67DD4ACC,
0xF9B9DF6F, 0x8EBEEFF9, 0x17B7BE43, 0x60B08ED5,
0xD6D6A3E8, 0xA1D1937E, 0x38D8C2C4, 0x4FDFF252,
0xD1BB67F1, 0xA6BC5767, 0x3FB506DD, 0x48B2364B,
0xD80D2BDA, 0xAF0A1B4C, 0x36034AF6, 0x41047A60,
0xDF60EFC3, 0xA867DF55, 0x316E8EEF, 0x4669BE79,
0xCB61B38C, 0xBC66831A, 0x256FD2A0, 0x5268E236,
0xCC0C7795, 0xBB0B4703, 0x220216B9, 0x5505262F,
0xC5BA3BBE, 0xB2BD0B28, 0x2BB45A92, 0x5CB36A04,
0xC2D7FFA7, 0xB5D0CF31, 0x2CD99E8B, 0x5BDEAE1D,
0x9B64C2B0, 0xEC63F226, 0x756AA39C, 0x026D930A,
0x9C0906A9, 0xEB0E363F, 0x72076785, 0x05005713,
0x95BF4A82, 0xE2B87A14, 0x7BB12BAE, 0x0CB61B38,
0x92D28E9B, 0xE5D5BE0D, 0x7CDCEFB7, 0x0BDBDF21,
0x86D3D2D4, 0xF1D4E242, 0x68DDB3F8, 0x1FDA836E,
0x81BE16CD, 0xF6B9265B, 0x6FB077E1, 0x18B74777,
0x88085AE6, 0xFF0F6A70, 0x66063BCA, 0x11010B5C,
0x8F659EFF, 0xF862AE69, 0x616BFFD3, 0x166CCF45,
0xA00AE278, 0xD70DD2EE, 0x4E048354, 0x3903B3C2,
0xA7672661, 0xD06016F7, 0x4969474D, 0x3E6E77DB,
0xAED16A4A, 0xD9D65ADC, 0x40DF0B66, 0x37D83BF0,
0xA9BCAE53, 0xDEBB9EC5, 0x47B2CF7F, 0x30B5FFE9,
0xBDBDF21C, 0xCABAC28A, 0x53B39330, 0x24B4A3A6,
0xBAD03605, 0xCDD70693, 0x54DE5729, 0x23D967BF,
0xB3667A2E, 0xC4614AB8, 0x5D681B02, 0x2A6F2B94,
0xB40BBE37, 0xC30C8EA1, 0x5A05DF1B, 0x2D02EF8D,
];
var lookupTable = (0, util_1.uint32ArrayFrom)(a_lookUpTable);
var aws_crc32_1 = require("./aws_crc32");
Object.defineProperty(exports, "AwsCrc32", { enumerable: true, get: function () { return aws_crc32_1.AwsCrc32; } });
//# sourceMappingURL=index.js.map

Some files were not shown because too many files have changed in this diff Show More