@omnizap-system/omnizap 2.6.1 → 2.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +78 -9
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/security-runner-hardening.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +1 -0
- package/app/config/index.js +6 -0
- package/app/configParts/adminIdentity.js +36 -7
- package/app/configParts/baileysConfig.js +343 -56
- package/app/configParts/groupUtils.js +226 -0
- package/app/configParts/loggerConfig.js +185 -0
- package/app/configParts/messagePersistenceService.js +307 -5
- package/app/configParts/sessionConfig.js +242 -0
- package/app/connection/baileysCompatibility.test.js +10 -1
- package/app/connection/baileysDbAuthState.js +205 -9
- package/app/connection/baileysLibsignalPatch.js +210 -0
- package/app/connection/groupOwnerWriteStateResolver.js +141 -0
- package/app/connection/socketController.js +694 -123
- package/app/connection/socketController.multiSession.test.js +128 -0
- package/app/controllers/messageController.js +1 -1
- package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
- package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +96 -4
- package/app/controllers/messageProcessingPipeline.js +90 -9
- package/app/controllers/messageProcessingPipeline.test.js +202 -0
- package/app/modules/adminModule/AGENT.md +1 -1
- package/app/modules/adminModule/commandConfig.json +3318 -1347
- package/app/modules/adminModule/groupCommandHandlers.js +856 -14
- package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
- package/app/modules/adminModule/groupWarningRepository.js +152 -0
- package/app/modules/aiModule/AGENT.md +47 -30
- package/app/modules/aiModule/aiConfigRuntime.js +1 -0
- package/app/modules/aiModule/catCommand.js +132 -25
- package/app/modules/aiModule/commandConfig.json +114 -28
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
- package/app/modules/gameModule/AGENT.md +1 -1
- package/app/modules/gameModule/commandConfig.json +29 -0
- package/app/modules/menuModule/AGENT.md +1 -1
- package/app/modules/menuModule/commandConfig.json +45 -10
- package/app/modules/menuModule/menuCatalogService.js +190 -0
- package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
- package/app/modules/menuModule/menuDynamicService.js +511 -0
- package/app/modules/menuModule/menuDynamicService.test.js +141 -0
- package/app/modules/menuModule/menus.js +36 -5
- package/app/modules/playModule/AGENT.md +10 -5
- package/app/modules/playModule/commandConfig.json +74 -16
- package/app/modules/playModule/playCommandConstants.js +13 -7
- package/app/modules/playModule/playCommandCore.js +4 -6
- package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
- package/app/modules/playModule/playConfigRuntime.js +5 -6
- package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerPackModule/AGENT.md +1 -1
- package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
- package/app/modules/stickerPackModule/commandConfig.json +29 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/geminiService.js +131 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/moduleAiHelpCoreService.js +33 -4
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +51 -21
- package/app/services/messaging/newsBroadcastService.js +843 -27
- package/app/services/multiSession/assignmentBalancerService.js +452 -0
- package/app/services/multiSession/groupOwnershipRepository.js +346 -0
- package/app/services/multiSession/groupOwnershipService.js +809 -0
- package/app/services/multiSession/groupOwnershipService.test.js +317 -0
- package/app/services/multiSession/sessionRegistryService.js +239 -0
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/groupConfigStore.js +41 -5
- package/app/store/premiumUserStore.js +21 -7
- package/app/utils/antiLink/antiLinkModule.js +391 -25
- package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
- package/database/index.js +6 -0
- package/database/migrations/20260307_d0_hardening_down.sql +1 -1
- package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
- package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
- package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
- package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
- package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
- package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
- package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
- package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
- package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
- package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
- package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
- package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
- package/database/schema.sql +102 -1
- package/docker-compose.yml +4 -1
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
- package/docs/security/omnizap-static-security-headers.conf +25 -0
- package/ecosystem.prod.config.cjs +31 -11
- package/index.js +52 -18
- package/observability/alert-rules.yml +20 -0
- package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
- package/observability/mysql-setup.sql +4 -4
- package/observability/system-admin-observability.md +26 -0
- package/package.json +14 -6
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/css/payments-react.css +478 -0
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/homeReactApp.js +2 -2
- package/public/js/apps/paymentsCancelReactApp.js +45 -0
- package/public/js/apps/paymentsReactApp.js +399 -0
- package/public/js/apps/paymentsSuccessReactApp.js +148 -0
- package/public/js/apps/stickersApp.js +255 -103
- package/public/js/apps/termsReactApp.js +57 -8
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +96 -47
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/pagamentos-cancelado.html +21 -0
- package/public/pages/pagamentos-sucesso.html +21 -0
- package/public/pages/pagamentos.html +30 -0
- package/public/pages/politica-de-privacidade.html +1 -1
- package/public/pages/stickers.html +5 -5
- package/public/pages/termos-de-uso-texto-integral.html +1 -1
- package/public/pages/termos-de-uso.html +1 -1
- package/public/pages/user-password-reset.html +3 -4
- package/public/pages/user-systemadm.html +8 -462
- package/public/pages/user.html +1 -1
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +13 -0
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +155 -0
- package/scripts/new-whatsapp-session.sh +564 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +254 -0
- package/server/controllers/payments/paymentsController.js +731 -0
- package/server/controllers/sticker/stickerCatalogController.js +9 -23
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
- package/server/controllers/system/systemController.js +228 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailAutomationRuntime.js +36 -1
- package/server/email/emailAutomationService.js +42 -1
- package/server/email/emailTemplateService.js +140 -33
- package/server/http/httpRequestUtils.js +18 -14
- package/server/http/httpServer.js +8 -4
- package/server/middleware/securityHeaders.js +35 -3
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +50 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/payments/paymentsRouter.js +47 -0
- package/server/routes/static/staticPageRouter.js +30 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +5 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
|
|
2
|
+
import logger from '#logger';
|
|
3
|
+
import { getAllToolRecords } from '../services/ai/moduleToolRegistryService.js';
|
|
4
|
+
import { applyCommandConfigEnrichmentSuggestion, saveCommandConfigEnrichmentSuggestion } from '../services/ai/commandConfigEnrichmentRepository.js';
|
|
5
|
+
import { generateCommandConfigEnrichmentSuggestion } from '../services/ai/commandConfigEnrichmentService.js';
|
|
6
|
+
import { upsertAiHelpCachedResponse } from '../services/ai/aiHelpResponseCacheRepository.js';
|
|
7
|
+
import { markToolCandidateCommandConfigCacheDirty } from '../services/ai/toolCandidateSelectorService.js';
|
|
8
|
+
|
|
9
|
+
const DEFAULT_INTERVAL_MS = 4 * 60 * 1000;
|
|
10
|
+
const DEFAULT_BATCH_SIZE = 12;
|
|
11
|
+
const DEFAULT_MIN_AUTO_APPLY_CONFIDENCE = 0.7;
|
|
12
|
+
const DEFAULT_MAX_HELP_QUESTIONS_PER_COMMAND = 3;
|
|
13
|
+
const DEFAULT_MAX_HELP_CALLS_PER_CYCLE = 42;
|
|
14
|
+
|
|
15
|
+
const parseEnvBool = (value, fallback) => {
|
|
16
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
17
|
+
const normalized = String(value).trim().toLowerCase();
|
|
18
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
19
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
20
|
+
return fallback;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const parseEnvInt = (value, fallback, min, max) => {
|
|
24
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
25
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
26
|
+
return Math.max(min, Math.min(max, parsed));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const parseEnvFloat = (value, fallback, min, max) => {
|
|
30
|
+
const parsed = Number.parseFloat(String(value ?? ''));
|
|
31
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
32
|
+
return Math.max(min, Math.min(max, parsed));
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const AI_HELP_CONTINUOUS_LEARNING_ENABLED = parseEnvBool(process.env.AI_HELP_CONTINUOUS_LEARNING_ENABLED, true);
|
|
36
|
+
const AI_HELP_CONTINUOUS_LEARNING_INTERVAL_MS = parseEnvInt(process.env.AI_HELP_CONTINUOUS_LEARNING_INTERVAL_MS, DEFAULT_INTERVAL_MS, 45_000, 24 * 60 * 60 * 1000);
|
|
37
|
+
const AI_HELP_CONTINUOUS_LEARNING_BATCH_SIZE = parseEnvInt(process.env.AI_HELP_CONTINUOUS_LEARNING_BATCH_SIZE, DEFAULT_BATCH_SIZE, 1, 120);
|
|
38
|
+
const AI_HELP_CONTINUOUS_LEARNING_MIN_AUTO_APPLY_CONFIDENCE = parseEnvFloat(process.env.AI_HELP_CONTINUOUS_LEARNING_MIN_AUTO_APPLY_CONFIDENCE, DEFAULT_MIN_AUTO_APPLY_CONFIDENCE, 0.1, 0.99);
|
|
39
|
+
const AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_QUESTIONS_PER_COMMAND = parseEnvInt(process.env.AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_QUESTIONS_PER_COMMAND, DEFAULT_MAX_HELP_QUESTIONS_PER_COMMAND, 1, 12);
|
|
40
|
+
const AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_CALLS_PER_CYCLE = parseEnvInt(process.env.AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_CALLS_PER_CYCLE, DEFAULT_MAX_HELP_CALLS_PER_CYCLE, 1, 250);
|
|
41
|
+
|
|
42
|
+
let schedulerHandle = null;
|
|
43
|
+
let schedulerStarted = false;
|
|
44
|
+
let cycleInProgress = false;
|
|
45
|
+
let proactiveCursorIndex = 0;
|
|
46
|
+
let proactiveRound = 0;
|
|
47
|
+
let proactiveRegistrySignature = '';
|
|
48
|
+
|
|
49
|
+
const normalizeText = (value) =>
|
|
50
|
+
String(value || '')
|
|
51
|
+
.trim()
|
|
52
|
+
.toLowerCase()
|
|
53
|
+
.normalize('NFD')
|
|
54
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
55
|
+
.replace(/[^a-z0-9\s/_.-]/g, ' ')
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim();
|
|
58
|
+
|
|
59
|
+
const normalizeDisplayText = (value) =>
|
|
60
|
+
String(value || '')
|
|
61
|
+
.trim()
|
|
62
|
+
.replace(/\s+/g, ' ');
|
|
63
|
+
|
|
64
|
+
const uniqueList = (items = [], limit = 8) => {
|
|
65
|
+
const output = [];
|
|
66
|
+
const seen = new Set();
|
|
67
|
+
for (const item of Array.isArray(items) ? items : []) {
|
|
68
|
+
const normalized = normalizeDisplayText(item);
|
|
69
|
+
if (!normalized) continue;
|
|
70
|
+
const dedupeKey = normalizeText(normalized);
|
|
71
|
+
if (!dedupeKey || seen.has(dedupeKey)) continue;
|
|
72
|
+
seen.add(dedupeKey);
|
|
73
|
+
output.push(normalized);
|
|
74
|
+
if (output.length >= limit) break;
|
|
75
|
+
}
|
|
76
|
+
return output;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const ensureArray = (value) => (Array.isArray(value) ? value : []);
|
|
80
|
+
const pickFirstText = (...values) => {
|
|
81
|
+
for (const value of values) {
|
|
82
|
+
const text = String(value ?? '').trim();
|
|
83
|
+
if (text) return text;
|
|
84
|
+
}
|
|
85
|
+
return '';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const readCommandUsage = (entry = {}) => {
|
|
89
|
+
const usageV2 = ensureArray(entry?.usage);
|
|
90
|
+
if (usageV2.length) return usageV2;
|
|
91
|
+
const docsUsage = ensureArray(entry?.docs?.usage_examples);
|
|
92
|
+
if (docsUsage.length) return docsUsage;
|
|
93
|
+
return ensureArray(entry?.metodos_de_uso);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const readCommandFaqPatterns = (entry = {}) => {
|
|
97
|
+
const discovery = entry?.discovery && typeof entry.discovery === 'object' && !Array.isArray(entry.discovery) ? entry.discovery : {};
|
|
98
|
+
const source = ensureArray(discovery?.faq_queries).length ? discovery.faq_queries : entry?.faq_patterns;
|
|
99
|
+
return ensureArray(source);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const readCommandUserPhrasings = (entry = {}) => {
|
|
103
|
+
const discovery = entry?.discovery && typeof entry.discovery === 'object' && !Array.isArray(entry.discovery) ? entry.discovery : {};
|
|
104
|
+
const source = ensureArray(discovery?.user_phrasings).length ? discovery.user_phrasings : entry?.user_phrasings;
|
|
105
|
+
return ensureArray(source);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const readCommandDescription = (entry = {}) => pickFirstText(entry?.description, entry?.docs?.summary, entry?.descricao);
|
|
109
|
+
|
|
110
|
+
const readCommandPermission = (entry = {}) => pickFirstText(entry?.permission, entry?.permissao_necessaria);
|
|
111
|
+
|
|
112
|
+
const readCommandContexts = (entry = {}) => {
|
|
113
|
+
const contextsV2 = ensureArray(entry?.contexts);
|
|
114
|
+
if (contextsV2.length) return contextsV2;
|
|
115
|
+
return ensureArray(entry?.local_de_uso);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const readCommandUsageLimit = (entry = {}) => pickFirstText(entry?.limits?.usage_description, entry?.limite_de_uso);
|
|
119
|
+
|
|
120
|
+
const renderUsage = (method, commandPrefix = '/') => String(method || '').replaceAll('<prefix>', commandPrefix);
|
|
121
|
+
|
|
122
|
+
const computeRegistrySignature = (records = []) => records.map((record) => `${record?.toolName || ''}:${record?.moduleKey || ''}:${record?.commandName || ''}`).join('|');
|
|
123
|
+
|
|
124
|
+
const buildDeterministicExplainAnswer = ({ record, commandPrefix = '/' }) => {
|
|
125
|
+
const entry = record?.commandEntry && typeof record.commandEntry === 'object' ? record.commandEntry : {};
|
|
126
|
+
const commandName = String(record?.commandName || '').trim();
|
|
127
|
+
const commandToken = `${commandPrefix}${commandName}`;
|
|
128
|
+
const usage = readCommandUsage(entry).map((line) => renderUsage(line, commandPrefix));
|
|
129
|
+
const description = readCommandDescription(entry) || 'Sem descricao cadastrada.';
|
|
130
|
+
const permission = readCommandPermission(entry) || 'nao definido';
|
|
131
|
+
const contexts = readCommandContexts(entry);
|
|
132
|
+
const whereLabel = contexts.length ? contexts.join(', ') : 'nao definido';
|
|
133
|
+
const usageLimit = readCommandUsageLimit(entry) || 'nao informado';
|
|
134
|
+
|
|
135
|
+
const lines = [`Comando: ${commandToken}`, `Resumo: ${description}`, '', `Quem pode usar: ${permission}`, `Onde pode usar: ${whereLabel}`, `Limite: ${usageLimit}`, '', 'Como usar:', ...(usage.length ? usage.map((line) => `- ${line}`) : [`- ${commandToken}`]), '', 'Resposta pre-carregada em background para acelerar o IA Helper.'];
|
|
136
|
+
return lines.join('\n');
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const buildDeterministicQuestionAnswer = ({ question, record, commandPrefix = '/' }) => {
|
|
140
|
+
const entry = record?.commandEntry && typeof record.commandEntry === 'object' ? record.commandEntry : {};
|
|
141
|
+
const commandName = String(record?.commandName || '').trim();
|
|
142
|
+
const commandToken = `${commandPrefix}${commandName}`;
|
|
143
|
+
const usage = readCommandUsage(entry).map((line) => renderUsage(line, commandPrefix));
|
|
144
|
+
const description = readCommandDescription(entry) || 'Sem descricao cadastrada.';
|
|
145
|
+
const start = normalizeDisplayText(question);
|
|
146
|
+
|
|
147
|
+
return [start ? `Pergunta: ${start}` : `Comando: ${commandToken}`, '', `Resumo: ${description}`, usage.length ? `Exemplo: ${usage[0]}` : `Exemplo: ${commandToken}`, `Para detalhes completos, use: ${commandPrefix}help ${commandName}`].join('\n');
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const buildSyntheticEvent = ({ record, round = 0 }) => {
|
|
151
|
+
const entry = record?.commandEntry && typeof record.commandEntry === 'object' ? record.commandEntry : {};
|
|
152
|
+
const commandName = String(record?.commandName || '').trim();
|
|
153
|
+
const usage = readCommandUsage(entry);
|
|
154
|
+
const faq = readCommandFaqPatterns(entry);
|
|
155
|
+
const phrasings = readCommandUserPhrasings(entry);
|
|
156
|
+
|
|
157
|
+
const candidates = uniqueList([...phrasings, ...faq, ...usage, `como usar ${commandName}`, `o que faz ${commandName}`, `quando devo usar ${commandName}`, `me explica o comando ${commandName}`], 18);
|
|
158
|
+
|
|
159
|
+
const picked = candidates.length ? candidates[round % candidates.length] : `como usar ${commandName}`;
|
|
160
|
+
return {
|
|
161
|
+
id: null,
|
|
162
|
+
user_question: picked,
|
|
163
|
+
normalized_question: normalizeText(picked),
|
|
164
|
+
tool_suggested: record?.toolName || commandName,
|
|
165
|
+
tool_executed: record?.toolName || commandName,
|
|
166
|
+
success: true,
|
|
167
|
+
confidence: 0.92,
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const buildHelpWarmupQuestions = ({ record, syntheticEvent }) => {
|
|
172
|
+
const entry = record?.commandEntry && typeof record.commandEntry === 'object' ? record.commandEntry : {};
|
|
173
|
+
const commandName = String(record?.commandName || '').trim();
|
|
174
|
+
const usage = readCommandUsage(entry);
|
|
175
|
+
const faq = readCommandFaqPatterns(entry);
|
|
176
|
+
const phrasings = readCommandUserPhrasings(entry);
|
|
177
|
+
|
|
178
|
+
return uniqueList([syntheticEvent?.user_question || '', ...phrasings, ...faq, ...usage, `como usar /${commandName}`, `quero exemplo real de ${commandName}`, `o que eu recebo de resposta ao usar ${commandName}`], AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_QUESTIONS_PER_COMMAND);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const saveAiHelpSeedCacheEntries = async ({ record, syntheticEvent, maxEntries = 3 }) => {
|
|
182
|
+
const result = {
|
|
183
|
+
helpCalls: 0,
|
|
184
|
+
helpErrors: 0,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const commandName = String(record?.commandName || '').trim();
|
|
188
|
+
const moduleKey = String(record?.moduleKey || '').trim();
|
|
189
|
+
if (!commandName || !moduleKey) return result;
|
|
190
|
+
|
|
191
|
+
const explainQuestion = `explicar comando ${commandName}`;
|
|
192
|
+
const explainAnswer = buildDeterministicExplainAnswer({
|
|
193
|
+
record,
|
|
194
|
+
commandPrefix: '/',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
await upsertAiHelpCachedResponse({
|
|
199
|
+
moduleKey,
|
|
200
|
+
scope: 'command_explain',
|
|
201
|
+
question: explainQuestion,
|
|
202
|
+
normalizedQuestion: explainQuestion,
|
|
203
|
+
answer: explainAnswer,
|
|
204
|
+
source: 'continuous_seed',
|
|
205
|
+
commandName,
|
|
206
|
+
metadata: {
|
|
207
|
+
mode: 'continuous_learning',
|
|
208
|
+
reason: 'command_explain_seed',
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
result.helpCalls += 1;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
result.helpErrors += 1;
|
|
214
|
+
logger.warn('Falha ao persistir seed de command_explain no IA Helper continuo.', {
|
|
215
|
+
action: 'ai_helper_continuous_learning_cache_seed_failed',
|
|
216
|
+
module: moduleKey,
|
|
217
|
+
command: commandName,
|
|
218
|
+
error: error?.message,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const questions = buildHelpWarmupQuestions({
|
|
223
|
+
record,
|
|
224
|
+
syntheticEvent,
|
|
225
|
+
}).slice(0, Math.max(0, maxEntries - result.helpCalls));
|
|
226
|
+
|
|
227
|
+
for (const question of questions) {
|
|
228
|
+
try {
|
|
229
|
+
await upsertAiHelpCachedResponse({
|
|
230
|
+
moduleKey,
|
|
231
|
+
scope: 'question',
|
|
232
|
+
question,
|
|
233
|
+
normalizedQuestion: question,
|
|
234
|
+
answer: buildDeterministicQuestionAnswer({
|
|
235
|
+
question,
|
|
236
|
+
record,
|
|
237
|
+
commandPrefix: '/',
|
|
238
|
+
}),
|
|
239
|
+
source: 'continuous_seed',
|
|
240
|
+
commandName,
|
|
241
|
+
metadata: {
|
|
242
|
+
mode: 'continuous_learning',
|
|
243
|
+
reason: 'question_seed',
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
result.helpCalls += 1;
|
|
247
|
+
} catch (error) {
|
|
248
|
+
result.helpErrors += 1;
|
|
249
|
+
logger.warn('Falha ao persistir seed de question no IA Helper continuo.', {
|
|
250
|
+
action: 'ai_helper_continuous_learning_cache_question_seed_failed',
|
|
251
|
+
module: moduleKey,
|
|
252
|
+
command: commandName,
|
|
253
|
+
question,
|
|
254
|
+
error: error?.message,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return result;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const selectProactiveBatch = () => {
|
|
263
|
+
const records = getAllToolRecords();
|
|
264
|
+
if (!records.length) {
|
|
265
|
+
proactiveCursorIndex = 0;
|
|
266
|
+
proactiveRound = 0;
|
|
267
|
+
proactiveRegistrySignature = '';
|
|
268
|
+
return {
|
|
269
|
+
records: [],
|
|
270
|
+
batch: [],
|
|
271
|
+
previousCursor: 0,
|
|
272
|
+
nextCursor: 0,
|
|
273
|
+
completedRound: false,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const signature = computeRegistrySignature(records);
|
|
278
|
+
if (signature !== proactiveRegistrySignature) {
|
|
279
|
+
proactiveRegistrySignature = signature;
|
|
280
|
+
proactiveCursorIndex = 0;
|
|
281
|
+
proactiveRound = 0;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const previousCursor = proactiveCursorIndex;
|
|
285
|
+
const safeBatchSize = Math.max(1, Math.min(AI_HELP_CONTINUOUS_LEARNING_BATCH_SIZE, records.length));
|
|
286
|
+
const batch = [];
|
|
287
|
+
for (let index = 0; index < safeBatchSize; index += 1) {
|
|
288
|
+
const cursor = (proactiveCursorIndex + index) % records.length;
|
|
289
|
+
batch.push(records[cursor]);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
proactiveCursorIndex = (proactiveCursorIndex + safeBatchSize) % records.length;
|
|
293
|
+
const completedRound = records.length > 0 && proactiveCursorIndex === 0;
|
|
294
|
+
if (completedRound) proactiveRound += 1;
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
records,
|
|
298
|
+
batch,
|
|
299
|
+
previousCursor,
|
|
300
|
+
nextCursor: proactiveCursorIndex,
|
|
301
|
+
completedRound,
|
|
302
|
+
};
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const processProactiveCommand = async ({ record, round = 0, helpCallBudget = Infinity }) => {
|
|
306
|
+
const syntheticEvent = buildSyntheticEvent({
|
|
307
|
+
record,
|
|
308
|
+
round,
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const result = {
|
|
312
|
+
suggestionGenerated: 0,
|
|
313
|
+
suggestionApplied: 0,
|
|
314
|
+
suggestionChanged: 0,
|
|
315
|
+
helpCalls: 0,
|
|
316
|
+
helpErrors: 0,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const suggestionOutput = await generateCommandConfigEnrichmentSuggestion({
|
|
320
|
+
learningEvent: syntheticEvent,
|
|
321
|
+
toolRecord: record,
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
if (suggestionOutput?.suggestion) {
|
|
325
|
+
const savedSuggestion = await saveCommandConfigEnrichmentSuggestion({
|
|
326
|
+
moduleKey: record.moduleKey,
|
|
327
|
+
commandName: record.commandName,
|
|
328
|
+
sourceTool: record.toolName,
|
|
329
|
+
sourceEventId: null,
|
|
330
|
+
question: syntheticEvent.user_question,
|
|
331
|
+
normalizedQuestion: syntheticEvent.normalized_question,
|
|
332
|
+
suggestion: suggestionOutput.suggestion,
|
|
333
|
+
confidence: suggestionOutput.confidence,
|
|
334
|
+
modelName: suggestionOutput.modelName,
|
|
335
|
+
source: suggestionOutput.source,
|
|
336
|
+
status: 'pending',
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (savedSuggestion?.id) {
|
|
340
|
+
result.suggestionGenerated += 1;
|
|
341
|
+
const sourceValue = String(savedSuggestion.source || '');
|
|
342
|
+
const shouldAutoApply = savedSuggestion.confidence >= AI_HELP_CONTINUOUS_LEARNING_MIN_AUTO_APPLY_CONFIDENCE && sourceValue.startsWith('llm');
|
|
343
|
+
|
|
344
|
+
if (shouldAutoApply) {
|
|
345
|
+
const applyResult = await applyCommandConfigEnrichmentSuggestion({
|
|
346
|
+
suggestionId: savedSuggestion.id,
|
|
347
|
+
reviewNotes: `auto_apply_proactive: confidence>=${AI_HELP_CONTINUOUS_LEARNING_MIN_AUTO_APPLY_CONFIDENCE}`,
|
|
348
|
+
});
|
|
349
|
+
if (applyResult?.applied) {
|
|
350
|
+
result.suggestionApplied += 1;
|
|
351
|
+
if (applyResult.changed) result.suggestionChanged += 1;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (helpCallBudget <= 0) {
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const cacheSeedResult = await saveAiHelpSeedCacheEntries({
|
|
362
|
+
record,
|
|
363
|
+
syntheticEvent,
|
|
364
|
+
maxEntries: Math.max(1, helpCallBudget),
|
|
365
|
+
});
|
|
366
|
+
result.helpCalls += cacheSeedResult.helpCalls;
|
|
367
|
+
result.helpErrors += cacheSeedResult.helpErrors;
|
|
368
|
+
|
|
369
|
+
return result;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const processContinuousLearningBatch = async ({ reason = 'scheduler' } = {}) => {
|
|
373
|
+
if (cycleInProgress) return;
|
|
374
|
+
if (!AI_HELP_CONTINUOUS_LEARNING_ENABLED) return;
|
|
375
|
+
|
|
376
|
+
cycleInProgress = true;
|
|
377
|
+
const startedAt = __timeNowMs();
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const selected = selectProactiveBatch();
|
|
381
|
+
if (!selected.batch.length) {
|
|
382
|
+
logger.info('Worker de aprendizado continuo IA sem comandos no registry.', {
|
|
383
|
+
action: 'ai_helper_continuous_learning_cycle_processed',
|
|
384
|
+
reason,
|
|
385
|
+
fetched_commands: 0,
|
|
386
|
+
generated_suggestions: 0,
|
|
387
|
+
applied_suggestions: 0,
|
|
388
|
+
changed_suggestions: 0,
|
|
389
|
+
help_calls: 0,
|
|
390
|
+
help_errors: 0,
|
|
391
|
+
duration_ms: __timeNowMs() - startedAt,
|
|
392
|
+
});
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let generatedSuggestions = 0;
|
|
397
|
+
let appliedSuggestions = 0;
|
|
398
|
+
let changedSuggestions = 0;
|
|
399
|
+
let helpCalls = 0;
|
|
400
|
+
let helpErrors = 0;
|
|
401
|
+
let processedCommands = 0;
|
|
402
|
+
|
|
403
|
+
for (const record of selected.batch) {
|
|
404
|
+
if (!record) continue;
|
|
405
|
+
const remainingHelpBudget = AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_CALLS_PER_CYCLE - helpCalls;
|
|
406
|
+
if (remainingHelpBudget <= 0 && processedCommands > 0) {
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
try {
|
|
411
|
+
const commandResult = await processProactiveCommand({
|
|
412
|
+
record,
|
|
413
|
+
round: proactiveRound,
|
|
414
|
+
helpCallBudget: Math.max(0, remainingHelpBudget),
|
|
415
|
+
});
|
|
416
|
+
generatedSuggestions += commandResult.suggestionGenerated;
|
|
417
|
+
appliedSuggestions += commandResult.suggestionApplied;
|
|
418
|
+
changedSuggestions += commandResult.suggestionChanged;
|
|
419
|
+
helpCalls += commandResult.helpCalls;
|
|
420
|
+
helpErrors += commandResult.helpErrors;
|
|
421
|
+
processedCommands += 1;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
logger.warn('Falha ao processar comando no worker de aprendizado continuo IA.', {
|
|
424
|
+
action: 'ai_helper_continuous_learning_command_failed',
|
|
425
|
+
module: record?.moduleKey || null,
|
|
426
|
+
command: record?.commandName || null,
|
|
427
|
+
error: error?.message,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (changedSuggestions > 0) {
|
|
433
|
+
markToolCandidateCommandConfigCacheDirty();
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
logger.info('Ciclo do worker de aprendizado continuo IA concluido.', {
|
|
437
|
+
action: 'ai_helper_continuous_learning_cycle_processed',
|
|
438
|
+
reason,
|
|
439
|
+
previous_cursor: selected.previousCursor,
|
|
440
|
+
next_cursor: selected.nextCursor,
|
|
441
|
+
completed_round: selected.completedRound,
|
|
442
|
+
round: proactiveRound,
|
|
443
|
+
registry_size: selected.records.length,
|
|
444
|
+
fetched_commands: selected.batch.length,
|
|
445
|
+
processed_commands: processedCommands,
|
|
446
|
+
generated_suggestions: generatedSuggestions,
|
|
447
|
+
applied_suggestions: appliedSuggestions,
|
|
448
|
+
changed_suggestions: changedSuggestions,
|
|
449
|
+
help_calls: helpCalls,
|
|
450
|
+
help_errors: helpErrors,
|
|
451
|
+
max_help_calls_per_cycle: AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_CALLS_PER_CYCLE,
|
|
452
|
+
duration_ms: __timeNowMs() - startedAt,
|
|
453
|
+
});
|
|
454
|
+
} finally {
|
|
455
|
+
cycleInProgress = false;
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
export const startAiHelperContinuousLearningWorker = () => {
|
|
460
|
+
if (schedulerStarted) return;
|
|
461
|
+
|
|
462
|
+
if (!AI_HELP_CONTINUOUS_LEARNING_ENABLED) {
|
|
463
|
+
logger.info('Worker de aprendizado continuo IA desativado.', {
|
|
464
|
+
action: 'ai_helper_continuous_learning_worker_disabled',
|
|
465
|
+
enabled: AI_HELP_CONTINUOUS_LEARNING_ENABLED,
|
|
466
|
+
});
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
schedulerStarted = true;
|
|
471
|
+
void processContinuousLearningBatch({ reason: 'startup' });
|
|
472
|
+
|
|
473
|
+
schedulerHandle = setInterval(() => {
|
|
474
|
+
void processContinuousLearningBatch({ reason: 'scheduler' });
|
|
475
|
+
}, AI_HELP_CONTINUOUS_LEARNING_INTERVAL_MS);
|
|
476
|
+
if (typeof schedulerHandle?.unref === 'function') {
|
|
477
|
+
schedulerHandle.unref();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
logger.info('Scheduler do worker de aprendizado continuo IA iniciado.', {
|
|
481
|
+
action: 'ai_helper_continuous_learning_worker_scheduler_started',
|
|
482
|
+
interval_ms: AI_HELP_CONTINUOUS_LEARNING_INTERVAL_MS,
|
|
483
|
+
batch_size: AI_HELP_CONTINUOUS_LEARNING_BATCH_SIZE,
|
|
484
|
+
min_auto_apply_confidence: AI_HELP_CONTINUOUS_LEARNING_MIN_AUTO_APPLY_CONFIDENCE,
|
|
485
|
+
max_help_questions_per_command: AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_QUESTIONS_PER_COMMAND,
|
|
486
|
+
max_help_calls_per_cycle: AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_CALLS_PER_CYCLE,
|
|
487
|
+
});
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
export const stopAiHelperContinuousLearningWorker = () => {
|
|
491
|
+
if (schedulerHandle) {
|
|
492
|
+
clearInterval(schedulerHandle);
|
|
493
|
+
schedulerHandle = null;
|
|
494
|
+
}
|
|
495
|
+
schedulerStarted = false;
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
export const runAiHelperContinuousLearningWorkerOnce = async (reason = 'manual') => {
|
|
499
|
+
await processContinuousLearningBatch({
|
|
500
|
+
reason,
|
|
501
|
+
});
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
export const getAiHelperContinuousLearningWorkerConfig = () => ({
|
|
505
|
+
enabled: AI_HELP_CONTINUOUS_LEARNING_ENABLED,
|
|
506
|
+
intervalMs: AI_HELP_CONTINUOUS_LEARNING_INTERVAL_MS,
|
|
507
|
+
batchSize: AI_HELP_CONTINUOUS_LEARNING_BATCH_SIZE,
|
|
508
|
+
minAutoApplyConfidence: AI_HELP_CONTINUOUS_LEARNING_MIN_AUTO_APPLY_CONFIDENCE,
|
|
509
|
+
maxHelpQuestionsPerCommand: AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_QUESTIONS_PER_COMMAND,
|
|
510
|
+
maxHelpCallsPerCycle: AI_HELP_CONTINUOUS_LEARNING_MAX_HELP_CALLS_PER_CYCLE,
|
|
511
|
+
startedAt: __timeNowIso(),
|
|
512
|
+
});
|
package/database/index.js
CHANGED
|
@@ -102,9 +102,15 @@ export const TABLES = {
|
|
|
102
102
|
MESSAGE_ANALYSIS_EVENT: 'message_analysis_event',
|
|
103
103
|
BAILEYS_EVENT_JOURNAL: 'baileys_event_journal',
|
|
104
104
|
BAILEYS_AUTH_STATE: 'baileys_auth_state',
|
|
105
|
+
WA_SESSION_REGISTRY: 'wa_session_registry',
|
|
106
|
+
GROUP_ASSIGNMENT: 'group_assignment',
|
|
107
|
+
GROUP_ASSIGNMENT_HISTORY: 'group_assignment_history',
|
|
105
108
|
CHATS: 'chats',
|
|
106
109
|
GROUPS_METADATA: 'groups_metadata',
|
|
107
110
|
GROUP_CONFIGS: 'group_configs',
|
|
111
|
+
GROUP_USER_WARNINGS: 'group_user_warnings',
|
|
112
|
+
SYSTEM_PREMIUM_USERS: 'system_premium_users',
|
|
113
|
+
SYSTEM_AI_PROMPTS: 'system_ai_prompts',
|
|
108
114
|
LID_MAP: 'lid_map',
|
|
109
115
|
STICKER_PACK: 'sticker_pack',
|
|
110
116
|
STICKER_ASSET: 'sticker_asset',
|
|
@@ -58,7 +58,7 @@ UPDATE schema_change_log
|
|
|
58
58
|
SET status = 'rolled_back',
|
|
59
59
|
notes = 'D0 rollback executed',
|
|
60
60
|
updated_at = CURRENT_TIMESTAMP
|
|
61
|
-
WHERE migration_key = @migration_key;
|
|
61
|
+
WHERE migration_key = CONVERT(@migration_key USING utf8mb4) COLLATE utf8mb4_unicode_ci;
|
|
62
62
|
|
|
63
63
|
DROP PROCEDURE IF EXISTS __ensure_index;
|
|
64
64
|
DROP PROCEDURE IF EXISTS __drop_index_if_exists;
|
|
@@ -47,7 +47,7 @@ UPDATE schema_change_log
|
|
|
47
47
|
SET status = 'rolled_back',
|
|
48
48
|
notes = 'D+7 rollback executed',
|
|
49
49
|
updated_at = CURRENT_TIMESTAMP
|
|
50
|
-
WHERE migration_key = @migration_key;
|
|
50
|
+
WHERE migration_key = CONVERT(@migration_key USING utf8mb4) COLLATE utf8mb4_unicode_ci;
|
|
51
51
|
|
|
52
52
|
DROP PROCEDURE IF EXISTS __drop_column_if_exists;
|
|
53
53
|
DROP PROCEDURE IF EXISTS __drop_index_if_exists;
|
|
@@ -88,7 +88,7 @@ UPDATE schema_change_log
|
|
|
88
88
|
SET status = 'rolled_back',
|
|
89
89
|
notes = 'D+30 rollback executed',
|
|
90
90
|
updated_at = CURRENT_TIMESTAMP
|
|
91
|
-
WHERE migration_key = @migration_key;
|
|
91
|
+
WHERE migration_key = CONVERT(@migration_key USING utf8mb4) COLLATE utf8mb4_unicode_ci;
|
|
92
92
|
|
|
93
93
|
DROP PROCEDURE IF EXISTS __drop_check_if_exists;
|
|
94
94
|
DROP PROCEDURE IF EXISTS __drop_index_if_exists;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
-- D+35 rollback
|
|
2
|
+
|
|
3
|
+
SET @migration_key := '20260411_d35_group_community_metadata';
|
|
4
|
+
|
|
5
|
+
DROP PROCEDURE IF EXISTS __drop_column_if_exists;
|
|
6
|
+
DROP PROCEDURE IF EXISTS __drop_index_if_exists;
|
|
7
|
+
|
|
8
|
+
DELIMITER $$
|
|
9
|
+
CREATE PROCEDURE __drop_column_if_exists(IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64))
|
|
10
|
+
BEGIN
|
|
11
|
+
IF EXISTS (
|
|
12
|
+
SELECT 1
|
|
13
|
+
FROM information_schema.columns
|
|
14
|
+
WHERE table_schema = DATABASE()
|
|
15
|
+
AND table_name = p_table_name
|
|
16
|
+
AND column_name = p_column_name
|
|
17
|
+
) THEN
|
|
18
|
+
SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` DROP COLUMN `', p_column_name, '`');
|
|
19
|
+
PREPARE stmt FROM @ddl;
|
|
20
|
+
EXECUTE stmt;
|
|
21
|
+
DEALLOCATE PREPARE stmt;
|
|
22
|
+
END IF;
|
|
23
|
+
END$$
|
|
24
|
+
|
|
25
|
+
CREATE PROCEDURE __drop_index_if_exists(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64))
|
|
26
|
+
BEGIN
|
|
27
|
+
IF EXISTS (
|
|
28
|
+
SELECT 1
|
|
29
|
+
FROM information_schema.statistics
|
|
30
|
+
WHERE table_schema = DATABASE()
|
|
31
|
+
AND table_name = p_table_name
|
|
32
|
+
AND index_name = p_index_name
|
|
33
|
+
) THEN
|
|
34
|
+
SET @ddl = CONCAT('ALTER TABLE `', p_table_name, '` DROP INDEX `', p_index_name, '`');
|
|
35
|
+
PREPARE stmt FROM @ddl;
|
|
36
|
+
EXECUTE stmt;
|
|
37
|
+
DEALLOCATE PREPARE stmt;
|
|
38
|
+
END IF;
|
|
39
|
+
END$$
|
|
40
|
+
DELIMITER ;
|
|
41
|
+
|
|
42
|
+
CALL __drop_index_if_exists('groups_metadata', 'idx_groups_metadata_is_community_parent');
|
|
43
|
+
CALL __drop_index_if_exists('groups_metadata', 'idx_groups_metadata_linked_parent_jid');
|
|
44
|
+
|
|
45
|
+
CALL __drop_column_if_exists('groups_metadata', 'addressing_mode');
|
|
46
|
+
CALL __drop_column_if_exists('groups_metadata', 'join_approval_mode');
|
|
47
|
+
CALL __drop_column_if_exists('groups_metadata', 'member_add_mode');
|
|
48
|
+
CALL __drop_column_if_exists('groups_metadata', 'is_community_announce');
|
|
49
|
+
CALL __drop_column_if_exists('groups_metadata', 'is_community');
|
|
50
|
+
CALL __drop_column_if_exists('groups_metadata', 'linked_parent_jid');
|
|
51
|
+
|
|
52
|
+
UPDATE schema_change_log
|
|
53
|
+
SET status = 'rolled_back',
|
|
54
|
+
notes = 'D+35 rollback executed',
|
|
55
|
+
updated_at = CURRENT_TIMESTAMP
|
|
56
|
+
WHERE migration_key = @migration_key;
|
|
57
|
+
|
|
58
|
+
DROP PROCEDURE IF EXISTS __drop_column_if_exists;
|
|
59
|
+
DROP PROCEDURE IF EXISTS __drop_index_if_exists;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- D+35 (2026-04-11) - Group community metadata
|
|
2
|
+
-- Scope: persist parent community relations and community flags in groups_metadata
|
|
3
|
+
|
|
4
|
+
SET @migration_key := '20260411_d35_group_community_metadata';
|
|
5
|
+
|
|
6
|
+
DROP PROCEDURE IF EXISTS __ensure_column;
|
|
7
|
+
DROP PROCEDURE IF EXISTS __ensure_index;
|
|
8
|
+
|
|
9
|
+
DELIMITER $$
|
|
10
|
+
CREATE PROCEDURE __ensure_column(IN p_table_name VARCHAR(64), IN p_column_name VARCHAR(64), IN p_ddl TEXT)
|
|
11
|
+
BEGIN
|
|
12
|
+
IF NOT EXISTS (
|
|
13
|
+
SELECT 1
|
|
14
|
+
FROM information_schema.columns
|
|
15
|
+
WHERE table_schema = DATABASE()
|
|
16
|
+
AND table_name = p_table_name
|
|
17
|
+
AND column_name = p_column_name
|
|
18
|
+
) THEN
|
|
19
|
+
SET @ddl = p_ddl;
|
|
20
|
+
PREPARE stmt FROM @ddl;
|
|
21
|
+
EXECUTE stmt;
|
|
22
|
+
DEALLOCATE PREPARE stmt;
|
|
23
|
+
END IF;
|
|
24
|
+
END$$
|
|
25
|
+
|
|
26
|
+
CREATE PROCEDURE __ensure_index(IN p_table_name VARCHAR(64), IN p_index_name VARCHAR(64), IN p_ddl TEXT)
|
|
27
|
+
BEGIN
|
|
28
|
+
IF NOT EXISTS (
|
|
29
|
+
SELECT 1
|
|
30
|
+
FROM information_schema.statistics
|
|
31
|
+
WHERE table_schema = DATABASE()
|
|
32
|
+
AND table_name = p_table_name
|
|
33
|
+
AND index_name = p_index_name
|
|
34
|
+
) THEN
|
|
35
|
+
SET @ddl = p_ddl;
|
|
36
|
+
PREPARE stmt FROM @ddl;
|
|
37
|
+
EXECUTE stmt;
|
|
38
|
+
DEALLOCATE PREPARE stmt;
|
|
39
|
+
END IF;
|
|
40
|
+
END$$
|
|
41
|
+
DELIMITER ;
|
|
42
|
+
|
|
43
|
+
CALL __ensure_column('groups_metadata', 'linked_parent_jid', 'ALTER TABLE groups_metadata ADD COLUMN linked_parent_jid VARCHAR(255) NULL AFTER creation');
|
|
44
|
+
CALL __ensure_column('groups_metadata', 'is_community', 'ALTER TABLE groups_metadata ADD COLUMN is_community TINYINT(1) NULL AFTER linked_parent_jid');
|
|
45
|
+
CALL __ensure_column('groups_metadata', 'is_community_announce', 'ALTER TABLE groups_metadata ADD COLUMN is_community_announce TINYINT(1) NULL AFTER is_community');
|
|
46
|
+
CALL __ensure_column('groups_metadata', 'member_add_mode', 'ALTER TABLE groups_metadata ADD COLUMN member_add_mode TINYINT(1) NULL AFTER is_community_announce');
|
|
47
|
+
CALL __ensure_column('groups_metadata', 'join_approval_mode', 'ALTER TABLE groups_metadata ADD COLUMN join_approval_mode TINYINT(1) NULL AFTER member_add_mode');
|
|
48
|
+
CALL __ensure_column('groups_metadata', 'addressing_mode', 'ALTER TABLE groups_metadata ADD COLUMN addressing_mode VARCHAR(8) NULL AFTER join_approval_mode');
|
|
49
|
+
|
|
50
|
+
CALL __ensure_index('groups_metadata', 'idx_groups_metadata_linked_parent_jid', 'CREATE INDEX idx_groups_metadata_linked_parent_jid ON groups_metadata (linked_parent_jid)');
|
|
51
|
+
CALL __ensure_index('groups_metadata', 'idx_groups_metadata_is_community_parent', 'CREATE INDEX idx_groups_metadata_is_community_parent ON groups_metadata (is_community, linked_parent_jid)');
|
|
52
|
+
|
|
53
|
+
INSERT INTO schema_change_log (migration_key, phase, status, notes)
|
|
54
|
+
VALUES (@migration_key, 'D+35', 'applied', 'groups_metadata community columns and indexes')
|
|
55
|
+
ON DUPLICATE KEY UPDATE
|
|
56
|
+
phase = VALUES(phase),
|
|
57
|
+
status = 'applied',
|
|
58
|
+
notes = VALUES(notes),
|
|
59
|
+
updated_at = CURRENT_TIMESTAMP;
|
|
60
|
+
|
|
61
|
+
DROP PROCEDURE IF EXISTS __ensure_column;
|
|
62
|
+
DROP PROCEDURE IF EXISTS __ensure_index;
|