v1.7.0: refactor max_bot to flat structure, add VCF+UserModel+NLP history context, portal pages and proxy fixes

- Refactored max_bot from nested packages to flat module structure
- Q2: Extended BotUser model (patronymic, email, org, address, vcf_raw, contact_hash, phone_verified, email_verified, last_interaction, total_conversations, total_tickets)
- Q2: VCF parser (FN, N, TEL, EMAIL, ORG, ADR), upsert on re-contact, NLP history context (_get_user_history_context -> YandexGPT)
- Q1: Broadcast preview modal with 10s confirmation timer
- Q3: CSS var(--white)->var(--bg-card), var(--text)->var(--text-primary)
- Q4: bot_settings showNotification(), editable max_bot_id
- Q5: Webhook secret passthrough via X-Max-Bot-Api-Secret
- Masking sensitive keys, dialog_cleared handler, migrate via _add_column_if_not_exists()
- Rate limit (asyncio.sleep 0.5 per 10), dead code removed, conv.intent context in contact.py
- Portal pages: bot_consent, bot_kb (edit), bot_settings, bot_test, bot_tickets, portal_settings
- Tests: 21/21 passing, added test_yandex_gpt.py, test_email_sender.py
- Deploy: deploy_full.sh, schema.sql, seed_knowledge_base.sql
This commit is contained in:
2026-05-29 02:30:30 +03:00
parent 493e0b37a1
commit 72b6879f4b
234 changed files with 26768 additions and 6240 deletions
+98
View File
@@ -0,0 +1,98 @@
-- Migration for v1.5.0: Yandex Disk integration + SLA Tickets + MAX notifications
-- File sync queue
CREATE TABLE IF NOT EXISTS file_sync_queue (
id SERIAL PRIMARY KEY,
local_path VARCHAR(500) NOT NULL,
remote_path VARCHAR(500) DEFAULT '',
entity_type VARCHAR(50) NOT NULL,
entity_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','uploading','synced','error')),
error TEXT DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_file_sync_status ON file_sync_queue(status);
CREATE INDEX IF NOT EXISTS idx_file_sync_entity ON file_sync_queue(entity_type, entity_id);
-- Bot tickets
CREATE TABLE IF NOT EXISTS bot_tickets (
id SERIAL PRIMARY KEY,
ticket_number VARCHAR(20) UNIQUE NOT NULL,
user_id INTEGER REFERENCES bot_users(id) ON DELETE SET NULL,
object_id INTEGER REFERENCES objects(id) ON DELETE SET NULL,
customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL,
title VARCHAR(500) NOT NULL,
description TEXT DEFAULT '',
priority VARCHAR(20) DEFAULT 'normal' CHECK (priority IN ('low','normal','high','urgent')),
status VARCHAR(20) NOT NULL DEFAULT 'open' CHECK (status IN ('open','in_progress','resolved','closed')),
created_by VARCHAR(20) DEFAULT 'bot',
assigned_to INTEGER REFERENCES users(id) ON DELETE SET NULL,
sla_contract_id INTEGER REFERENCES sla_contracts(id) ON DELETE SET NULL,
has_attachment BOOLEAN DEFAULT FALSE,
attachment_type VARCHAR(50) DEFAULT '',
attachment_path VARCHAR(500) DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMP NULL,
closed_at TIMESTAMP NULL
);
CREATE INDEX IF NOT EXISTS idx_bot_ticket_status ON bot_tickets(status);
CREATE INDEX IF NOT EXISTS idx_bot_ticket_user ON bot_tickets(user_id);
CREATE INDEX IF NOT EXISTS idx_bot_ticket_object ON bot_tickets(object_id);
CREATE INDEX IF NOT EXISTS idx_bot_ticket_number ON bot_tickets(ticket_number);
-- Bot ticket messages
CREATE TABLE IF NOT EXISTS bot_ticket_messages (
id SERIAL PRIMARY KEY,
ticket_id INTEGER REFERENCES bot_tickets(id) ON DELETE CASCADE,
direction VARCHAR(3) NOT NULL CHECK (direction IN ('in','out')),
text TEXT DEFAULT '',
has_attachment BOOLEAN DEFAULT FALSE,
attachment_path VARCHAR(500) DEFAULT '',
sender VARCHAR(20) DEFAULT 'client',
timestamp TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_bot_ticket_msg_ticket ON bot_ticket_messages(ticket_id);
-- Bot ticket status log
CREATE TABLE IF NOT EXISTS bot_ticket_statuses (
id SERIAL PRIMARY KEY,
ticket_id INTEGER REFERENCES bot_tickets(id) ON DELETE CASCADE,
old_status VARCHAR(20) DEFAULT '',
new_status VARCHAR(20) NOT NULL,
changed_by VARCHAR(20) DEFAULT 'system',
timestamp TIMESTAMP NOT NULL DEFAULT NOW(),
comment TEXT DEFAULT ''
);
CREATE INDEX IF NOT EXISTS idx_bot_ticket_status_log ON bot_ticket_statuses(ticket_id);
-- Add max_user_id to users table
ALTER TABLE users ADD COLUMN IF NOT EXISTS max_user_id BIGINT NULL;
-- Add Yandex Disk settings
INSERT INTO bot_settings (key, value, description) VALUES
('yandex_disk_token', 'y0__wgBEMHM9zsYiuxBIMi6sswXML_twIQIyCDWBrrNPjQcoQZR6J2x57oBLcg', 'OAuth токен Яндекс Диска'),
('yandex_disk_root', '/AegisOne_Service', 'Корневая папка на Яндекс Диске'),
('yandex_disk_client_id', '00fd594faf604161b773a53737bb5462', 'Client ID приложения Яндекс'),
('yandex_disk_client_secret', '32a46b6dfd3c499e8cced25fed64b426', 'Client Secret приложения Яндекс'),
('storage_backend', 'yandex', 'Режим хранения: local/yandex/both'),
('max_file_size_mb', '100', 'Максимальный размер файла (MB)'),
('public_link_expiry_days', '30', 'Срок публичных ссылок по умолчанию (дни)'),
('auto_archive_enabled', 'true', 'Авто-архивация закрытых обращений'),
('auto_archive_after_days', '30', 'Через сколько дней архивировать'),
('max_group_chat_id', '', 'ID группового чата MAX для уведомлений')
ON CONFLICT (key) DO NOTHING;
-- Add SLA Ticket feature
INSERT INTO bot_features (feature_key, name, description, enabled, sort_order) VALUES
('sla_ticket', 'Заявка по SLA', 'Подать заявку о неисправности для клиентов с активным SLA', true, 0)
ON CONFLICT (feature_key) DO NOTHING;
-- Add ticket-related response templates
INSERT INTO bot_response_templates (template_key, template_text, description, variables) VALUES
('sla_ticket_check', 'Проверяю ваш SLA контракт...', 'Проверка SLA перед созданием заявки', '[]'),
('sla_ticket_found', 'SLA найден: {object_name}, уровень: {service_level}. Опишите неисправность. Вы можете прикрепить фото или документ.', 'SLA найден, запрос описания', '["object_name","service_level"]'),
('sla_ticket_not_found', 'SLA контракт не найден или не активен. Свяжитесь с нами:\n\n📞 {phone_1}\n📞 {phone_2}\n✉️ {support_email}', 'SLA не найден', '["phone_1","phone_2","support_email"]'),
('sla_ticket_created', 'Заявка #{ticket_number} создана.\n\nПриоритет: {priority}\nОжидаемое время реакции: {response_time}\n\nИнженер будет назначен в ближайшее время.', 'Подтверждение создания заявки', '["ticket_number","priority","response_time"]'),
('ticket_status_update', 'Ваша заявка #{ticket_number} — {status}.\nИнженер: {engineer}', 'Уведомление о смене статуса заявки', '["ticket_number","status","engineer"]'),
('ticket_resolved', 'Заявка #{ticket_number} выполнена. Оцените качество:\n\n⭐ 1 — Плохо\n⭐⭐ 2\n⭐⭐⭐ 3\n⭐⭐⭐⭐ 4\n⭐⭐⭐⭐⭐ 5 — Отлично', 'Запрос оценки после выполнения', '["ticket_number"]')
ON CONFLICT (template_key) DO NOTHING;
+121
View File
@@ -0,0 +1,121 @@
-- Migration v1.5.5: New tables + seed data
-- Run: psql -U aegisone -d aegisone -f v1.5.5_tables_and_seed.sql
BEGIN;
-- ===================== New tables =====================
CREATE TABLE IF NOT EXISTS bot_notifications (
id SERIAL PRIMARY KEY,
type VARCHAR(50) NOT NULL DEFAULT 'info',
recipient_type VARCHAR(20) NOT NULL DEFAULT 'operator',
recipient_max_user_id BIGINT NULL,
title VARCHAR(255) DEFAULT '',
body TEXT DEFAULT '',
related_type VARCHAR(50) DEFAULT '',
related_id INTEGER NULL,
is_read BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
CHECK (recipient_type IN ('operator','level2','client'))
);
CREATE TABLE IF NOT EXISTS bot_notification_subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES bot_users(id) ON DELETE CASCADE,
notify_on_status_change BOOLEAN NOT NULL DEFAULT FALSE,
notify_on_reply BOOLEAN NOT NULL DEFAULT FALSE,
notify_on_broadcast BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE(user_id)
);
CREATE TABLE IF NOT EXISTS bot_risk_questions (
id SERIAL PRIMARY KEY,
question_key VARCHAR(100) UNIQUE NOT NULL,
question VARCHAR(500) NOT NULL,
weight INTEGER NOT NULL DEFAULT 10,
sort_order INTEGER DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT TRUE
);
CREATE TABLE IF NOT EXISTS bot_tickets (
id SERIAL PRIMARY KEY,
ticket_number VARCHAR(20) UNIQUE NOT NULL,
user_id INTEGER REFERENCES bot_users(id) ON DELETE SET NULL,
object_id INTEGER NULL,
customer_id INTEGER NULL,
title VARCHAR(255) NOT NULL,
description TEXT DEFAULT '',
priority VARCHAR(20) DEFAULT 'normal',
status VARCHAR(20) NOT NULL DEFAULT 'open',
created_by VARCHAR(20) DEFAULT 'bot',
sla_contract_id INTEGER NULL,
has_attachment BOOLEAN DEFAULT FALSE,
attachment_type VARCHAR(50) DEFAULT '',
attachment_path VARCHAR(500) DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
CHECK (priority IN ('low','normal','high','urgent')),
CHECK (status IN ('open','in_progress','resolved','closed'))
);
CREATE TABLE IF NOT EXISTS bot_ticket_messages (
id SERIAL PRIMARY KEY,
ticket_id INTEGER REFERENCES bot_tickets(id) ON DELETE CASCADE NOT NULL,
direction VARCHAR(10) NOT NULL,
text TEXT DEFAULT '',
sender VARCHAR(50) DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS bot_ticket_statuses (
id SERIAL PRIMARY KEY,
ticket_id INTEGER REFERENCES bot_tickets(id) ON DELETE CASCADE NOT NULL,
old_status VARCHAR(20) DEFAULT '',
new_status VARCHAR(20) NOT NULL,
changed_by VARCHAR(50) DEFAULT '',
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
-- ===================== Add recipient_ids to broadcasts if missing =====================
ALTER TABLE bot_broadcasts ADD COLUMN IF NOT EXISTS recipient_ids JSONB DEFAULT '[]'::jsonb;
-- ===================== Ensure new columns exist on existing tables =====================
ALTER TABLE bot_notifications ADD COLUMN IF NOT EXISTS recipient_max_user_id BIGINT NULL;
ALTER TABLE bot_notifications ADD COLUMN IF NOT EXISTS related_type VARCHAR(50) DEFAULT '';
ALTER TABLE bot_notifications ADD COLUMN IF NOT EXISTS related_id INTEGER NULL;
ALTER TABLE bot_notification_subscriptions ADD COLUMN IF NOT EXISTS notify_on_status_change BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE bot_notification_subscriptions ADD COLUMN IF NOT EXISTS notify_on_reply BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE bot_notification_subscriptions ADD COLUMN IF NOT EXISTS notify_on_broadcast BOOLEAN NOT NULL DEFAULT FALSE;
-- Drop old subscribe_types column if it exists (replaced by boolean columns)
ALTER TABLE bot_notification_subscriptions DROP COLUMN IF EXISTS subscribe_types;
-- ===================== Seed: enable all features =====================
UPDATE bot_features SET enabled = true WHERE enabled = false;
-- ===================== Seed: risk_questions =====================
INSERT INTO bot_risk_questions (question_key, question, weight, sort_order, is_active) VALUES
('no_archive', '📋 Отсутствует архив документации?', 25, 1, true),
('no_power_backup', '🔋 Нет резервного питания?', 20, 2, true),
('no_regulations', '📜 Нет регламентов обслуживания?', 15, 3, true),
('system_failures', '⚠️ Были отказы системы за последний год?', 20, 4, true),
('no_documentation', '🗂️ Нет схем и паспортов объекта?', 10, 5, true),
('outdated_equipment', '🕰️ Оборудование старше 5 лет?', 5, 6, true),
('no_monitoring', '📡 Нет удалённого мониторинга?', 5, 7, true)
ON CONFLICT (question_key) DO NOTHING;
-- ===================== Seed: default role permissions for new menu items =====================
INSERT INTO role_menu_permissions (role, menu_key, visible)
SELECT * FROM (VALUES
('engineer', 'bot_tickets', true),
('engineer', 'bot_notifications', true),
('engineer', 'bot_risk_questions', true)
) AS v(role, menu_key, visible)
WHERE NOT EXISTS (
SELECT 1 FROM role_menu_permissions r
WHERE r.role = v.role AND r.menu_key = v.menu_key
);
COMMIT;