diff --git a/app/database/migrations/2026_07_02_100001_create_knowledge_chunks_table.php b/app/database/migrations/2026_07_02_100001_create_knowledge_chunks_table.php new file mode 100644 index 00000000..74c51b60 --- /dev/null +++ b/app/database/migrations/2026_07_02_100001_create_knowledge_chunks_table.php @@ -0,0 +1,42 @@ +insert([ + 'source_path' => 'help/project.md', + 'title' => 'Что такое проект', + 'tour' => 'create-project', + 'topics' => 'заявка на лиды, создать проект, источник', + 'chunk_index' => 0, + 'content' => 'Проект — это заявка на поток лидов с выбранного источника.', + 'created_at' => now(), + 'updated_at' => now(), + ]); + + $found = DB::select( + "SELECT id, title FROM knowledge_chunks + WHERE search_tsv @@ websearch_to_tsquery('russian', ?)", + ['что такое проект'] + ); + + expect($found)->toHaveCount(1) + ->and($found[0]->title)->toBe('Что такое проект'); +}); diff --git a/db/CHANGELOG_schema.md b/db/CHANGELOG_schema.md index 1f60e2f0..772afd3e 100644 --- a/db/CHANGELOG_schema.md +++ b/db/CHANGELOG_schema.md @@ -2,7 +2,18 @@ **Назначение:** консолидированный журнал изменений `schema.sql`. Содержит тридцать записей в обратном хронологическом порядке (v8.33 → v8.32 → v8.31 → v8.30 → v8.29 → v8.28 → v8.27 → v8.26 → v8.25 → v8.24 → v8.23 → v8.22 → v8.21 → v8.20 → v8.19 → v8.18 → v8.17 → v8.16 → v8.15 → v8.14 → v8.13 → v8.12 → v8.11 → v8.10 → v8.9 → v8.8 → v8.7 → v8.6 → v8.5 → v8.4 → v8.3 → v8.2), как принято в keep-a-changelog. -**Файл схемы:** `schema.sql` (текущая версия — v8.63, консолидированная — разворачивает БД с нуля). +**Файл схемы:** `schema.sql` (текущая версия — v8.64, консолидированная — разворачивает БД с нуля). + +## v8.64 (2026-07-10) — ИИ-бот техподдержки Jivo: knowledge_chunks + bot_dialogs + ++`knowledge_chunks` (база знаний ИИ-бота, глобальная, без RLS — публичные статьи инструкции resources/help/*.md; +generated column `search_tsv` russian + GIN-индекс `idx_knowledge_chunks_search`; заполняется командой +`help:rebuild-knowledge`; спека `docs/superpowers/specs/2026-07-02-jivo-ai-support-bot-design.md §3`). + ++`bot_dialogs` (журнал диалогов бота, глобальная, без RLS — ПДн клиентов не пишем; `direction in/out`, +`matched_chunks JSONB`, `latency_ms`, `escalated`; индекс `idx_bot_dialogs_chat (jivo_chat_id, created_at)`; +спека §5). Счётчики: таблиц 82→84 (regular 72→74) / индексов 130→132. Миграции 2026_07_02_100001/100002. +Ветка worktree-jivo-bot-core (перебазирована на канон main 10.07.2026), НЕ на проде. ## ⏸ ХВОСТ (2026-07-08) — Автоподбор: RLS-политика межтенантного распорядителя шага 2 diff --git a/db/schema.sql b/db/schema.sql index 4e3ee827..ae1630f2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,6 +1,7 @@ -- ============================================================================= -- schema.sql — единая схема БД для SaaS-аналога crm.bp-gr.ru («Лидерра») --- Версия: v8.62 (06.07.2026 — Автоподбор canon-sync ДЕНЕЖНОЙ таблицы: в CHECK balance_transactions_type_check добавлен тип 'autopodbor_charge' (списание за прогон автоподбора, AutopodborChargeService; миграция 2026_06_28_110100 — ранее НЕ была свёрнута в тело DDL, нашёл rls-reviewer при подготовке боевого выката). Структурно меняется только значение CHECK — счётчики таблиц/индексов/RLS/функций/триггеров/партиций БЕЗ изменений. Воркстри-фича avtopodbor, НЕ на проде. Прежняя версия ниже.) +-- Версия: v8.64 (10.07.2026 — ИИ-бот техподдержки Jivo: +2 таблицы (knowledge_chunks — база знаний FTS russian GIN, глобальная без RLS; bot_dialogs — журнал диалогов, глобальная без RLS) + 2 индекса (idx_knowledge_chunks_search GIN, idx_bot_dialogs_chat). Спека docs/superpowers/specs/2026-07-02-jivo-ai-support-bot-design.md. Миграции 2026_07_02_100001/100002. Счётчики: таблиц 82→84 (regular 72→74) / индексов 130→132. RLS/функций/триггеров без изменений.) +-- Базовая версия: v8.62 (06.07.2026 — Автоподбор canon-sync ДЕНЕЖНОЙ таблицы: в CHECK balance_transactions_type_check добавлен тип 'autopodbor_charge' (списание за прогон автоподбора, AutopodborChargeService; миграция 2026_06_28_110100 — ранее НЕ была свёрнута в тело DDL, нашёл rls-reviewer при подготовке боевого выката). Структурно меняется только значение CHECK — счётчики таблиц/индексов/RLS/функций/триггеров/партиций БЕЗ изменений. Воркстри-фича avtopodbor, НЕ на проде. Прежняя версия ниже.) -- Базовая версия: v8.63 (07.07.2026 — Автоподбор: +колонка dismissed_actualize_keys JSONB NOT NULL DEFAULT '[]' у autopodbor_competitors — отклонённые клиентом ключи актуализации (кнопка «Отказаться»: находка нового сайта/адреса уходит, её ключи запоминаются у фирмы поля, ProposalClassifier их вычитает → та же находка больше не всплывает; реально другой новый сайт даст новый ключ → покажется). Миграция 2026_07_07_130000 идемпотентна (Schema::hasColumn), migrate:fresh зелёный, ветка автоподбора 185/185. Структурно: autopodbor_competitors +1 колонка. Воркстри-фича avtopodbor, НЕ на проде (колонка добавлена на прод миграцией 07.07). RLS/индексов/функций/триггеров/партиций без изменений.) Прежняя v8.62 (06.07.2026 — canon-sync денежной таблицы: balance_transactions.type CHECK += 'autopodbor_charge', пропущенный инкремент миграции 2026_06_28_110100). Прежняя v8.61 (05.07.2026 — Автоподбор, закрытие отложенного «хвоста» + журнал слияний. Свёрнуты в тело DDL ранее отложенные инкременты: autopodbor_competitors +box VARCHAR(16) DEFAULT 'proposal'+chk (proposal|field|archived, миграции 2026_06_29_120000/2026_07_01_100000) +индекс autopodbor_competitors_tenant_box_idx (tenant_id,box) [это и был «⏸ ХВОСТ» из v8.60] +phones JSONB DEFAULT '[]' (2026_07_03_120000) +elements JSONB (2026_07_04_140000); autopodbor_runs +progress JSONB +result JSONB (2026_07_05_120000/130000). НОВАЯ таблица autopodbor_merge_events (журнал слияний конкурентов + снимок поглощённых карточек absorbed для истории/возврата; per-tenant RLS ENABLE+FORCE tenant_isolation; FK user_id→users SET NULL, survivor_id→autopodbor_competitors SET NULL; индекс (tenant_id,created_at); миграция 2026_07_05_140000). ALTER-миграции автоподбора идемпотентны (elements/runs_progress/runs_result доguard-ены Schema::hasColumn; add_box/add_phones/add_phone_type/add_where_found — hasColumn; create — to_regclass): migrate:fresh зелёный end-to-end, весь блок автоподбора 428/428, squawk 0, rls-reviewer OK. Структурно: autopodbor_competitors +3 колонки +1 индекс +1 CHECK; autopodbor_runs +2 колонки; +1 regular-таблица autopodbor_merge_events (+1 индекс +1 RLS-политика). Воркстри-фича avtopodbor, НЕ на проде. Функций/триггеров/партиций без изменений. Прежняя v8.60, 01.07.2026 — Автоподбор: box += 'archived' у autopodbor_sources CHECK (мягкое удаление конкурента + архив старого сайта при актуализации), миграция 2026_07_01_100000; тот же 'archived' в DB у autopodbor_competitors.box — но его CHECK в этом DDL по-прежнему отложенный хвост, см. ниже. Прежняя v8.59, 30.06.2026 — Автоподбор, богатый провенанс источника + canon-sync инкрементов 29.06: autopodbor_sources +3 колонки where_found JSONB (список «где нашли» [{label,url}], подтверждения = длина) / office VARCHAR(255) (адрес филиала из карточки справочника) / confirmations SMALLINT DEFAULT 1 (сортировка «больше подтверждений — выше»), миграция 2026_06_30_120000 (идемпотентная ADD COLUMN, nullable/default — старые строки не переписываются, RLS построчная не меняется). В ТОТ ЖЕ DDL внесены ранее не синканные инкременты 29.06: phone_type VARCHAR(12)+chk (миграция 2026_06_29_120100), box VARCHAR(16) DEFAULT 'proposal'+chk+индекс autopodbor_sources_competitor_box_idx (миграция 2026_06_29_120000). Структурно для autopodbor_sources: +5 колонок (3 новых + 2 catch-up), +1 индекс, +2 CHECK. ⏸ ХВОСТ: box у autopodbor_competitors (+колонка+chk+индекс autopodbor_competitors_tenant_box_idx, та же миграция 2026_06_29_120000) в DDL ещё НЕ синкан — отдельный canon-sync; точная пересверка счётчиков таблиц/индексов/RLS отложена (прецедент v8.54/v8.55). Воркстри-фича avtopodbor, НЕ на проде. RLS/функций/триггеров/партиций без изменений.) -- Базовая версия: v8.58 (28.06.2026 — Автоподбор конкурентов: +3 regular-таблицы autopodbor_runs/competitors/sources, RLS tenant_isolation в харднинг-форме (ENABLE+FORCE+NULLIF), +6 индексов (вкл. 2 UNIQUE dedup: autopodbor_competitor_dedup, autopodbor_source_dedup), +3 RLS-политики. Счётчики: таблиц 79→82 (regular 69→72) / индексов 124→130 / RLS 44→47. Миграции 2026_06_28_100000/100100/100200. Функций/триггеров/партиций без изменений. GRANTs через blanket ON ALL TABLES в db/02_grants.sql — отдельных строк не добавлено.) -- Базовая версия: v8.57 (26.06.2026 — RLS GUC hardening: ВСЕ 44 политики tenant_isolation приведены к NULLIF(current_setting('app.current_tenant_id', true), '')::bigint — устранён класс отказов на Managed PG/PgBouncer, когда GUC app.current_tenant_id пуст ('' → 22P02) либо не задан (→ 42704). Инцидент 26.06: вход в портал падал на резолве users (60 ошибок, все на users). 5 bootstrap-таблиц (users, auth_log, email_verifications, user_recovery_codes, user_sessions) дополнительно получили разрешающую ветку «NULLIF(...) IS NULL OR ...» — читаются/пишутся ДО tenant-контекста на auth-роутах без 'tenant' middleware; при ЗАДАННОМ tenant изоляция НЕ меняется (rls-reviewer APPROVE). Структурно БД НЕ меняется — переписаны только USING/WITH CHECK (счётчики таблиц/индексов/RLS=44/функций/триггеров без изменений). Миграция 2026_06_26_153000_rls_nullif_guc_hardening (идемпотентна). Применена на боевой кластер; lead_charges FORCE RLS сохранён.) @@ -32,7 +33,7 @@ -- Базовая версия: v8.29 (22.05.2026 — webhook_log: supplier audit columns) -- Базовая версия: v8.28 (22.05.2026 — tenant_operations_log: журнал тенант-уровневых операций вне сделок (проекты, API-ключи, webhook URL), append-only hash-chain, P2 operational journaling closure) -- Базовая версия: v8.27 (21.05.2026 — drop projects.archived_at: feature архива заменена настоящим удалением с защитой по сделкам (ProjectService::delete())) --- Метрики: 82 базовых таблицы (72 regular + 10 partitioned parents: deals + supplier_lead_costs + 6 audit + lead_region_resolution_log + project_routing_snapshots) + 16 партиций / 130 индексов (+6 autopodbor: 4 обычных + 2 UNIQUE dedup) / 47 RLS-политик / 5 функций / 15 триггеров +-- Метрики: 84 базовых таблицы (74 regular + 10 partitioned parents: deals + supplier_lead_costs + 6 audit + lead_region_resolution_log + project_routing_snapshots) + 16 партиций / 132 индекса (+6 autopodbor: 4 обычных + 2 UNIQUE dedup; +idx_knowledge_chunks_search GIN; +idx_bot_dialogs_chat) / 47 RLS-политик / 5 функций / 15 триггеров -- NB (23.06.2026 сверка-чистка перед запуском): RLS-политик исправлено 42→44 — реальное число активных CREATE POLICY в теле == 44 == на боевом liderra.ru (pg_policies). Расхождение было исторический недоучёт в бегущем счётчике шапки (tenant_requisites v8.43 и др. добавлены в тело без правки сводной метрики). Структурно БД не менялась — только корректность числа. -- NB (28.06.2026): индексов 124→130 (+6 autopodbor). Счётчик шапки (124) соответствует заявленному в v8.57; реальное число CREATE INDEX/CREATE UNIQUE INDEX в теле — 127 (128 строк минус 1 закомментированная call_recordings). Дрейф +3 — исторический недоучёт дельта-миграций (аналогично дрейфу RLS 42→44 от 23.06). Цифра 130 в шапке = 124+6 (канон шапки + добавленные autopodbor); пересверка всего тела — отдельный canon-sync. -- NB (28.06.2026): RLS-политик 44→47 (+3 autopodbor tenant_isolation). Счётчик шапки (47) = канон-baseline 44 (v8.57) + 3. Реальное число CREATE POLICY в теле после добавления — 49 (из них 44 tenant_isolation). Дрейф +2 — тот же класс исторического недоучёта бегущего счётчика, что зафиксирован для RLS 42→44 (23.06) и индексов выше; изоляция всех 3 таблиц автоподбора корректна (rls-reviewer APPROVE по сути, блокер — только метрика). Пересверка всего тела (полный пересчёт CREATE POLICY) — отдельный canon-sync. @@ -3784,6 +3785,85 @@ BEGIN END IF; END $$; +-- ============================================================================= +-- knowledge_chunks — база знаний ИИ-бота (v8.58, спека 2026-07-02-jivo-ai-support-bot-design §3) +-- ============================================================================= +-- Глобальная таблица (НЕ tenant-scoped): только публичные статьи клиентской инструкции, +-- данных клиентов здесь нет по определению — RLS не требуется. +-- search_tsv — generated column (russian): title+topics+content → tsvector + GIN-индекс. +-- Заполняется командой help:rebuild-knowledge (ночной schedule 04:30). +-- Миграция 2026_07_02_100001_create_knowledge_chunks_table. +-- ============================================================================= +CREATE TABLE knowledge_chunks ( + id BIGSERIAL PRIMARY KEY, + source_path VARCHAR(255) NOT NULL, + title VARCHAR(255) NOT NULL, + tour VARCHAR(100), + topics TEXT NOT NULL DEFAULT '', + chunk_index INTEGER NOT NULL, + content TEXT NOT NULL, + search_tsv tsvector GENERATED ALWAYS AS ( + to_tsvector('russian', coalesce(title, '') || ' ' || coalesce(topics, '') || ' ' || coalesce(content, '')) + ) STORED, + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), + CONSTRAINT uq_knowledge_chunks_source_chunk UNIQUE (source_path, chunk_index) +); + +CREATE INDEX idx_knowledge_chunks_search ON knowledge_chunks USING GIN (search_tsv); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_app_user') THEN + GRANT SELECT, INSERT, UPDATE, DELETE ON knowledge_chunks TO crm_app_user; + GRANT USAGE, SELECT ON SEQUENCE knowledge_chunks_id_seq TO crm_app_user; + END IF; + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_app_admin') THEN + GRANT SELECT, INSERT, UPDATE, DELETE ON knowledge_chunks TO crm_app_admin; + GRANT USAGE, SELECT ON SEQUENCE knowledge_chunks_id_seq TO crm_app_admin; + END IF; + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_migrator') THEN + GRANT SELECT, INSERT, UPDATE, DELETE ON knowledge_chunks TO crm_migrator; + GRANT USAGE, SELECT ON SEQUENCE knowledge_chunks_id_seq TO crm_migrator; + END IF; +END $$; + +-- ============================================================================= +-- bot_dialogs — журнал диалогов ИИ-бота (v8.58, спека §5) +-- ============================================================================= +-- Глобальная (НЕ tenant-scoped) в v1: диалоги Jivo анонимны до этапа личных ответов; +-- ПДн клиентов не пишем. direction: in = сообщение клиента, out = ответ бота. +-- Миграция 2026_07_02_100002_create_bot_dialogs_table. +-- ============================================================================= +CREATE TABLE bot_dialogs ( + id BIGSERIAL PRIMARY KEY, + jivo_chat_id VARCHAR(64) NOT NULL, + direction VARCHAR(3) NOT NULL CHECK (direction IN ('in', 'out')), + message TEXT NOT NULL, + matched_chunks JSONB, + latency_ms INTEGER, + escalated BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +CREATE INDEX idx_bot_dialogs_chat ON bot_dialogs (jivo_chat_id, created_at); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_app_user') THEN + GRANT SELECT, INSERT ON bot_dialogs TO crm_app_user; + GRANT USAGE, SELECT ON SEQUENCE bot_dialogs_id_seq TO crm_app_user; + END IF; + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_app_admin') THEN + GRANT SELECT, INSERT ON bot_dialogs TO crm_app_admin; + GRANT USAGE, SELECT ON SEQUENCE bot_dialogs_id_seq TO crm_app_admin; + END IF; + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'crm_migrator') THEN + GRANT SELECT, INSERT ON bot_dialogs TO crm_migrator; + GRANT USAGE, SELECT ON SEQUENCE bot_dialogs_id_seq TO crm_migrator; + END IF; +END $$; + -- ============================================================================= -- КОНЕЦ schema.sql -- =============================================================================