ваше сообщение коммита
This commit is contained in:
406
SOFT_DESCRIPTION.md
Normal file
406
SOFT_DESCRIPTION.md
Normal file
@@ -0,0 +1,406 @@
|
||||
# DApp for Business
|
||||
|
||||
## Краткое описание
|
||||
**DApp for Business** — это современное веб3-приложение, позволяющее предпринимателям использовать цифровое юридическое лицо для обслуживания клиентов, приема безналичных платежей, безопасного управления активами и прозрачного учета с помощью смарт-контрактов и искусственного интеллекта.
|
||||
|
||||
---
|
||||
|
||||
## Основные возможности
|
||||
- Создание и управление цифровым юридическим лицом на блокчейне
|
||||
- Прием безналичных платежей (криптовалюта, стейблкоины)
|
||||
- Безопасное хранение и управление бизнес-активами через смарт-контракты
|
||||
- Прозрачный учет операций и автоматизация отчетности
|
||||
- Интеграция с ИИ для анализа данных и автоматизации бизнес-процессов
|
||||
- Управление клиентской базой и сервисами через децентрализованное приложение
|
||||
|
||||
---
|
||||
|
||||
## Целевая аудитория
|
||||
- Индивидуальные предприниматели
|
||||
- Малый и средний бизнес
|
||||
- Стартапы, работающие с цифровыми активами
|
||||
- Фрилансеры, желающие автоматизировать бизнес-процессы
|
||||
|
||||
---
|
||||
|
||||
## Преимущества
|
||||
- Полная прозрачность и доверие благодаря блокчейну
|
||||
- Автоматизация рутинных задач с помощью ИИ
|
||||
- Безопасное управление активами без посредников
|
||||
- Гибкая интеграция с внешними сервисами и кошельками
|
||||
- Масштабируемость и независимость от традиционных банковских систем
|
||||
|
||||
---
|
||||
|
||||
## Технические требования
|
||||
- Операционная система: Linux, macOS, Windows (рекомендуется WSL2 для Windows)
|
||||
- Docker
|
||||
- Node.js (v16+)
|
||||
- Yarn
|
||||
- Браузер с поддержкой Web3 (например, MetaMask)
|
||||
|
||||
---
|
||||
|
||||
## Установка и запуск
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
```bash
|
||||
git clone https://github.com/your-org/dapp-for-business.git
|
||||
cd dapp-for-business
|
||||
```
|
||||
2. Установите зависимости:
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
3. Запустите приложение в Docker:
|
||||
```bash
|
||||
docker-compose up --build
|
||||
```
|
||||
4. Откройте приложение в браузере по адресу: [http://localhost:3000](http://localhost:3000)
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
1. Зарегистрируйте цифровое юридическое лицо через интерфейс приложения.
|
||||
2. Подключите криптокошелек (MetaMask или другой Web3-кошелек).
|
||||
3. Настройте параметры бизнеса и добавьте клиентов.
|
||||
4. Начните принимать платежи и управлять активами через смарт-контракты.
|
||||
|
||||
---
|
||||
|
||||
## Структура проекта
|
||||
|
||||
- `frontend/` — клиентская часть на Vue.js (исходный код, конфиги, сборка, nginx)
|
||||
- `backend/` — серверная логика, API, работа с БД, интеграция со смарт-контрактами
|
||||
- `vector-search/` — сервис поиска и работы с векторными данными (Python, FastAPI)
|
||||
- `webssh-agent/` — сервис для работы с SSH-агентом через веб-интерфейс (Node.js)
|
||||
- `scripts/` — bash-скрипты для автоматизации (миграции, обновления DNS и др.)
|
||||
- `md/` — дополнительная документация и технические описания
|
||||
- `docs/` — документация и примеры
|
||||
- `docker-compose.yml`, `Dockerfile` — конфигурация контейнеризации и сборки
|
||||
- `setup.sh`, `clean-logs.sh` — вспомогательные скрипты для установки и обслуживания
|
||||
|
||||
---
|
||||
|
||||
## Часто задаваемые вопросы (FAQ)
|
||||
|
||||
- **Как подключить кошелек?**
|
||||
Используйте MetaMask или любой другой Web3-кошелек, следуя инструкции в интерфейсе приложения.
|
||||
|
||||
- **Какие криптовалюты поддерживаются?**
|
||||
Поддерживаются основные токены стандарта ERC-20 и стейблкоины.
|
||||
|
||||
- **Безопасно ли хранить активы в приложении?**
|
||||
Все операции выполняются через проверенные смарт-контракты, код которых открыт и доступен для аудита.
|
||||
|
||||
---
|
||||
|
||||
## Особенности развертывания и автономной работы
|
||||
|
||||
- Программное обеспечение с ИИ может работать полностью автономно на локальном устройстве без доступа к интернету после установки.
|
||||
- С локального устройства возможно открыть доступ к сервису для интернет-пользователей (например, через проброс портов или настройку прокси).
|
||||
- Приложение может быть установлено и сразу в облачный сервис (VPS, облачные платформы и т.д.) для круглосуточного доступа из любой точки мира.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## Управление настройками и доступом
|
||||
|
||||
- Управление всеми настройками приложения осуществляется через удобный веб-интерфейс.
|
||||
- Доступ к настройкам получает только пользователь, чей криптокошелек содержит специальный админ-токен управления смарт-контрактом.
|
||||
- После первого входа в приложение администратор может изменить стандартный админ-токен, создав собственный токен через специальную форму для деплоя смарт-контракта прямо в интерфейсе приложения.
|
||||
- Таким образом, ваш кошелек — это ваш смарт-контракт, ваши токены, ваш управляемый доступ и ваши активы: только вы контролируете все ключевые параметры и права.
|
||||
- Для получения бесплатных обновлений приложения в течение 5 лет администратору необходимо отправить первичный админ-токен на собственный смарт-контракт, созданный в приложении (операция выполняется через интерфейс).
|
||||
|
||||
> **Примечание:** Все смарт-контракты приложения построены на базе проверенных и безопасных библиотек OpenZeppelin.
|
||||
|
||||
---
|
||||
|
||||
## Инструкция по заполнению формы «Блокчейн-настройки»
|
||||
|
||||
1. **Создание нового DLE (Digital Legal Entity)**
|
||||
- **Имя DLE**: Введите уникальное название вашей цифровой организации (например, My DLE).
|
||||
- **Символ токена управления (GT)**: Укажите короткое обозначение токена (3-5 латинских символов, например, MDGT).
|
||||
|
||||
2. **Выбор кода деятельности (ISIC)**
|
||||
- Последовательно выберите секцию, раздел, группу и класс деятельности из выпадающих списков.
|
||||
- После выбора нужного кода нажмите «Добавить код деятельности». Можно добавить несколько кодов.
|
||||
|
||||
3. **Партнёры**
|
||||
- Для каждого партнёра укажите:
|
||||
- **Адрес партнёра** (Ethereum-адрес, например, 0x...).
|
||||
- **Сумму GT для партнёра** (количество токенов управления).
|
||||
- Для добавления нового партнёра используйте кнопку «Добавить партнёра».
|
||||
- Для удаления — «Удалить партнёра».
|
||||
|
||||
4. **RPC-конфигурации**
|
||||
- В разделе «Добавить новую RPC конфигурацию» выберите сеть из списка или укажите пользовательский ID и Chain ID.
|
||||
- Введите RPC URL (например, https://...).
|
||||
- При необходимости воспользуйтесь предложенным URL.
|
||||
- Нажмите «Добавить RPC» для сохранения конфигурации.
|
||||
|
||||
5. **Выбор сети для деплоя**
|
||||
- В выпадающем списке выберите блокчейн-сеть, в которую будет развёрнут смарт-контракт вашей организации.
|
||||
|
||||
6. **Приватный ключ для деплоя**
|
||||
- Введите приватный ключ деплоера (будет использоваться только для развертывания смарт-контракта).
|
||||
|
||||
7. **Пользовательские настройки газа (опционально)**
|
||||
- Если требуется, включите опцию «Использовать пользовательские настройки газа» и укажите лимит газа, максимальную и приоритетную комиссию.
|
||||
|
||||
8. **Сохранение и деплой**
|
||||
- После заполнения всех полей проверьте введённые данные.
|
||||
- Нажмите кнопку деплоя (или «Сохранить»), чтобы развернуть смарт-контракт и завершить настройку.
|
||||
|
||||
**Важно:**
|
||||
- Доступ к настройкам имеют только пользователи с админ-токеном в кошельке.
|
||||
- После деплоя вы сможете управлять организацией через смарт-контракт и веб-интерфейс.
|
||||
|
||||
---
|
||||
|
||||
## Управление DLE и модульными смарт-контрактами
|
||||
|
||||
После создания смарт-контракта с админ-токеном управления в разделе CRM приложения появляется интерфейс для управления функциями смарт-контракта и добавления модульных смарт-контрактов.
|
||||
|
||||
### Как управлять DLE через интерфейс
|
||||
|
||||
1. **Переход к управлению DLE**
|
||||
- В разделе CRM нажмите на блок "Управление DLE" и кнопку "Подробнее" или перейдите по адресу `/dle-management`.
|
||||
|
||||
2. **Выбор DLE**
|
||||
- В списке отобразятся все созданные вами DLE. Выберите нужную организацию для управления.
|
||||
|
||||
3. **Основная информация**
|
||||
- Вкладка "Основная информация" содержит сведения о названии, символе токена, местонахождении, кодах деятельности, дате создания и адресах смарт-контрактов (токен, таймлок, Governor). Можно скопировать адреса или открыть их в обозревателе блокчейна.
|
||||
|
||||
4. **Предложения**
|
||||
- На вкладке "Предложения" можно создавать новые предложения для управления DLE (например, изменение параметров, добавление участников и др.).
|
||||
- Для создания предложения заполните заголовок и описание, затем отправьте на голосование.
|
||||
- Участвуйте в голосовании по предложениям ("За" или "Против"). Статус предложения обновляется автоматически.
|
||||
|
||||
5. **Управление (Governance)**
|
||||
- Вкладка "Управление" позволяет просматривать и изменять параметры управления: порог предложения, кворум, задержку и период голосования.
|
||||
|
||||
6. **Модули**
|
||||
- Вкладка "Модули" предназначена для подключения дополнительных модульных смарт-контрактов (например, токенизация активов, мультиподпись, дивиденды, стейкинг, приём платежей и др.).
|
||||
- Для установки модуля нажмите "Установить" напротив нужного модуля. Для удаления — "Удалить".
|
||||
- Для модуля "Прием платежей" выберите токены, которые будут приниматься, и сохраните настройки.
|
||||
|
||||
7. **Удаление DLE**
|
||||
- При необходимости можно удалить DLE (доступно только администратору). Будьте внимательны: действие необратимо.
|
||||
|
||||
**Важно:**
|
||||
- Все действия с DLE и модулями доступны только пользователям с админ-токеном в кошельке.
|
||||
- Управление DLE реализовано через смарт-контракты на базе OpenZeppelin, что обеспечивает безопасность и прозрачность операций.
|
||||
|
||||
---
|
||||
|
||||
## Настройка и использование ИИ-моделей
|
||||
|
||||
После установки приложения администратор может:
|
||||
- Добавить ключи своих ИИ-моделей по подписке (например, OpenAI, Gemini и др.) в настройках приложения.
|
||||
- Скачать одну из доступных ИИ-моделей и развернуть её локально на своём устройстве для автоматизации обслуживания клиентов бизнеса без рисков утечки конфиденциальных данных.
|
||||
|
||||
Для дообучения добавленных моделей деталям вашего бизнеса предусмотрена возможность:
|
||||
- Создавать собственные таблицы в установленной базе данных через веб-интерфейс приложения.
|
||||
- Заполнять эти таблицы данными, которые ИИ-ассистент сможет использовать для генерации персонализированных сообщений клиентам бизнеса как в чате приложения, так и в других каналах коммуникации (например, email, Telegram и др.).
|
||||
|
||||
Это позволяет максимально адаптировать ИИ-ассистента под специфику вашего бизнеса и обеспечить безопасность корпоративных данных.
|
||||
|
||||
---
|
||||
|
||||
## Интерактивный обмен контентом, публикация и интеграция с ИИ
|
||||
|
||||
- Пользователи могут создавать и публиковать веб-страницы (о компании, продуктах, статьи) с помощью удобной формы.
|
||||
- Каждая страница получает уникальный URL, оптимизирована для SEO и доступна для поиска в интернете и ИИ-системах.
|
||||
- После публикации страницы можно делиться ими в корпоративном чате: появляется интерактивная карточка с кнопкой для просмотра содержимого, а также возможностью задать вопрос по содержимому страницы.
|
||||
- Страницы автоматически интегрируются с RAG: разбиваются на смысловые блоки, векторизуются и используются ИИ-ассистентом для поиска и генерации ответов.
|
||||
- При публикации можно выбрать интеграцию с RAG и добавить Q&A по теме страницы для последующего поиска.
|
||||
- Возможна публикация страниц в соцсетях и блогах (Medium, LinkedIn, Instagram, Telegram и др.) через API с выбором платформ.
|
||||
- Все компоненты реализованы с учётом безопасности и приватности данных, поддерживается удаление и редактирование страниц.
|
||||
- Используются современные RAG-фреймворки (LlamaIndex, LangChain) и актуальные модели для векторизации (OpenAI, Sentence Transformers и др.).
|
||||
- Страницы открыты для индексации поисковыми системами и ИИ-ботами, что обеспечивает максимальную видимость и доступность информации.
|
||||
|
||||
---
|
||||
|
||||
## Управление контактами в CRM
|
||||
|
||||
В разделе "Контакты" CRM администраторы приложения могут:
|
||||
- Отслеживать и управлять всеми выбранными контактами через удобную таблицу.
|
||||
- Использовать фильтры быстрого поиска по имени, email, Telegram, кошельку, типу контакта, дате и тегам.
|
||||
- Применять кнопки быстрых действий для массовой рассылки сообщений, импорта контактов, удаления выбранных записей.
|
||||
- Просматривать подробную информацию о каждом контакте, редактировать имя, email, Telegram, кошелек, язык общения.
|
||||
- Добавлять и удалять теги для контакта, а также создавать новые теги прямо из интерфейса.
|
||||
- Блокировать и разблокировать пользователей, полностью удалять контакт.
|
||||
- Вести чат с каждым контактом прямо в интерфейсе приложения, использовать ИИ-ассистента для генерации ответов.
|
||||
- Все изменения и действия с контактами доступны только администраторам.
|
||||
|
||||
Это позволяет эффективно управлять клиентской базой, быстро находить нужные контакты и автоматизировать коммуникации с помощью встроенных инструментов.
|
||||
|
||||
|
||||
## Контакты и поддержка
|
||||
|
||||
- Email: info@hb3-accelerator.com
|
||||
- Telegram: @yourproject_support
|
||||
- Сайт: [https://hb3-accelerator.com](https://hb3-accelerator.com)
|
||||
|
||||
---
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT License
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
офер:
|
||||
|
||||
Привет! Я **Алекс** из венчурного фонда **HB3 Accelerator**.
|
||||
|
||||
Если ваша компания:
|
||||
* использует **CRM** для обслуживания клиентов и имеет штат **продавцов и бухгалтеров**;
|
||||
* нуждается в **безналичных платежах без лимитов, ограничений и с низкими комиссиями**;
|
||||
* ценит **прозрачный учёт и свободное управление активами**,
|
||||
|
||||
то я буду рад предложить вам **программное обеспечение с ИИ и смарт-контрактами**.
|
||||
|
||||
|
||||
примеры вопросов оо клиентов:
|
||||
|
||||
1. Как ваш софт интегрируется с уже существующими CRM-системами? Например, у нас сейчас используется 1С и Bitrix24.
|
||||
2. Какие конкретно задачи автоматизирует ваш ИИ? Это только аналитика или он может, например, помогать продавцам в реальном времени?
|
||||
3. Как обеспечивается безопасность и прозрачность работы со смарт-контрактами? Кто их разрабатывает и кто несёт ответственность в случае ошибки?
|
||||
4. Какой у вас опыт внедрения подобных решений в СНГ? Есть ли кейсы в моей отрасли (например, розничная торговля)?
|
||||
5. Какой порядок внедрения: сколько времени занимает интеграция, кто обучает персонал?
|
||||
6. Какие комиссии по безналичным платежам и с какими банками/платёжными системами вы работаете?
|
||||
7. Какой уровень поддержки вы предоставляете после внедрения?
|
||||
|
||||
|
||||
ответ:
|
||||
|
||||
### Наше комплексное решение для вашего бизнеса
|
||||
|
||||
Наше программное обеспечение включает **встроенную CRM-систему**, куда вы можете легко загрузить необходимые контакты из ваших текущих CRM для **омниканального обслуживания**.
|
||||
|
||||
**ИИ-ассистент**, интегрированный в наш софт, способен обучаться работе с сегментами ваших клиентов и поставщиков. Обучение происходит на основе правил, которые вы устанавливаете и загружаете в **векторную базу данных**. Эта база надёжно хранит ваши конфиденциальные данные либо на **локальном**, либо на **облачном сервере**.
|
||||
|
||||
Мы — **молодой стартап**, представляющий свой первый технологический продукт. Внедрение нашего решения занимает от **нескольких часов до одного года**, в зависимости от сложности интеграции.
|
||||
|
||||
---
|
||||
|
||||
### Условия сотрудничества
|
||||
|
||||
Мы предоставляем **5 лет обновлений** для вашего программного обеспечения.
|
||||
|
||||
Кроме того, если **в течение первого года** мы не сможем настроить софт под индивидуальные потребности вашего бизнеса, вы можете получить **возврат 70% от стоимости**.
|
||||
|
||||
|
||||
вопросы:
|
||||
|
||||
Какой у вас опыт работы с интеграцией в 1С и Bitrix24? Есть ли готовые модули или потребуется доработка под нас?
|
||||
Как реализована миграция данных из старых CRM? Кто этим занимается — ваша команда или наши специалисты?
|
||||
Какой стек технологий вы используете для ИИ и смарт-контрактов? На каких блокчейн-платформах строится ваша система?
|
||||
Какой SLA по поддержке и реагированию на инциденты? Есть ли круглосуточная поддержка?
|
||||
Как лицензируется продукт: это подписка, разовая покупка или гибридная модель?
|
||||
Какой порядок оплаты: аванс, поэтапно, после внедрения?
|
||||
Как вы обеспечиваете соответствие требованиям законодательства РФ/СНГ по хранению и обработке персональных данных?
|
||||
Есть ли демо-доступ или пилотный проект, чтобы мы могли протестировать систему на реальных данных?
|
||||
|
||||
ответы :
|
||||
|
||||
---
|
||||
|
||||
### Особенности нашего решения
|
||||
|
||||
Мы не занимаемся интеграциями с **устаревшими CRM-системами**. Однако ваши сотрудники смогут легко выполнить **миграцию данных** при поддержке нашего **ИИ-ассистента**.
|
||||
|
||||
Наш **ИИ-ассистент** обеспечивает **круглосуточную поддержку**. Инциденты с нашей стороны **исключены**, поскольку приобретаемое вами программное обеспечение является **полностью локальным решением**.
|
||||
|
||||
---
|
||||
|
||||
### Безопасность и конфиденциальность данных
|
||||
|
||||
Хранение персональных данных **соответствует требованиям законодательства**. Все данные **зашифрованы** и хранятся **на вашей территории**.
|
||||
|
||||
---
|
||||
|
||||
### Технологический стек
|
||||
|
||||
Вот основные технологии, которые мы используем в нашем **backend-решении**:
|
||||
|
||||
* **Искусственный интеллект и машинное обучение:** `@anthropic-ai/sdk`, `@google/genai`, `@langchain/community`, `@langchain/core`, `@langchain/ollama`, `langchain`, `openai`
|
||||
* **Блокчейн и смарт-контракты:** `@openzeppelin/contracts`, `ethers`, `siwe`, `viem`
|
||||
* **Веб-сервер и API:** `express`, `cors`, `helmet`, `express-rate-limit`
|
||||
* **Базы данных:** `pg`, `connect-pg-simple`
|
||||
* **Безопасность:** `csurf`, `express-session`, `session-file-store`, `cookie`
|
||||
* **Обработка электронной почты:** `imap`, `mailparser`, `nodemailer`
|
||||
* **Мессенджеры:** `node-telegram-bot-api`, `telegraf`, `ws`
|
||||
* **Утилиты и вспомогательные библиотеки:** `archiver`, `axios`, `cron`, `dotenv`, `multer`, `node-cron`, `semver`, `winston`
|
||||
* **Инструменты разработки:** `nodemon`, `eslint`, `prettier`, `hardhat`, `mocha`, `chai`, `typescript`
|
||||
|
||||
|
||||
вопросы:
|
||||
|
||||
1. Как реализована поддержка и обновления: если решение полностью локальное, как будут устанавливаться апдейты и исправления? Это делается через удалённый доступ, или вы предоставляете инструкции для нашей IT-команды?
|
||||
2. Какой механизм резервного копирования и восстановления данных предусмотрен в вашем решении?
|
||||
3. Если потребуется интеграция с внешними сервисами (например, платёжные шлюзы, государственные системы учёта), возможно ли это реализовать на вашей платформе?
|
||||
4. Какой минимальный и рекомендуемый состав IT-специалистов нужен для поддержки вашего ПО на стороне клиента?
|
||||
5. Какой порядок лицензирования используемых вами open-source библиотек и SDK? Нет ли рисков для конечного пользователя?
|
||||
6. Предусмотрена ли возможность кастомизации интерфейса и бизнес-логики под наши процессы?
|
||||
7. Какой минимальный объём внедрения (по стоимости или количеству пользователей) вы рассматриваете?
|
||||
|
||||
|
||||
ответ: Конечно! Вот ответы на вопросы предпринимателя, составленные на основе документации к продукту DApp for Business:
|
||||
|
||||
---
|
||||
|
||||
### 1. Как реализована поддержка и обновления: если решение полностью локальное, как будут устанавливаться апдейты и исправления? Это делается через удалённый доступ, или вы предоставляете инструкции для нашей IT-команды?
|
||||
|
||||
**Ответ:**
|
||||
Обновления предоставляются бесплатно в течение 5 лет. Программное обеспечение устанавливается и работает полностью локально, без необходимости постоянного интернет-доступа. Для установки обновлений вы можете использовать предоставленные bash-скрипты (`setup.sh`, `clean-logs.sh`) и инструкции из документации. При необходимости можно открыть доступ к сервису для интернет-пользователей (например, через проброс портов или прокси), но это не обязательно. Все инструкции по обновлению и обслуживанию доступны вашей IT-команде, удалённый доступ не требуется.
|
||||
|
||||
---
|
||||
|
||||
### 2. Какой механизм резервного копирования и восстановления данных предусмотрен в вашем решении?
|
||||
|
||||
**Ответ:**
|
||||
В документации прямо не описан отдельный модуль резервного копирования, однако, поскольку все данные хранятся локально (или на вашем облачном сервере), вы полностью контролируете процесс бэкапа. Используются стандартные базы данных (`pg` — PostgreSQL), для которых легко настраиваются регулярные резервные копии с помощью штатных инструментов PostgreSQL или через Docker-скрипты. Также можно использовать bash-скрипты из папки `scripts/` для автоматизации резервного копирования и восстановления.
|
||||
|
||||
---
|
||||
|
||||
### 3. Если потребуется интеграция с внешними сервисами (например, платёжные шлюзы, государственные системы учёта), возможно ли это реализовать на вашей платформе?
|
||||
|
||||
**Ответ:**
|
||||
Да, гибкая интеграция с внешними сервисами и кошельками поддерживается. Приложение масштабируемо и не зависит от традиционных банковских систем. В разделе "Модули" можно подключать дополнительные смарт-контракты, в том числе для приёма платежей, токенизации активов и других задач. Также реализована возможность публикации и интеграции с внешними платформами через API (например, соцсети, мессенджеры, внешние сервисы учёта).
|
||||
|
||||
---
|
||||
|
||||
### 4. Какой минимальный и рекомендуемый состав IT-специалистов нужен для поддержки вашего ПО на стороне клиента?
|
||||
|
||||
**Ответ:**
|
||||
Для базовой эксплуатации достаточно одного системного администратора или DevOps-специалиста, знакомого с Docker, Linux и базовыми инструментами Node.js/PostgreSQL. Вся установка и обслуживание автоматизированы скриптами и не требуют глубоких знаний в программировании. Для расширенной кастомизации или интеграции с внешними сервисами может потребоваться разработчик с опытом работы с Node.js, смарт-контрактами (Solidity) и API.
|
||||
|
||||
---
|
||||
|
||||
### 5. Какой порядок лицензирования используемых вами open-source библиотек и SDK? Нет ли рисков для конечного пользователя?
|
||||
|
||||
**Ответ:**
|
||||
Программное обеспечение распространяется по лицензии MIT, что гарантирует отсутствие ограничений для конечного пользователя. Все используемые библиотеки (например, OpenZeppelin, LangChain, Express, PostgreSQL и др.) также имеют открытые лицензии (MIT, Apache 2.0 и аналогичные), что исключает юридические риски для вашего бизнеса.
|
||||
|
||||
---
|
||||
|
||||
### 6. Предусмотрена ли возможность кастомизации интерфейса и бизнес-логики под наши процессы?
|
||||
|
||||
**Ответ:**
|
||||
Да, архитектура приложения модульная и предусматривает возможность кастомизации. Вы можете добавлять собственные таблицы в базу данных через веб-интерфейс, подключать новые модули смарт-контрактов, настраивать параметры бизнеса, интегрировать свои ИИ-модели и дообучать их на ваших данных. Интерфейс реализован на Vue.js и может быть доработан под ваши задачи.
|
||||
|
||||
---
|
||||
|
||||
### 7. Какой минимальный объём внедрения (по стоимости или количеству пользователей) вы рассматриваете?
|
||||
|
||||
**Ответ:**
|
||||
В документации не указаны ограничения по минимальному объёму внедрения или количеству пользователей. Продукт ориентирован как на индивидуальных предпринимателей, так и на малый и средний бизнес, стартапы и фрилансеров. Вы можете начать с одного пользователя и масштабировать решение по мере роста бизнеса.
|
||||
|
||||
16
backend/db/migrations/048_add_order_to_user_rows.sql
Normal file
16
backend/db/migrations/048_add_order_to_user_rows.sql
Normal file
@@ -0,0 +1,16 @@
|
||||
-- 048_add_order_to_user_rows.sql
|
||||
-- Добавляет поле order в user_rows для поддержки сортировки строк
|
||||
|
||||
ALTER TABLE user_rows ADD COLUMN "order" INTEGER DEFAULT 0;
|
||||
|
||||
-- Проставить уникальные значения order для существующих строк (по id)
|
||||
DO $$
|
||||
DECLARE
|
||||
r RECORD;
|
||||
idx INTEGER := 1;
|
||||
BEGIN
|
||||
FOR r IN SELECT id FROM user_rows ORDER BY id LOOP
|
||||
UPDATE user_rows SET "order" = idx WHERE id = r.id;
|
||||
idx := idx + 1;
|
||||
END LOOP;
|
||||
END$$;
|
||||
@@ -93,7 +93,6 @@
|
||||
"semver": "^7.7.1",
|
||||
"**/utf7/semver": "^7.7.1",
|
||||
"tar-fs": "^3.0.0",
|
||||
"parse-duration": "^1.1.0",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"nanoid": "^5.0.0",
|
||||
"brace-expansion": "^2.0.1"
|
||||
|
||||
@@ -365,6 +365,27 @@ router.patch('/:id', async (req, res, next) => {
|
||||
}
|
||||
});
|
||||
|
||||
// PATCH: массовое обновление порядка строк (order)
|
||||
router.patch('/:id/rows/order', async (req, res, next) => {
|
||||
try {
|
||||
const tableId = req.params.id;
|
||||
const { order } = req.body; // order: [{rowId, order}, ...]
|
||||
if (!Array.isArray(order)) {
|
||||
return res.status(400).json({ error: 'order должен быть массивом' });
|
||||
}
|
||||
for (const item of order) {
|
||||
if (!item.rowId || typeof item.order !== 'number') continue;
|
||||
await db.getQuery()(
|
||||
'UPDATE user_rows SET "order" = $1, updated_at = NOW() WHERE id = $2 AND table_id = $3',
|
||||
[item.order, item.rowId, tableId]
|
||||
);
|
||||
}
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
|
||||
// Получить id колонок с purpose 'question' и 'answer'
|
||||
async function getQuestionAnswerColumnIds(tableId) {
|
||||
const { rows } = await db.getQuery()(
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
<template v-if="column.type === 'multiselect'">
|
||||
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
|
||||
<span v-if="selectedMultiNames.length">{{ selectedMultiNames.join(', ') }}</span>
|
||||
<span v-else style="color:#bbb">—</span>
|
||||
<span v-else class="cell-plus-icon" title="Добавить">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
|
||||
<rect x="8" y="4" width="2" height="10" rx="1" fill="#4f8cff"/>
|
||||
<rect x="4" y="8" width="10" height="2" rx="1" fill="#4f8cff"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="tags-cell-edit">
|
||||
<div class="tags-multiselect">
|
||||
@@ -23,7 +29,13 @@
|
||||
<template v-else-if="column.type === 'relation'">
|
||||
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
|
||||
<span v-if="selectedRelationName">{{ selectedRelationName }}</span>
|
||||
<span v-else style="color:#bbb">—</span>
|
||||
<span v-else class="cell-plus-icon" title="Добавить">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
|
||||
<rect x="8" y="4" width="2" height="10" rx="1" fill="#4f8cff"/>
|
||||
<rect x="4" y="8" width="10" height="2" rx="1" fill="#4f8cff"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="tags-cell-edit">
|
||||
<select v-model="editRelationValue" class="notion-input">
|
||||
@@ -42,13 +54,19 @@
|
||||
<template v-else-if="column.type === 'multiselect-relation'">
|
||||
<div v-if="!editing" @click="editing = true" class="tags-cell-view">
|
||||
<span v-if="selectedMultiRelationNames.length">{{ selectedMultiRelationNames.map(prettyDisplay).join(', ') }}</span>
|
||||
<span v-else>{{ prettyDisplay(localValue) }}</span>
|
||||
<span v-else class="cell-plus-icon" title="Добавить">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
|
||||
<rect x="8" y="4" width="2" height="10" rx="1" fill="#4f8cff"/>
|
||||
<rect x="4" y="8" width="10" height="2" rx="1" fill="#4f8cff"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="tags-cell-edit">
|
||||
<div class="tags-multiselect">
|
||||
<div v-for="option in multiRelationOptions" :key="option.id" class="tag-option">
|
||||
<input type="checkbox" :id="'cell-multirel-' + option.id + '-' + rowId" :value="String(option.id)" v-model="editMultiRelationValues" />
|
||||
<label :for="'cell-multirel-' + option.id + '-' + rowId">{{ prettyDisplay(option.display) }}</label>
|
||||
<label :for="'cell-multirel-' + option.id + '-' + rowId">{{ prettyDisplay(option.display, multiRelationOptions.value) }}</label>
|
||||
<button class="delete-tag-btn" @click.prevent="deleteTag(option.id)" title="Удалить тег">×</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -67,20 +85,33 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-if="!editing" class="cell-view-value" @click="editing = true">
|
||||
<span v-if="isArrayString(localValue)">{{ parseArrayString(localValue).join(', ') }}</span>
|
||||
<input
|
||||
<span v-else-if="localValue">{{ localValue }}</span>
|
||||
<span v-else class="cell-plus-icon" title="Добавить">
|
||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none">
|
||||
<circle cx="9" cy="9" r="8" fill="#f3f4f6" stroke="#b6c6e6"/>
|
||||
<rect x="8" y="4" width="2" height="10" rx="1" fill="#4f8cff"/>
|
||||
<rect x="4" y="8" width="10" height="2" rx="1" fill="#4f8cff"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
v-else
|
||||
v-model="localValue"
|
||||
@blur="save"
|
||||
@keyup.enter="save"
|
||||
@blur="saveAndExit"
|
||||
@keyup.enter="saveAndExit"
|
||||
:placeholder="column.name"
|
||||
class="cell-input"
|
||||
autofocus
|
||||
ref="textareaRef"
|
||||
@input="autoResize"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onMounted, computed } from 'vue';
|
||||
import { ref, watch, onMounted, computed, nextTick } from 'vue';
|
||||
import tablesService from '../../services/tablesService';
|
||||
const props = defineProps(['rowId', 'column', 'cellValues']);
|
||||
const emit = defineEmits(['update']);
|
||||
@@ -110,6 +141,27 @@ const selectedMultiRelationNames = ref([]);
|
||||
const showAddTagInput = ref(false);
|
||||
const newTagName = ref('');
|
||||
|
||||
const textareaRef = ref(null);
|
||||
|
||||
function autoResize() {
|
||||
const ta = textareaRef.value;
|
||||
if (ta) {
|
||||
ta.style.height = 'auto';
|
||||
ta.style.height = ta.scrollHeight + 'px';
|
||||
}
|
||||
}
|
||||
|
||||
watch(editing, (val) => {
|
||||
if (val) {
|
||||
nextTick(() => {
|
||||
if (textareaRef.value) {
|
||||
autoResize();
|
||||
setTimeout(() => autoResize(), 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Добавляем watch для отслеживания изменений в мультисвязях
|
||||
watch(editMultiRelationValues, (newValues, oldValues) => {
|
||||
console.log('[editMultiRelationValues] changed from:', oldValues, 'to:', newValues);
|
||||
@@ -472,6 +524,11 @@ function save() {
|
||||
emit('update', localValue.value);
|
||||
}
|
||||
|
||||
function saveAndExit() {
|
||||
save();
|
||||
editing.value = false;
|
||||
}
|
||||
|
||||
function isArrayString(val) {
|
||||
if (typeof val !== 'string') return false;
|
||||
try {
|
||||
@@ -482,36 +539,62 @@ function isArrayString(val) {
|
||||
}
|
||||
}
|
||||
function parseArrayString(val) {
|
||||
if (typeof val !== 'string') return [];
|
||||
// Пробуем как JSON
|
||||
try {
|
||||
const arr = JSON.parse(val);
|
||||
return Array.isArray(arr) ? arr : [val];
|
||||
} catch {
|
||||
return [val];
|
||||
if (Array.isArray(arr)) return arr.map(String);
|
||||
} catch {}
|
||||
// Пробуем как PostgreSQL-массив
|
||||
if (/^\{.*\}$/.test(val)) {
|
||||
return val.replace(/[{}\s"]/g, '').split(',').filter(Boolean);
|
||||
}
|
||||
// Если просто строка
|
||||
if (val.trim().length > 0) return [val.trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function prettyDisplay(val) {
|
||||
if (isArrayString(val)) {
|
||||
return parseArrayString(val).join(', ');
|
||||
function prettyDisplay(val, optionsArr) {
|
||||
const arr = parseArrayString(val);
|
||||
if (!arr.length) return '—';
|
||||
if (optionsArr && Array.isArray(optionsArr)) {
|
||||
// Для relation/multiselect-relation ищу display по id
|
||||
return arr.map(id => {
|
||||
const found = optionsArr.find(opt => String(opt.id) === String(id) || String(opt) === String(id));
|
||||
return found ? (found.display || found) : id;
|
||||
}).join(', ');
|
||||
}
|
||||
return val;
|
||||
return arr.join(', ');
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cell-input {
|
||||
width: 100%;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 5px;
|
||||
padding: 0.3em 0.5em;
|
||||
font-size: 1em;
|
||||
background: #fff;
|
||||
transition: border 0.2s;
|
||||
border: none !important;
|
||||
outline: none !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
padding: 0 !important;
|
||||
resize: none !important;
|
||||
width: 100% !important;
|
||||
min-height: 32px;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
}
|
||||
.cell-input:focus {
|
||||
border: 1.5px solid #2ecc40;
|
||||
outline: none;
|
||||
}
|
||||
.tags-cell-view, .tags-cell-edit, .lookup-cell-view, .tag-option, .multi-relation-option, .add-multiselect-option, .add-tag-form, .multi-relation-options, .multi-relation-edit, .multi-relation-actions, .action-buttons {
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
height: auto !important;
|
||||
min-height: 1.7em;
|
||||
align-items: flex-start !important;
|
||||
vertical-align: top !important;
|
||||
overflow: visible !important;
|
||||
}
|
||||
.tags-cell-view {
|
||||
min-height: 1.7em;
|
||||
cursor: pointer;
|
||||
@@ -706,4 +789,30 @@ function prettyDisplay(val) {
|
||||
.add-tag-block {
|
||||
margin: 0.7em 0;
|
||||
}
|
||||
.cell-view-value {
|
||||
display: block;
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: anywhere !important;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s;
|
||||
min-height: 32px;
|
||||
}
|
||||
.cell-view-value:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
.cell-plus-icon {
|
||||
color: #b6c6e6;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2em;
|
||||
transition: color 0.15s;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.cell-plus-icon:hover {
|
||||
color: #4f8cff;
|
||||
}
|
||||
</style>
|
||||
@@ -2,16 +2,13 @@
|
||||
<div class="user-table-header" v-if="tableMeta">
|
||||
<h2>{{ tableMeta.name }}</h2>
|
||||
<div class="table-desc">{{ tableMeta.description }}</div>
|
||||
<div class="table-header-actions" style="display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-top: 8px; margin-bottom: 18px;">
|
||||
<el-button type="danger" :disabled="!selectedRows.length" @click="deleteSelectedRows">Удалить выбранные</el-button>
|
||||
<span v-if="selectedRows.length">Выбрано: {{ selectedRows.length }}</span>
|
||||
<button v-if="isAdmin" class="rebuild-btn" @click="rebuildIndex" :disabled="rebuilding">
|
||||
{{ rebuilding ? 'Пересборка...' : 'Пересобрать индекс' }}
|
||||
</button>
|
||||
<span v-if="rebuildStatus" :class="['rebuild-status', rebuildStatus.success ? 'success' : 'error']">
|
||||
{{ rebuildStatus.message }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Фильтры на Element Plus -->
|
||||
<div class="table-filters-el" v-if="relationFilterDefs.length">
|
||||
<!-- Только фильтры по multiselect-relation -->
|
||||
<el-button @click="resetFilters" type="default" icon="el-icon-refresh">Сбросить фильтры</el-button>
|
||||
<template v-for="def in relationFilterDefs" :key="def.col.id">
|
||||
<el-select
|
||||
v-model="relationFilters[def.filterKey]"
|
||||
@@ -24,54 +21,91 @@
|
||||
<el-option v-for="opt in def.options" :key="opt.id" :label="opt.display" :value="opt.id" />
|
||||
</el-select>
|
||||
</template>
|
||||
<el-button @click="resetFilters" type="default" icon="el-icon-refresh">Сбросить фильтры</el-button>
|
||||
</div>
|
||||
<span v-if="rebuildStatus" :class="['rebuild-status', rebuildStatus.success ? 'success' : 'error']">
|
||||
{{ rebuildStatus.message }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- Удаляю .table-filters-el -->
|
||||
<div class="notion-table-wrapper">
|
||||
<table class="notion-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="col in columns" :key="col.id" @dblclick="editColumn(col)" class="th-col">
|
||||
<span v-if="!editingCol || editingCol.id !== col.id">{{ col.name }}</span>
|
||||
<input v-else v-model="colEditValue" @blur="saveColEdit(col)" @keyup.enter="saveColEdit(col)" @keyup.esc="cancelColEdit" class="notion-input" />
|
||||
<el-table
|
||||
:data="filteredRows"
|
||||
border
|
||||
style="width: 100%"
|
||||
:header-cell-style="{ background: '#f3f4f6', fontWeight: 600 }"
|
||||
:cell-style="{ whiteSpace: 'normal', wordBreak: 'break-word', minWidth: '80px' }"
|
||||
:row-class-name="() => 'el-table-row-custom'"
|
||||
row-key="id"
|
||||
@selection-change="handleSelectionChange"
|
||||
>
|
||||
<el-table-column type="selection" width="48" fixed="left" />
|
||||
<el-table-column
|
||||
v-for="col in columns"
|
||||
:key="col.id"
|
||||
:prop="'col_' + col.id"
|
||||
:label="col.name"
|
||||
:resizable="true"
|
||||
:min-width="120"
|
||||
:show-overflow-tooltip="false"
|
||||
>
|
||||
<template #header="{ column }">
|
||||
<template v-if="editingCol && editingCol.id === col.id">
|
||||
<input v-model="colEditValue" class="notion-input" style="width: 90px; display: inline-block;" @keyup.enter="saveColEdit(col)" />
|
||||
<button class="save-btn" @click="saveColEdit(col)">Сохранить</button>
|
||||
<button class="cancel-btn" @click="cancelColEdit">Отмена</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>{{ col.name }}</span>
|
||||
<button class="col-menu" @click.stop="openColMenu(col, $event)">⋮</button>
|
||||
<!-- Меню столбца -->
|
||||
<div v-if="openedColMenuId === col.id" class="context-menu" :style="colMenuStyle">
|
||||
<button class="menu-item" @click="startRenameCol(col)">Переименовать</button>
|
||||
<button class="menu-item" @click="startChangeTypeCol(col)">Изменить тип</button>
|
||||
<button class="menu-item danger" @click="deleteColumn(col)">Удалить</button>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<button class="add-col" @click="showAddColModal = true">+</button>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in filteredRows" :key="row.id">
|
||||
<td v-for="col in columns" :key="col.id">
|
||||
</template>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<TableCell
|
||||
:rowId="row.id"
|
||||
:column="col"
|
||||
:cellValues="cellValues"
|
||||
@update="val => saveCellValue(row.id, col.id, val)"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- Было два столбца: один для плюса, один для ⋮. Теперь объединяем: -->
|
||||
<el-table-column
|
||||
label=""
|
||||
width="48"
|
||||
align="center"
|
||||
fixed="right"
|
||||
class-name="add-col-header"
|
||||
:resizable="false"
|
||||
>
|
||||
<template #header>
|
||||
<button class="add-col-btn" @click="addColumn" title="Добавить столбец">
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="11" cy="11" r="10" fill="#f3f4f6" stroke="#b6c6e6"/>
|
||||
<rect x="10" y="5.5" width="2" height="11" rx="1" fill="#4f8cff"/>
|
||||
<rect x="5.5" y="10" width="11" height="2" rx="1" fill="#4f8cff"/>
|
||||
</svg>
|
||||
</button>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<button class="row-menu" @click.stop="openRowMenu(row, $event)">⋮</button>
|
||||
<!-- Меню строки -->
|
||||
<teleport to="body">
|
||||
<div v-if="openedRowMenuId === row.id" class="context-menu" :style="rowMenuStyle">
|
||||
<button class="menu-item" @click="addRowAfter(row)">Добавить строку</button>
|
||||
<button class="menu-item" @click="moveRowUp(row)" :disabled="rows.findIndex(r => r.id === row.id) === 0">Переместить вверх</button>
|
||||
<button class="menu-item" @click="moveRowDown(row)" :disabled="rows.findIndex(r => r.id === row.id) === rows.length - 1">Переместить вниз</button>
|
||||
<button class="menu-item danger" @click="deleteRow(row)">Удалить</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td :colspan="columns.length + 1">
|
||||
<button class="add-row" @click="addRow">+ Добавить строку</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- Оверлей для закрытия меню по клику вне -->
|
||||
</teleport>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<teleport to="body">
|
||||
<div v-if="openedColMenuId" class="context-menu" :style="colMenuStyle">
|
||||
<button class="menu-item" @click="editColumn(columns.find(c => c.id === openedColMenuId))">Редактировать</button>
|
||||
<button class="menu-item danger" @click="deleteColumn(columns.find(c => c.id === openedColMenuId))">Удалить</button>
|
||||
<!-- <button class="menu-item" @click="addColumn">Добавить столбец</button> -->
|
||||
</div>
|
||||
</teleport>
|
||||
<div v-if="openedColMenuId || openedRowMenuId" class="menu-overlay" @click="closeMenus"></div>
|
||||
<!-- Модалка добавления столбца -->
|
||||
<div v-if="showAddColModal" class="modal-backdrop">
|
||||
@@ -136,6 +170,19 @@ const tableMeta = ref(null);
|
||||
// const selectedProduct = ref('');
|
||||
// const productOptions = ref([]);
|
||||
const filteredRows = ref([]);
|
||||
const selectedRows = ref([]);
|
||||
function handleSelectionChange(val) {
|
||||
selectedRows.value = val;
|
||||
}
|
||||
async function deleteSelectedRows() {
|
||||
if (!selectedRows.value.length) return;
|
||||
if (!confirm(`Удалить выбранные строки (${selectedRows.value.length})?`)) return;
|
||||
for (const row of selectedRows.value) {
|
||||
await tablesService.deleteRow(row.id);
|
||||
}
|
||||
selectedRows.value = [];
|
||||
await fetchTable();
|
||||
}
|
||||
|
||||
// Для модалки добавления столбца
|
||||
const showAddColModal = ref(false);
|
||||
@@ -244,6 +291,7 @@ async function handleAddColumn() {
|
||||
closeAddColModal();
|
||||
await fetchTable();
|
||||
await updateRelationFilterDefs(); // Явно обновляем фильтры
|
||||
window.dispatchEvent(new Event('placeholders-updated'));
|
||||
}
|
||||
|
||||
async function deleteColumn(col) {
|
||||
@@ -252,6 +300,7 @@ async function deleteColumn(col) {
|
||||
await tablesService.deleteColumn(col.id);
|
||||
await fetchTable();
|
||||
await updateRelationFilterDefs(); // Явно обновляем фильтры
|
||||
window.dispatchEvent(new Event('placeholders-updated'));
|
||||
}
|
||||
|
||||
// Удаляю все переменные, функции и UI, связанные с tags, tagOptions, selectedTags, loadTags, updateFilterOptions с tags, и т.д.
|
||||
@@ -411,6 +460,9 @@ function addColumn() {
|
||||
function addRow() {
|
||||
tablesService.addRow(props.tableId).then(fetchTable);
|
||||
}
|
||||
function addRowAfter(row) {
|
||||
tablesService.addRow(props.tableId, row.id).then(fetchTable);
|
||||
}
|
||||
function openColMenu(col, event) {
|
||||
openedColMenuId.value = col.id;
|
||||
openedRowMenuId.value = null;
|
||||
@@ -438,6 +490,33 @@ async function deleteRow(row) {
|
||||
await fetchTable();
|
||||
}
|
||||
|
||||
async function saveRowsOrder() {
|
||||
// Сохраняем новый порядок строк на сервере
|
||||
const orderArr = rows.value.map((row, idx) => ({ rowId: row.id, order: idx + 1 }));
|
||||
await tablesService.updateRowsOrder(props.tableId, orderArr);
|
||||
}
|
||||
|
||||
function moveRowUp(row) {
|
||||
const idx = rows.value.findIndex(r => r.id === row.id);
|
||||
if (idx > 0) {
|
||||
const temp = rows.value[idx - 1];
|
||||
rows.value[idx - 1] = rows.value[idx];
|
||||
rows.value[idx] = temp;
|
||||
saveRowsOrder();
|
||||
fetchTable();
|
||||
}
|
||||
}
|
||||
function moveRowDown(row) {
|
||||
const idx = rows.value.findIndex(r => r.id === row.id);
|
||||
if (idx < rows.value.length - 1) {
|
||||
const temp = rows.value[idx + 1];
|
||||
rows.value[idx + 1] = rows.value[idx];
|
||||
rows.value[idx] = temp;
|
||||
saveRowsOrder();
|
||||
fetchTable();
|
||||
}
|
||||
}
|
||||
|
||||
async function rebuildIndex() {
|
||||
rebuilding.value = true;
|
||||
rebuildStatus.value = null;
|
||||
@@ -458,12 +537,12 @@ async function rebuildIndex() {
|
||||
|
||||
<style scoped>
|
||||
.user-table-header {
|
||||
max-width: 1100px;
|
||||
margin: 32px auto 0 auto;
|
||||
padding: 32px 24px 18px 24px;
|
||||
background: #fff;
|
||||
border-radius: 18px;
|
||||
box-shadow: 0 4px 24px rgba(0,0,0,0.08);
|
||||
/* max-width: 1100px; */
|
||||
margin: 0 auto 0 auto;
|
||||
/* padding: 32px 24px 18px 24px; */
|
||||
/* background: #fff; */
|
||||
/* border-radius: 18px; */
|
||||
/* box-shadow: 0 4px 24px rgba(0,0,0,0.08); */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -515,41 +594,24 @@ async function rebuildIndex() {
|
||||
}
|
||||
|
||||
.notion-table-wrapper {
|
||||
max-width: 1100px;
|
||||
margin: 24px auto 0 auto;
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
|
||||
padding: 12px 6px 18px 6px;
|
||||
/* max-width: 1100px; */
|
||||
margin: 0 auto 0 auto;
|
||||
/* background: #fff; */
|
||||
/* border-radius: 6px; */
|
||||
/* box-shadow: 0 1px 4px rgba(0,0,0,0.04); */
|
||||
/* padding: 12px 6px 18px 6px; */
|
||||
}
|
||||
|
||||
.notion-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.98rem;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.notion-table th, .notion-table td {
|
||||
border: 1px solid #e5e7eb;
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
background: #fff;
|
||||
font-size: 0.98rem;
|
||||
.el-table__cell, .el-table th, .el-table td {
|
||||
height: auto !important;
|
||||
min-height: 36px;
|
||||
white-space: normal !important;
|
||||
word-break: break-word !important;
|
||||
min-width: 80px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.notion-table th {
|
||||
background: #f3f4f6;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid #d1d5db;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 7px;
|
||||
}
|
||||
|
||||
.notion-table tr:hover td {
|
||||
background: #f5f7fa;
|
||||
.el-table-row-custom {
|
||||
/* Можно добавить стили для высоты строк, если нужно */
|
||||
}
|
||||
|
||||
.notion-input {
|
||||
@@ -602,7 +664,7 @@ async function rebuildIndex() {
|
||||
|
||||
.context-menu {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
z-index: 2000;
|
||||
min-width: 120px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
@@ -691,6 +753,25 @@ async function rebuildIndex() {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.add-col-header .add-col-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: background 0.18s;
|
||||
}
|
||||
.add-col-header .add-col-btn:hover svg circle {
|
||||
fill: #e5e7eb;
|
||||
stroke: #4f8cff;
|
||||
}
|
||||
.add-col-header .add-col-btn:active svg circle {
|
||||
fill: #dbeafe;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.notion-table-wrapper, .table-filters-el {
|
||||
padding: 4px 1vw;
|
||||
|
||||
@@ -63,5 +63,10 @@ export default {
|
||||
async rebuildIndex(tableId) {
|
||||
const res = await api.post(`/tables/${tableId}/rebuild-index`);
|
||||
return res.data;
|
||||
},
|
||||
async updateRowsOrder(tableId, orderArr) {
|
||||
// orderArr: [{rowId, order}, ...]
|
||||
const res = await api.patch(`/tables/${tableId}/rows/order`, { order: orderArr });
|
||||
return res.data;
|
||||
}
|
||||
};
|
||||
@@ -2,13 +2,98 @@
|
||||
<BaseLayout>
|
||||
<div class="content-page-block">
|
||||
<h2>Контент</h2>
|
||||
<p>Здесь будет размещён контент.</p>
|
||||
<form class="content-form" @submit.prevent>
|
||||
<div class="form-group">
|
||||
<label for="title">Заголовок страницы *</label>
|
||||
<input v-model="form.title" id="title" type="text" required />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="summary">Краткое описание *</label>
|
||||
<textarea v-model="form.summary" id="summary" required rows="2" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="content">Основной контент *</label>
|
||||
<textarea v-model="form.content" id="content" required rows="6" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="image">Изображение/обложка</label>
|
||||
<input v-model="form.image" id="image" type="text" placeholder="URL или имя файла" />
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="tags">Теги</label>
|
||||
<div class="tags-input">
|
||||
<input
|
||||
v-model="tagInput"
|
||||
@keydown.enter.prevent="addTag"
|
||||
@blur="addTag"
|
||||
placeholder="Введите тег и нажмите Enter"
|
||||
/>
|
||||
<div class="tags-list">
|
||||
<span v-for="(tag, idx) in form.tags" :key="tag" class="tag">
|
||||
{{ tag }}
|
||||
<button type="button" @click="removeTag(idx)">×</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Категория</label>
|
||||
<select v-model="form.category" id="category">
|
||||
<option value="">Не выбрано</option>
|
||||
<option value="О компании">О компании</option>
|
||||
<option value="Продукты">Продукты</option>
|
||||
<option value="Блог">Блог</option>
|
||||
<option value="FAQ">FAQ</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="addToChat">Добавить в чат</label>
|
||||
<select v-model="form.addToChat" id="addToChat">
|
||||
<option value="yes">Да</option>
|
||||
<option value="no">Нет</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="rag">Интегрировать с RAG</label>
|
||||
<select v-model="form.rag" id="rag">
|
||||
<option value="yes">Да</option>
|
||||
<option value="no">Нет</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="submit-btn" type="submit">Сохранить</button>
|
||||
</form>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import BaseLayout from '../components/BaseLayout.vue';
|
||||
|
||||
const form = ref({
|
||||
title: '',
|
||||
summary: '',
|
||||
content: '',
|
||||
image: '',
|
||||
tags: [],
|
||||
category: '',
|
||||
addToChat: 'yes',
|
||||
rag: 'yes',
|
||||
});
|
||||
|
||||
const tagInput = ref('');
|
||||
|
||||
function addTag() {
|
||||
const tag = tagInput.value.trim();
|
||||
if (tag && !form.value.tags.includes(tag)) {
|
||||
form.value.tags.push(tag);
|
||||
}
|
||||
tagInput.value = '';
|
||||
}
|
||||
|
||||
function removeTag(idx) {
|
||||
form.value.tags.splice(idx, 1);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -22,4 +107,62 @@ import BaseLayout from '../components/BaseLayout.vue';
|
||||
position: relative;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.content-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 18px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
input[type="text"], textarea, select {
|
||||
border: 1px solid #d0d0d0;
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
font-size: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
.tags-input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.tags-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
.tag {
|
||||
background: #f0f0f0;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.tag button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: #888;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.submit-btn {
|
||||
background: #2d72d9;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 10px 0;
|
||||
font-size: 1.1em;
|
||||
cursor: pointer;
|
||||
margin-top: 12px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.submit-btn:hover {
|
||||
background: #1a4e96;
|
||||
}
|
||||
</style>
|
||||
@@ -101,7 +101,7 @@
|
||||
<script setup>
|
||||
import BaseLayout from '@/components/BaseLayout.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ref, onMounted, computed, watch } from 'vue';
|
||||
import { ref, onMounted, computed, watch, onBeforeUnmount } from 'vue';
|
||||
import axios from 'axios';
|
||||
import RuleEditor from '@/components/ai-assistant/RuleEditor.vue';
|
||||
import SystemMonitoring from '@/components/ai-assistant/SystemMonitoring.vue';
|
||||
@@ -184,6 +184,12 @@ onMounted(() => {
|
||||
loadLLMModels();
|
||||
loadEmbeddingModels();
|
||||
loadPlaceholders();
|
||||
// Подписка на глобальное событие обновления плейсхолдеров
|
||||
window.addEventListener('placeholders-updated', loadPlaceholders);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('placeholders-updated', loadPlaceholders);
|
||||
});
|
||||
async function saveSettings() {
|
||||
await axios.put('/settings/ai-assistant', settings.value);
|
||||
|
||||
199
md/interactive-content-sharing.md
Normal file
199
md/interactive-content-sharing.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Интерактивный обмен веб-страницами с ИИ-ассистентом в корпоративном чате
|
||||
|
||||
## Описание задачи
|
||||
|
||||
Реализовать функционал, позволяющий пользователям создавать веб-страницы с данными о компании, продуктах или статьями (блог), делиться ими в корпоративном чате в виде интерактивных сообщений с кнопками, а также обеспечивать автоматическое добавление этих страниц в базу знаний (RAG) для поиска и генерации ответов ИИ-ассистентом. Страницы должны быть доступны для поиска в интернете по ключевым словам.
|
||||
|
||||
---
|
||||
|
||||
## Основные требования
|
||||
|
||||
1. **Создание веб-страниц**
|
||||
- Форма на странице `/content` для ввода информации (название, описание, текст, теги, изображения и т.д.).
|
||||
- Возможность предпросмотра и редактирования страницы до публикации.
|
||||
- Кнопка "Поделиться", после нажатия которой страница сохраняется и становится доступной для дальнейших действий.
|
||||
|
||||
2. **Публикация и доступность**
|
||||
- После публикации страница доступна по уникальному URL (например, `/content/page/123` или `/content/page/название`).
|
||||
- Страница оптимизирована для SEO (мета-теги, ЧПУ-адреса, sitemap.xml).
|
||||
- При необходимости — настройка индексации для поисковых систем (robots.txt).
|
||||
- **Оптимизация для ИИ-поиска**: страницы должны быть структурированы и открыты для индексации поисковыми ИИ (Perplexity, GPT, Gemini и др.). Рекомендуется:
|
||||
- Использовать семантическую разметку (schema.org, JSON-LD)
|
||||
- Добавлять структурированные данные (FAQ, Article, Product)
|
||||
- Открывать страницы для crawler-ботов ИИ (не блокировать их в robots.txt)
|
||||
- Обеспечивать чистый, легко читаемый HTML-код
|
||||
- Добавлять релевантные ключевые слова и теги
|
||||
|
||||
3. **Интеграция с корпоративным чатом**
|
||||
- После публикации пользователь может отправить страницу в чат с помощью кнопки "Поделиться в чат".
|
||||
- В чате появляется интерактивное сообщение (карточка) с краткой информацией о странице и кнопкой (например, "Показать содержимое").
|
||||
- При нажатии на кнопку ассистент отправляет содержимое страницы (или его резюме) в чат.
|
||||
- Возможность задать вопрос по содержимому страницы через чат (ассистент ищет ответ только в этой странице).
|
||||
- При публикации страницы в чатах пользователей (веб, Telegram, email) вместе с ответами на сообщения должно автоматически добавляться интерактивное сообщение с заголовком страницы и кнопкой "Подробнее". При нажатии на кнопку ИИ-ассистент отправляет содержимое веб-страницы в чат.
|
||||
|
||||
4. **Интеграция с RAG (Retrieval-Augmented Generation)**
|
||||
- После публикации страница автоматически разбивается на смысловые блоки (например, по абзацам).
|
||||
- Каждый блок векторизуется (создается embedding) и сохраняется в векторное хранилище (Qdrant, Pinecone и т.д.) с метаданными (id страницы, теги, автор, дата).
|
||||
- Ассистент использует эти данные для поиска и генерации ответов на вопросы пользователей.
|
||||
- В форме создания страницы добавить выпадающий список "Интегрировать с RAG" (Да/Нет). При выборе "Да" ИИ-ассистент анализирует содержимое страницы, автоматически предлагает список возможных вопросов и ответов (Q&A) по теме страницы. Пользователь может отредактировать эти вопросы и ответы, отметить чекбоксами нужные и нажать "Добавить" — выбранные Q&A будут сохранены в базу знаний для последующего поиска и генерации ответов.
|
||||
|
||||
5. **Поиск в интернете**
|
||||
- Страницы доступны для индексации поисковыми системами.
|
||||
- Поиск по ключевым словам приводит к отображению соответствующих страниц.
|
||||
|
||||
---
|
||||
|
||||
## Пользовательские сценарии
|
||||
|
||||
1. **Создание и публикация страницы**
|
||||
- Пользователь заполняет форму на `/content`, нажимает "Поделиться".
|
||||
- Страница сохраняется, появляется уникальный URL.
|
||||
|
||||
2. **Обмен в чате**
|
||||
- Пользователь нажимает "Поделиться в чат".
|
||||
- В чате появляется карточка с кнопкой "Показать содержимое".
|
||||
- Другой пользователь нажимает кнопку — ассистент отправляет содержимое страницы в чат.
|
||||
|
||||
3. **Поиск и ответы ассистента**
|
||||
- Пользователь задаёт вопрос по теме страницы.
|
||||
- Ассистент ищет ответ в RAG по содержимому страницы и формирует релевантный ответ.
|
||||
|
||||
4. **Поиск через интернет**
|
||||
- Внешний пользователь находит страницу через поисковик по ключевым словам.
|
||||
|
||||
---
|
||||
|
||||
## Техническая реализация
|
||||
|
||||
### Frontend
|
||||
- Vue.js, локальные scoped-стили
|
||||
- Форма создания/редактирования страницы
|
||||
- Компонент карточки для чата с интерактивной кнопкой
|
||||
|
||||
### Backend
|
||||
- Node.js/NestJS/Fastify/Express
|
||||
- REST API для создания, публикации, получения страниц
|
||||
- Векторизация и интеграция с векторным хранилищем (Qdrant, Pinecone)
|
||||
- Генерация и отправка сообщений в чат
|
||||
|
||||
### Векторное хранилище (RAG)
|
||||
- Сохранение embedding-блоков с метаданными
|
||||
- Поиск по embedding для генерации ответов ассистентом
|
||||
|
||||
### SEO и индексация
|
||||
- Генерация мета-тегов, sitemap.xml
|
||||
- ЧПУ-адреса страниц
|
||||
- Настройка robots.txt
|
||||
|
||||
---
|
||||
|
||||
## Диаграмма взаимодействия
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Frontend
|
||||
participant Backend
|
||||
participant VectorDB
|
||||
participant Chat
|
||||
participant SearchEngine
|
||||
|
||||
User->>Frontend: Заполняет форму на /content
|
||||
Frontend->>Backend: POST /api/content (данные)
|
||||
Backend->>Backend: Создает страницу, сохраняет в БД
|
||||
Backend->>VectorDB: Векторизация, добавление в RAG
|
||||
Backend->>Frontend: Возвращает ссылку на страницу
|
||||
Frontend->>Chat: Отправляет карточку в чат
|
||||
Chat->>User: Показывает карточку + ответ ассистента
|
||||
SearchEngine->>Backend: Индексирует страницу (если разрешено)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Примечания
|
||||
- Все компоненты должны быть реализованы с учетом безопасности и приватности данных.
|
||||
- Необходимо предусмотреть возможность удаления и редактирования страниц.
|
||||
- Для интеграции с ИИ-ассистентом использовать современные RAG-фреймворки (LlamaIndex, LangChain и т.д.).
|
||||
- Для векторизации использовать актуальные модели (OpenAI, Sentence Transformers и др.).
|
||||
|
||||
---
|
||||
|
||||
## Автоматический выбор и добавление интерактивных сообщений ИИ-ассистентом
|
||||
|
||||
ИИ-ассистент может самостоятельно анализировать контекст диалога и принимать решение о добавлении интерактивных сообщений (карточек) в чат, чтобы повысить релевантность и удобство взаимодействия.
|
||||
|
||||
### Принцип работы
|
||||
|
||||
1. **Анализ контекста диалога**
|
||||
- Ассистент анализирует сообщения пользователя, определяет тему, намерение и ключевые слова.
|
||||
- Выполняет поиск по базе знаний (RAG) для нахождения релевантных страниц или блоков.
|
||||
|
||||
2. **Принятие решения о вставке интерактивного сообщения**
|
||||
- Если найден релевантный контент, ассистент решает, нужно ли добавить карточку с кнопкой (например, "Подробнее").
|
||||
- Решение зависит от:
|
||||
- Тематики и повторяемости вопросов
|
||||
- Новизны или важности контента
|
||||
- Явных триггеров в сообщениях пользователя (например, "покажи инструкцию", "расскажи подробнее" и т.д.)
|
||||
|
||||
3. **Формирование интерактивного сообщения**
|
||||
- Ассистент формирует карточку с заголовком, кратким описанием и кнопкой (например, "Подробнее").
|
||||
- При нажатии на кнопку пользователем ассистент отправляет содержимое страницы или выбранного блока в чат.
|
||||
|
||||
### Пример сценария
|
||||
|
||||
- Пользователь: "Как подключить наш продукт X?"
|
||||
- Ассистент:
|
||||
1. Находит в RAG инструкцию по подключению продукта X.
|
||||
2. Отвечает кратко и автоматически добавляет интерактивную карточку с кнопкой "Показать инструкцию".
|
||||
3. При нажатии на кнопку отправляет подробную инструкцию в чат.
|
||||
|
||||
### Преимущества
|
||||
- Пользователь получает релевантную информацию в нужный момент, не перегружая чат лишними карточками.
|
||||
- Ассистент становится более "умным" и проактивным.
|
||||
- Повышается вовлечённость и удовлетворённость пользователей.
|
||||
|
||||
---
|
||||
|
||||
## Публикация страниц в социальных сетях и блогах через API
|
||||
|
||||
Система может поддерживать публикацию созданных страниц на страницах компании в социальных сетях и блогах (Medium, LinkedIn, Instagram, Paragraph, Telegraph, Telegram и др.) с помощью соответствующих API.
|
||||
|
||||
### Возможности
|
||||
- Публикация статей и постов на платформах: Medium, LinkedIn (страницы компаний), Instagram (бизнес-аккаунты), Paragraph, Telegraph, Telegram (боты/каналы) и др.
|
||||
- Выбор платформ для публикации пользователем (чекбоксы или список).
|
||||
- Формирование контента с учётом особенностей каждой платформы (текст, изображения, разметка).
|
||||
|
||||
### Принцип работы
|
||||
1. Пользователь выбирает опцию "Опубликовать в соцсетях" после создания страницы.
|
||||
2. Открывается окно выбора платформ и авторизации (OAuth2, если требуется).
|
||||
3. Для каждой платформы формируется подходящий формат контента.
|
||||
4. Система отправляет запросы к API выбранных платформ для публикации.
|
||||
5. Пользователь получает уведомление об успешной публикации или ошибке.
|
||||
|
||||
### Ограничения и нюансы
|
||||
- Для некоторых платформ требуется бизнес-аккаунт и прохождение модерации приложения (LinkedIn, Instagram).
|
||||
- Формат и объём контента может отличаться (например, Instagram — только изображение+текст).
|
||||
- Возможны лимиты на частоту публикаций и размер постов.
|
||||
- Некоторые платформы могут задерживать публикацию для модерации.
|
||||
|
||||
### Пример архитектуры
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Frontend
|
||||
participant Backend
|
||||
participant SocialAPI
|
||||
|
||||
User->>Frontend: Нажимает “Опубликовать в соцсетях”
|
||||
Frontend->>Backend: POST /api/publish-to-social (данные, токены)
|
||||
Backend->>SocialAPI: Публикует на выбранных платформах
|
||||
SocialAPI-->>Backend: Ответ (успех/ошибка)
|
||||
Backend-->>Frontend: Сообщение о результате
|
||||
Frontend-->>User: Уведомление
|
||||
```
|
||||
|
||||
### Рекомендации
|
||||
- Для каждой платформы реализовать отдельный модуль интеграции или использовать сторонние сервисы (Zapier, Make).
|
||||
- Предусмотреть очередь публикаций и логи для отслеживания статуса.
|
||||
- Обеспечить безопасное хранение и обновление токенов авторизации.
|
||||
Reference in New Issue
Block a user