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
@@ -0,0 +1,139 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* { margin:0; padding:0; box-sizing:border-box; }
body { font-family:'Inter','Segoe UI',sans-serif; background:#1B2838; color:#E8EDF2; font-size:13px; height:100vh; display:flex; flex-direction:column; }
.test-container { display:flex; flex:1; gap:12px; padding:12px; overflow:hidden; }
.test-chat { flex:1; border:1px solid #253248; border-radius:10px; display:flex; flex-direction:column; overflow:hidden; }
.test-chat-header { background:#0A1628; padding:10px 14px; font-weight:600; border-bottom:1px solid #253248; font-size:12px; }
.test-chat-messages { flex:1; overflow-y:auto; padding:10px 14px; }
.test-chat-input { display:flex; border-top:1px solid #253248; }
.test-chat-input input { flex:1; border:none; padding:10px 14px; font-size:13px; outline:none; background:#1B2838; color:#E8EDF2; }
.test-chat-input button { padding:10px 16px; border:none; background:#00ADEF; color:#fff; cursor:pointer; font-weight:600; }
.test-log { width:300px; border:1px solid #253248; border-radius:10px; padding:10px; overflow-y:auto; font-family:monospace; font-size:11px; line-height:1.5; background:#0A1628; }
.test-log .log-entry { margin-bottom:4px; padding:3px 0; border-bottom:1px solid #1e3044; }
.test-log .log-entry .time { color:#6B8299; }
.test-log .log-entry .event { color:#00ADEF; font-weight:600; }
.msg-bot { background:#1e3044; padding:7px 12px; border-radius:8px 8px 8px 0; margin-bottom:6px; max-width:85%; }
.msg-user { background:#00ADEF; color:#fff; padding:7px 12px; border-radius:8px 8px 0 8px; margin-bottom:6px; max-width:85%; margin-left:auto; }
.test-scenarios { display:flex; gap:6px; padding:8px 12px; flex-wrap:wrap; border-bottom:1px solid #253248; }
.test-scenarios button { font-size:11px; padding:4px 10px; background:#253248; color:#E8EDF2; border:1px solid #3a4a60; border-radius:4px; cursor:pointer; }
.test-scenarios button:hover { background:#00ADEF; color:#fff; }
</style>
</head>
<body>
<div class="test-scenarios">
<button onclick="runScenario('greeting')">Приветствие</button>
<button onclick="runScenario('consent_yes')">Согласие → заявка</button>
<button onclick="runScenario('consent_no')">Отказ согласия</button>
<button onclick="runScenario('question')">Общий вопрос</button>
<button onclick="clearTest()">Очистить</button>
</div>
<div class="test-container">
<div class="test-chat">
<div class="test-chat-header">Чат с ботом (симуляция)</div>
<div class="test-chat-messages" id="test-messages"></div>
<div class="test-chat-input">
<input type="text" id="test-input" placeholder="Введите сообщение..." onkeypress="if(event.key==='Enter') sendTestMessage()">
<button onclick="sendTestMessage()">Отправить</button>
</div>
</div>
<div class="test-log" id="test-log"></div>
</div>
<script>
var testUserId = 999999001;
function log(event, detail){
var logDiv = document.getElementById('test-log');
var time = new Date().toLocaleTimeString();
var entry = document.createElement('div');
entry.className = 'log-entry';
entry.innerHTML = '<span class="time">[' + time + ']</span> <span class="event">' + event + '</span> ' + (detail || '');
logDiv.appendChild(entry);
logDiv.scrollTop = logDiv.scrollHeight;
}
function addMessage(text, from){
var msgs = document.getElementById('test-messages');
var div = document.createElement('div');
div.className = from === 'bot' ? 'msg-bot' : 'msg-user';
div.textContent = text;
msgs.appendChild(div);
msgs.scrollTop = msgs.scrollHeight;
}
function sendTestMessage(){
var input = document.getElementById('test-input');
var text = input.value.trim();
if(!text) return;
input.value = '';
addMessage(text, 'user');
log('message_created', 'Пользователь: ' + text);
simulateWebhook('message_created', text);
}
function runScenario(scenario){
clearTest();
switch(scenario){
case 'greeting':
log('bot_started', 'Пользователь начал диалог');
simulateWebhook('bot_started', null);
break;
case 'consent_yes':
log('bot_started', 'Пользователь начал диалог');
simulateWebhook('bot_started', null);
setTimeout(function(){
addMessage('Здравствуйте! Я хочу оставить заявку.', 'user');
log('message_created', 'Пользователь: хочу оставить заявку');
simulateWebhook('message_created', 'Здравствуйте! Я хочу оставить заявку.');
}, 1000);
break;
case 'consent_no':
log('bot_started', 'Пользователь начал диалог');
simulateWebhook('bot_started', null);
break;
case 'question':
log('bot_started', 'Пользователь начал диалог');
simulateWebhook('bot_started', null);
setTimeout(function(){
addMessage('Какие услуги вы предоставляете?', 'user');
log('message_created', 'Пользователь: какие услуги?');
simulateWebhook('message_created', 'Какие услуги вы предоставляете?');
}, 1000);
break;
}
}
function simulateWebhook(type, text){
var payload = {update_type: type};
if(type === 'bot_started'){
payload.user = {user_id: testUserId, first_name: 'Тест', last_name: 'Пользователь'};
payload.chat_id = testUserId;
} else if(type === 'message_created'){
payload.sender = {user_id: testUserId};
payload.chat_id = testUserId;
payload.body = {text: text, attachments: []};
}
log('webhook', 'Отправка на /webhook (' + type + ')');
fetch('/api/bot/webhook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Max-Bot-Api-Secret': 'aegisone_bot_secret_2026',
},
body: JSON.stringify(payload)
}).then(function(r){ return r.json() }).then(function(d){
log('response', JSON.stringify(d));
if(d && d.ok) log('success', 'Событие обработано');
else log('error', 'Ошибка: ' + JSON.stringify(d));
}).catch(function(err){
log('error', 'Сетевая ошибка: ' + err);
});
}
function clearTest(){
document.getElementById('test-messages').innerHTML = '';
document.getElementById('test-log').innerHTML = '';
document.getElementById('test-input').value = '';
log('info', 'Тест очищен');
}
</script>
</body>
</html>