@omnizap-system/omnizap 2.6.1 → 2.6.2

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.
Files changed (156) hide show
  1. package/.env.example +54 -9
  2. package/.github/workflows/ci.yml +3 -3
  3. package/.github/workflows/security-runner-hardening.yml +1 -1
  4. package/.github/workflows/security-zap-full-scan.yml +1 -0
  5. package/app/config/index.js +2 -0
  6. package/app/configParts/adminIdentity.js +5 -5
  7. package/app/configParts/baileysConfig.js +226 -55
  8. package/app/configParts/groupUtils.js +5 -0
  9. package/app/configParts/messagePersistenceService.js +143 -3
  10. package/app/configParts/sessionConfig.js +157 -0
  11. package/app/connection/baileysCompatibility.test.js +1 -1
  12. package/app/connection/groupOwnerWriteStateResolver.js +109 -0
  13. package/app/connection/socketController.js +625 -124
  14. package/app/connection/socketController.multiSession.test.js +108 -0
  15. package/app/controllers/messageController.js +1 -1
  16. package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
  17. package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
  18. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
  19. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
  20. package/app/controllers/messageProcessingPipeline.js +88 -9
  21. package/app/controllers/messageProcessingPipeline.test.js +200 -0
  22. package/app/modules/adminModule/AGENT.md +1 -1
  23. package/app/modules/adminModule/commandConfig.json +3318 -1347
  24. package/app/modules/adminModule/groupCommandHandlers.js +856 -14
  25. package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
  26. package/app/modules/adminModule/groupWarningRepository.js +152 -0
  27. package/app/modules/aiModule/AGENT.md +47 -30
  28. package/app/modules/aiModule/aiConfigRuntime.js +1 -0
  29. package/app/modules/aiModule/catCommand.js +132 -25
  30. package/app/modules/aiModule/commandConfig.json +114 -28
  31. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
  32. package/app/modules/gameModule/AGENT.md +1 -1
  33. package/app/modules/gameModule/commandConfig.json +29 -0
  34. package/app/modules/menuModule/AGENT.md +1 -1
  35. package/app/modules/menuModule/commandConfig.json +45 -10
  36. package/app/modules/menuModule/menuCatalogService.js +190 -0
  37. package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
  38. package/app/modules/menuModule/menuDynamicService.js +511 -0
  39. package/app/modules/menuModule/menuDynamicService.test.js +141 -0
  40. package/app/modules/menuModule/menus.js +36 -5
  41. package/app/modules/playModule/AGENT.md +10 -5
  42. package/app/modules/playModule/commandConfig.json +74 -16
  43. package/app/modules/playModule/playCommandConstants.js +13 -7
  44. package/app/modules/playModule/playCommandCore.js +4 -6
  45. package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
  46. package/app/modules/playModule/playConfigRuntime.js +5 -6
  47. package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
  48. package/app/modules/quoteModule/AGENT.md +1 -1
  49. package/app/modules/quoteModule/commandConfig.json +29 -0
  50. package/app/modules/rpgPokemonModule/AGENT.md +1 -1
  51. package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
  52. package/app/modules/statsModule/AGENT.md +1 -1
  53. package/app/modules/statsModule/commandConfig.json +58 -0
  54. package/app/modules/stickerModule/AGENT.md +1 -1
  55. package/app/modules/stickerModule/commandConfig.json +145 -0
  56. package/app/modules/stickerPackModule/AGENT.md +1 -1
  57. package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
  58. package/app/modules/stickerPackModule/commandConfig.json +29 -0
  59. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +1 -1
  60. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
  61. package/app/modules/stickerPackModule/stickerPackService.js +13 -6
  62. package/app/modules/systemMetricsModule/AGENT.md +1 -1
  63. package/app/modules/systemMetricsModule/commandConfig.json +29 -0
  64. package/app/modules/tiktokModule/AGENT.md +1 -1
  65. package/app/modules/tiktokModule/commandConfig.json +29 -0
  66. package/app/modules/userModule/AGENT.md +1 -1
  67. package/app/modules/userModule/commandConfig.json +29 -0
  68. package/app/modules/waifuPicsModule/AGENT.md +57 -27
  69. package/app/modules/waifuPicsModule/commandConfig.json +87 -0
  70. package/app/observability/metrics.js +136 -0
  71. package/app/services/ai/commandConfigEnrichmentService.js +229 -47
  72. package/app/services/ai/geminiService.js +131 -7
  73. package/app/services/ai/geminiService.test.js +59 -2
  74. package/app/services/ai/moduleAiHelpCoreService.js +33 -4
  75. package/app/services/group/groupMetadataService.js +24 -1
  76. package/app/services/infra/dbWriteQueue.js +51 -21
  77. package/app/services/messaging/newsBroadcastService.js +843 -27
  78. package/app/services/multiSession/assignmentBalancerService.js +457 -0
  79. package/app/services/multiSession/groupOwnershipRepository.js +381 -0
  80. package/app/services/multiSession/groupOwnershipService.js +890 -0
  81. package/app/services/multiSession/groupOwnershipService.test.js +309 -0
  82. package/app/services/multiSession/sessionRegistryService.js +293 -0
  83. package/app/store/aiPromptStore.js +36 -19
  84. package/app/store/groupConfigStore.js +41 -5
  85. package/app/store/premiumUserStore.js +21 -7
  86. package/app/utils/antiLink/antiLinkModule.js +352 -16
  87. package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
  88. package/database/index.js +6 -0
  89. package/database/migrations/20260307_d0_hardening_down.sql +1 -1
  90. package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
  91. package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
  92. package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
  93. package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
  94. package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
  95. package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
  96. package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
  97. package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
  98. package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
  99. package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
  100. package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
  101. package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
  102. package/database/schema.sql +102 -1
  103. package/docker-compose.yml +4 -1
  104. package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
  105. package/docs/compliance/privacy-policy-2026-03-07.md +2 -2
  106. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
  107. package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
  108. package/docs/security/omnizap-static-security-headers.conf +25 -0
  109. package/ecosystem.prod.config.cjs +31 -11
  110. package/index.js +52 -18
  111. package/observability/alert-rules.yml +20 -0
  112. package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
  113. package/observability/mysql-setup.sql +4 -4
  114. package/observability/system-admin-observability.md +26 -0
  115. package/package.json +12 -5
  116. package/public/comandos/commands-catalog.json +2253 -78
  117. package/public/js/apps/commandsReactApp.js +267 -87
  118. package/public/js/apps/createPackApp.js +3 -3
  119. package/public/js/apps/stickersApp.js +255 -103
  120. package/public/js/apps/termsReactApp.js +57 -8
  121. package/public/js/apps/userPasswordResetReactApp.js +406 -0
  122. package/public/js/apps/userReactApp.js +96 -47
  123. package/public/js/apps/userSystemAdmReactApp.js +1506 -0
  124. package/public/pages/politica-de-privacidade.html +1 -1
  125. package/public/pages/stickers.html +5 -5
  126. package/public/pages/termos-de-uso-texto-integral.html +1 -1
  127. package/public/pages/termos-de-uso.html +1 -1
  128. package/public/pages/user-password-reset.html +3 -4
  129. package/public/pages/user-systemadm.html +8 -462
  130. package/public/pages/user.html +1 -1
  131. package/scripts/clear-whatsapp-session.sh +123 -0
  132. package/scripts/core-ai-mode.mjs +163 -0
  133. package/scripts/deploy.sh +10 -0
  134. package/scripts/enrich-command-config-ux-openai.mjs +492 -0
  135. package/scripts/generate-commands-catalog.mjs +155 -0
  136. package/scripts/new-whatsapp-session.sh +317 -0
  137. package/scripts/security-web-surface-check.mjs +218 -0
  138. package/server/controllers/admin/adminPanelHandlers.js +253 -3
  139. package/server/controllers/admin/systemAdminController.js +267 -0
  140. package/server/controllers/sticker/stickerCatalogController.js +9 -23
  141. package/server/controllers/system/contactController.js +9 -17
  142. package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
  143. package/server/controllers/system/systemController.js +254 -1
  144. package/server/controllers/userController.js +6 -0
  145. package/server/email/emailTemplateService.js +3 -2
  146. package/server/http/httpServer.js +8 -4
  147. package/server/middleware/securityHeaders.js +20 -1
  148. package/server/routes/admin/systemAdminRouter.js +6 -0
  149. package/server/routes/indexRouter.js +30 -6
  150. package/server/routes/observability/grafanaProxyRouter.js +254 -0
  151. package/server/routes/static/staticPageRouter.js +27 -1
  152. package/server/utils/publicContact.js +31 -0
  153. package/utils/whatsapp/contactEnv.js +39 -0
  154. package/vite.config.mjs +2 -1
  155. package/app/modules/playModule/local/installYtDlp.js +0 -25
  156. package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+ import 'dotenv/config';
3
+
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ import OpenAI from 'openai';
9
+ import prettier from 'prettier';
10
+ import { z } from 'zod';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const repoRoot = path.resolve(__dirname, '..');
15
+ const modulesRoot = path.join(repoRoot, 'app', 'modules');
16
+
17
+ const DEFAULT_MODEL = String(process.env.COMMAND_CONFIG_UX_ENRICH_MODEL || process.env.OPENAI_MODEL || 'gpt-4o-mini').trim() || 'gpt-4o-mini';
18
+ const DEFAULT_DELAY_MS = Math.max(0, Number.parseInt(String(process.env.COMMAND_CONFIG_UX_ENRICH_DELAY_MS || '120'), 10) || 120);
19
+ const MAX_ATTEMPTS = Math.max(1, Number.parseInt(String(process.env.COMMAND_CONFIG_UX_ENRICH_MAX_ATTEMPTS || '3'), 10) || 3);
20
+
21
+ const UX_FIELDS = ['resumo_usuario', 'quando_usar', 'exemplos_reais', 'resposta_esperada', 'erros_comuns_usuario', 'passos_se_der_erro'];
22
+
23
+ const OUTPUT_SCHEMA = z
24
+ .object({
25
+ resumo_usuario: z.string(),
26
+ quando_usar: z.array(z.string()),
27
+ exemplos_reais: z.array(
28
+ z.object({
29
+ situacao: z.string(),
30
+ comando: z.string(),
31
+ resposta_esperada: z.string(),
32
+ variacao: z.string().optional(),
33
+ }),
34
+ ),
35
+ resposta_esperada: z.array(z.string()),
36
+ erros_comuns_usuario: z.array(z.string()),
37
+ passos_se_der_erro: z.array(z.string()),
38
+ })
39
+ .strict();
40
+
41
+ const SYSTEM_PROMPT = ['Voce escreve textos de ajuda para usuario final de um bot WhatsApp.', 'Responda SOMENTE JSON valido com as chaves exatas:', '{"resumo_usuario":"","quando_usar":[],"exemplos_reais":[{"situacao":"","comando":"","resposta_esperada":"","variacao":""}],"resposta_esperada":[],"erros_comuns_usuario":[],"passos_se_der_erro":[]}.', 'Regras:', '- pt-BR simples, objetivo e pratico.', '- Nao use linguagem tecnica de desenvolvimento.', '- Mostre acao concreta do usuario (o que fazer agora).', '- Comandos em exemplos devem manter "<prefix>" quando aplicavel.', '- Inclua restricao Premium quando existir.', '- Evite promessas absolutas e evite texto repetido.'].join(' ');
42
+
43
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
44
+
45
+ const isObject = (value) => Boolean(value && typeof value === 'object' && !Array.isArray(value));
46
+
47
+ const ensureArray = (value) => (Array.isArray(value) ? value : []);
48
+
49
+ const normalizeText = (value) =>
50
+ String(value || '')
51
+ .trim()
52
+ .replace(/\s+/g, ' ');
53
+
54
+ const ensureSentence = (value) => {
55
+ const text = normalizeText(value);
56
+ if (!text) return '';
57
+ if (/[.!?]$/.test(text)) return text;
58
+ return `${text}.`;
59
+ };
60
+
61
+ const uniqueStrings = (values, { max = 8, minLength = 2, maxLength = 220 } = {}) => {
62
+ const out = [];
63
+ const seen = new Set();
64
+ for (const value of ensureArray(values)) {
65
+ const normalized = normalizeText(value).slice(0, maxLength);
66
+ if (!normalized || normalized.length < minLength) continue;
67
+ const key = normalized.toLowerCase();
68
+ if (seen.has(key)) continue;
69
+ seen.add(key);
70
+ out.push(normalized);
71
+ if (out.length >= max) break;
72
+ }
73
+ return out;
74
+ };
75
+
76
+ const parseArgs = (argv) => {
77
+ const options = {
78
+ moduleFilter: '',
79
+ commandFilter: '',
80
+ limit: Number.POSITIVE_INFINITY,
81
+ overwrite: false,
82
+ dryRun: false,
83
+ model: DEFAULT_MODEL,
84
+ delayMs: DEFAULT_DELAY_MS,
85
+ };
86
+
87
+ for (const arg of argv) {
88
+ if (!arg) continue;
89
+ if (arg === '--overwrite') {
90
+ options.overwrite = true;
91
+ continue;
92
+ }
93
+ if (arg === '--dry-run') {
94
+ options.dryRun = true;
95
+ continue;
96
+ }
97
+ if (arg.startsWith('--module=')) {
98
+ options.moduleFilter = normalizeText(arg.slice('--module='.length)).toLowerCase();
99
+ continue;
100
+ }
101
+ if (arg.startsWith('--command=')) {
102
+ options.commandFilter = normalizeText(arg.slice('--command='.length)).toLowerCase();
103
+ continue;
104
+ }
105
+ if (arg.startsWith('--limit=')) {
106
+ const parsed = Number.parseInt(arg.slice('--limit='.length), 10);
107
+ if (Number.isFinite(parsed) && parsed > 0) {
108
+ options.limit = parsed;
109
+ }
110
+ continue;
111
+ }
112
+ if (arg.startsWith('--model=')) {
113
+ const model = normalizeText(arg.slice('--model='.length));
114
+ if (model) options.model = model;
115
+ continue;
116
+ }
117
+ if (arg.startsWith('--delay-ms=')) {
118
+ const parsed = Number.parseInt(arg.slice('--delay-ms='.length), 10);
119
+ if (Number.isFinite(parsed) && parsed >= 0) {
120
+ options.delayMs = parsed;
121
+ }
122
+ }
123
+ }
124
+
125
+ return options;
126
+ };
127
+
128
+ const listModuleConfigPaths = async () => {
129
+ const dirs = await fs.readdir(modulesRoot, { withFileTypes: true });
130
+ const files = [];
131
+ for (const entry of dirs) {
132
+ if (!entry.isDirectory()) continue;
133
+ const configPath = path.join(modulesRoot, entry.name, 'commandConfig.json');
134
+ try {
135
+ await fs.access(configPath);
136
+ files.push(configPath);
137
+ } catch {
138
+ // ignore modules sem commandConfig
139
+ }
140
+ }
141
+ return files.sort((a, b) => a.localeCompare(b, 'pt-BR'));
142
+ };
143
+
144
+ const commandMatchesFilter = (command, commandFilter) => {
145
+ if (!commandFilter) return true;
146
+ const name = normalizeText(command?.name).toLowerCase();
147
+ if (name.includes(commandFilter)) return true;
148
+ const aliases = ensureArray(command?.aliases).map((alias) => normalizeText(alias).toLowerCase());
149
+ return aliases.some((alias) => alias.includes(commandFilter));
150
+ };
151
+
152
+ const resolveRequirements = (command) => {
153
+ const req = isObject(command?.requirements) ? command.requirements : isObject(command?.pre_condicoes) ? command.pre_condicoes : {};
154
+ return {
155
+ group: Boolean(req.require_group ?? req.requer_grupo),
156
+ admin: Boolean(req.require_group_admin ?? req.requer_admin),
157
+ owner: Boolean(req.require_bot_owner ?? req.requer_admin_principal),
158
+ googleLogin: Boolean(req.require_google_login ?? req.requer_google_login),
159
+ nsfw: Boolean(req.require_nsfw_enabled ?? req.requer_nsfw),
160
+ media: Boolean(req.require_media ?? req.requer_midia),
161
+ reply: Boolean(req.require_reply_message ?? req.requer_mensagem_respondida),
162
+ };
163
+ };
164
+
165
+ const resolvePremium = (command) => {
166
+ const access = isObject(command?.access) ? command.access : isObject(command?.acesso) ? command.acesso : {};
167
+ return {
168
+ premium: Boolean(access.premium_only ?? access.somente_premium),
169
+ plans: uniqueStrings(access.allowed_plans || access.planos_permitidos, { max: 8, maxLength: 40 }),
170
+ };
171
+ };
172
+
173
+ const resolveUsageMethods = (command) =>
174
+ uniqueStrings(command?.metodos_de_uso || command?.usage, {
175
+ max: 6,
176
+ maxLength: 220,
177
+ });
178
+
179
+ const resolveResponses = (command) => {
180
+ const responses = isObject(command?.responses) ? command.responses : isObject(command?.respostas_padrao) ? command.respostas_padrao : {};
181
+ return {
182
+ success: normalizeText(responses.success || responses.sucesso),
183
+ usageError: normalizeText(responses.usage_error || responses.erro_uso),
184
+ permissionError: normalizeText(responses.permission_error || responses.erro_permissao),
185
+ };
186
+ };
187
+
188
+ const resolveArgs = (command) =>
189
+ ensureArray(command?.arguments || command?.argumentos)
190
+ .map((arg) => ({
191
+ name: normalizeText(arg?.name || arg?.nome),
192
+ type: normalizeText(arg?.type || arg?.tipo || 'string'),
193
+ required: Boolean(arg?.required ?? arg?.obrigatorio),
194
+ description: normalizeText(arg?.description || arg?.descricao),
195
+ }))
196
+ .filter((arg) => arg.name)
197
+ .slice(0, 6);
198
+
199
+ const normalizeExampleCommand = (value, fallback) => {
200
+ const raw = normalizeText(value);
201
+ const candidate = raw || normalizeText(fallback);
202
+ if (!candidate) return '';
203
+ if (candidate.startsWith('/')) return `<prefix>${candidate.slice(1)}`;
204
+ return candidate;
205
+ };
206
+
207
+ const buildFallbackUx = (context) => {
208
+ const commandName = context?.name || 'comando';
209
+ const commandUsage = context?.usageMethods?.[0] || `<prefix>${commandName}`;
210
+ const description = normalizeText(context?.description) || `Use <prefix>${commandName} para executar esta acao`;
211
+ const successText = normalizeText(context?.responses?.success) || 'O bot confirma que executou com sucesso';
212
+ const usageErrorText = normalizeText(context?.responses?.usageError) || 'Se o formato estiver errado, o bot mostra como corrigir';
213
+ const permissionText = normalizeText(context?.responses?.permissionError) || 'Sem permissao, o bot informa o motivo';
214
+
215
+ return {
216
+ resumo_usuario: ensureSentence(description),
217
+ quando_usar: uniqueStrings([`Quando voce precisa desta acao: ${ensureSentence(description)}`, context?.requirements?.group ? 'Funciona dentro de grupos.' : 'Pode ser usado no privado e em grupo.', context?.requirements?.admin ? 'Voce precisa ser admin para executar.' : '', context?.premium?.premium ? 'Disponivel para usuarios Premium.' : ''], { max: 5 }),
218
+ exemplos_reais: [
219
+ {
220
+ situacao: ensureSentence(`Cenario comum para usar ${commandUsage}`),
221
+ comando: normalizeExampleCommand(commandUsage, `<prefix>${commandName}`),
222
+ resposta_esperada: ensureSentence(successText),
223
+ variacao: ensureSentence(usageErrorText),
224
+ },
225
+ ],
226
+ resposta_esperada: uniqueStrings([`Sucesso: ${ensureSentence(successText)}`, `Uso incorreto: ${ensureSentence(usageErrorText)}`, `Permissao: ${ensureSentence(permissionText)}`], { max: 5 }),
227
+ erros_comuns_usuario: uniqueStrings(['Digitar o comando fora do formato esperado.', context?.requirements?.group ? 'Tentar executar fora de um grupo.' : '', context?.requirements?.admin ? 'Tentar executar sem ser admin.' : '', context?.premium?.premium ? 'Tentar usar sem plano Premium ativo.' : ''], { max: 5 }),
228
+ passos_se_der_erro: uniqueStrings(['Copie e teste um exemplo desta pagina.', 'Confira se voce esta no local correto e com permissao.', 'Se continuar com erro, fale com o admin no privado.'], { max: 5 }),
229
+ };
230
+ };
231
+
232
+ const sanitizeUxPayload = (payload, context) => {
233
+ const fallback = buildFallbackUx(context);
234
+ const usageFallback = context?.usageMethods?.[0] || `<prefix>${context?.name || 'comando'}`;
235
+
236
+ const examples = ensureArray(payload?.exemplos_reais)
237
+ .map((example) => {
238
+ if (!isObject(example)) return null;
239
+ const situacao = ensureSentence(example.situacao) || ensureSentence(fallback.exemplos_reais[0].situacao);
240
+ const comando = normalizeExampleCommand(example.comando, usageFallback);
241
+ const resposta = ensureSentence(example.resposta_esperada) || ensureSentence(fallback.exemplos_reais[0].resposta_esperada);
242
+ const variacao = ensureSentence(example.variacao || '') || ensureSentence(fallback.exemplos_reais[0].variacao);
243
+ if (!comando) return null;
244
+ return {
245
+ situacao,
246
+ comando,
247
+ resposta_esperada: resposta,
248
+ variacao,
249
+ };
250
+ })
251
+ .filter(Boolean)
252
+ .slice(0, 3);
253
+
254
+ const normalized = {
255
+ resumo_usuario: ensureSentence(payload?.resumo_usuario) || fallback.resumo_usuario,
256
+ quando_usar: uniqueStrings(payload?.quando_usar, { max: 5 }),
257
+ exemplos_reais: examples,
258
+ resposta_esperada: uniqueStrings(payload?.resposta_esperada, { max: 5 }),
259
+ erros_comuns_usuario: uniqueStrings(payload?.erros_comuns_usuario, { max: 5 }),
260
+ passos_se_der_erro: uniqueStrings(payload?.passos_se_der_erro, { max: 5 }),
261
+ };
262
+
263
+ if (!normalized.quando_usar.length) normalized.quando_usar = fallback.quando_usar;
264
+ if (!normalized.exemplos_reais.length) normalized.exemplos_reais = fallback.exemplos_reais;
265
+ if (!normalized.resposta_esperada.length) normalized.resposta_esperada = fallback.resposta_esperada;
266
+ if (!normalized.erros_comuns_usuario.length) normalized.erros_comuns_usuario = fallback.erros_comuns_usuario;
267
+ if (!normalized.passos_se_der_erro.length) normalized.passos_se_der_erro = fallback.passos_se_der_erro;
268
+
269
+ return normalized;
270
+ };
271
+
272
+ const extractCurrentUx = (command) => {
273
+ const userExperience = isObject(command?.user_experience) ? command.user_experience : {};
274
+ return {
275
+ resumo_usuario: userExperience.resumo_usuario ?? command?.resumo_usuario ?? '',
276
+ quando_usar: userExperience.quando_usar ?? command?.quando_usar ?? [],
277
+ exemplos_reais: userExperience.exemplos_reais ?? command?.exemplos_reais ?? [],
278
+ resposta_esperada: userExperience.resposta_esperada ?? command?.resposta_esperada ?? [],
279
+ erros_comuns_usuario: userExperience.erros_comuns_usuario ?? command?.erros_comuns_usuario ?? [],
280
+ passos_se_der_erro: userExperience.passos_se_der_erro ?? command?.passos_se_der_erro ?? [],
281
+ };
282
+ };
283
+
284
+ const hasCompleteUx = (command) => {
285
+ const ux = extractCurrentUx(command);
286
+ return normalizeText(ux.resumo_usuario).length >= 8 && uniqueStrings(ux.quando_usar, { max: 10 }).length > 0 && ensureArray(ux.exemplos_reais).length > 0 && uniqueStrings(ux.resposta_esperada, { max: 10 }).length > 0 && uniqueStrings(ux.erros_comuns_usuario, { max: 10 }).length > 0 && uniqueStrings(ux.passos_se_der_erro, { max: 10 }).length > 0;
287
+ };
288
+
289
+ const buildCommandContext = ({ moduleDirName, command }) => {
290
+ const name = normalizeText(command?.name);
291
+ const description = normalizeText(command?.description || command?.descricao);
292
+ const usageMethods = resolveUsageMethods(command);
293
+ const requirements = resolveRequirements(command);
294
+ const premium = resolvePremium(command);
295
+ const responses = resolveResponses(command);
296
+ const argumentsList = resolveArgs(command);
297
+ const aliases = uniqueStrings(command?.aliases, { max: 8, maxLength: 60 });
298
+ const category = normalizeText(command?.categoria || command?.category);
299
+ const currentUx = extractCurrentUx(command);
300
+
301
+ return {
302
+ module: moduleDirName,
303
+ name,
304
+ aliases,
305
+ category,
306
+ description,
307
+ usageMethods,
308
+ requirements,
309
+ premium,
310
+ responses,
311
+ arguments: argumentsList,
312
+ currentUx,
313
+ };
314
+ };
315
+
316
+ const extractJsonContent = (completion) => {
317
+ const content = completion?.choices?.[0]?.message?.content;
318
+ if (typeof content === 'string') return content;
319
+ if (Array.isArray(content)) {
320
+ return content
321
+ .map((item) => {
322
+ if (!item) return '';
323
+ if (typeof item === 'string') return item;
324
+ if (typeof item?.text === 'string') return item.text;
325
+ if (typeof item?.text?.value === 'string') return item.text.value;
326
+ return '';
327
+ })
328
+ .filter(Boolean)
329
+ .join('\n')
330
+ .trim();
331
+ }
332
+ return '';
333
+ };
334
+
335
+ const generateUxWithOpenAI = async ({ client, model, context }) => {
336
+ const userPayload = {
337
+ objective: 'Gerar conteudo de pagina de comando focado em usuario final',
338
+ output_keys: UX_FIELDS,
339
+ command_context: context,
340
+ };
341
+
342
+ let lastError = null;
343
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt += 1) {
344
+ try {
345
+ const requestPayload = {
346
+ model,
347
+ response_format: { type: 'json_object' },
348
+ messages: [
349
+ { role: 'system', content: SYSTEM_PROMPT },
350
+ { role: 'user', content: JSON.stringify(userPayload, null, 2) },
351
+ ],
352
+ };
353
+ if (
354
+ !String(model || '')
355
+ .trim()
356
+ .toLowerCase()
357
+ .startsWith('gpt-5')
358
+ ) {
359
+ requestPayload.temperature = 0.2;
360
+ }
361
+
362
+ const completion = await client.chat.completions.create(requestPayload);
363
+
364
+ const content = extractJsonContent(completion);
365
+ const parsed = JSON.parse(content);
366
+ const validated = OUTPUT_SCHEMA.parse(parsed);
367
+ return sanitizeUxPayload(validated, context);
368
+ } catch (error) {
369
+ lastError = error;
370
+ if (attempt < MAX_ATTEMPTS) {
371
+ await sleep(400 * attempt);
372
+ }
373
+ }
374
+ }
375
+
376
+ throw lastError || new Error('Falha ao gerar UX com OpenAI');
377
+ };
378
+
379
+ const writeFormattedJson = async (filePath, payload) => {
380
+ const serialized = `${JSON.stringify(payload, null, 2)}\n`;
381
+ const prettierConfig = (await prettier.resolveConfig(filePath)) || {};
382
+ const formatted = await prettier.format(serialized, {
383
+ ...prettierConfig,
384
+ parser: 'json',
385
+ filepath: filePath,
386
+ });
387
+ await fs.writeFile(filePath, formatted, 'utf8');
388
+ };
389
+
390
+ const main = async () => {
391
+ const options = parseArgs(process.argv.slice(2));
392
+ const apiKey = normalizeText(process.env.OPENAI_API_KEY);
393
+ if (!apiKey) {
394
+ throw new Error('OPENAI_API_KEY nao configurada. Defina no ambiente ou no arquivo .env');
395
+ }
396
+
397
+ const client = new OpenAI({
398
+ apiKey,
399
+ timeout: 30_000,
400
+ maxRetries: 1,
401
+ });
402
+
403
+ const configPaths = await listModuleConfigPaths();
404
+ const stats = {
405
+ scanned: 0,
406
+ target: 0,
407
+ updated: 0,
408
+ skippedExisting: 0,
409
+ failed: 0,
410
+ filesChanged: 0,
411
+ };
412
+
413
+ console.log(`[ux-enrich] model=${options.model} dryRun=${options.dryRun} overwrite=${options.overwrite} limit=${Number.isFinite(options.limit) ? options.limit : 'all'} delayMs=${options.delayMs}`);
414
+
415
+ for (const configPath of configPaths) {
416
+ const moduleDirName = path.basename(path.dirname(configPath));
417
+ if (options.moduleFilter && moduleDirName.toLowerCase() !== options.moduleFilter) {
418
+ continue;
419
+ }
420
+
421
+ const raw = await fs.readFile(configPath, 'utf8');
422
+ const parsed = JSON.parse(raw);
423
+ const commands = ensureArray(parsed?.commands);
424
+
425
+ let fileChanged = false;
426
+
427
+ for (const command of commands) {
428
+ if (stats.target >= options.limit) break;
429
+ if (!isObject(command)) continue;
430
+ if (command.enabled === false) continue;
431
+ if (!commandMatchesFilter(command, options.commandFilter)) continue;
432
+
433
+ stats.scanned += 1;
434
+
435
+ if (!options.overwrite && hasCompleteUx(command)) {
436
+ stats.skippedExisting += 1;
437
+ continue;
438
+ }
439
+
440
+ const commandName = normalizeText(command.name);
441
+ if (!commandName) continue;
442
+
443
+ stats.target += 1;
444
+ const context = buildCommandContext({ moduleDirName, command });
445
+
446
+ try {
447
+ const ux = await generateUxWithOpenAI({
448
+ client,
449
+ model: options.model,
450
+ context,
451
+ });
452
+
453
+ const previousUx = isObject(command.user_experience) ? command.user_experience : {};
454
+ command.user_experience = {
455
+ ...previousUx,
456
+ ...ux,
457
+ resumo_usuario_origem: 'auto_ia_assistida',
458
+ resumo_usuario_revisao_pendente: true,
459
+ };
460
+
461
+ stats.updated += 1;
462
+ fileChanged = true;
463
+ console.log(`[ux-enrich] ok ${moduleDirName}/${commandName}`);
464
+ } catch (error) {
465
+ stats.failed += 1;
466
+ console.warn(`[ux-enrich] fail ${moduleDirName}/${commandName}: ${error?.message || 'erro desconhecido'}`);
467
+ }
468
+
469
+ if (options.delayMs > 0) {
470
+ await sleep(options.delayMs);
471
+ }
472
+ }
473
+
474
+ if (fileChanged) {
475
+ stats.filesChanged += 1;
476
+ if (!options.dryRun) {
477
+ await writeFormattedJson(configPath, parsed);
478
+ }
479
+ console.log(`[ux-enrich] file ${options.dryRun ? 'would-update' : 'updated'} ${path.relative(repoRoot, configPath)}`);
480
+ }
481
+
482
+ if (stats.target >= options.limit) break;
483
+ }
484
+
485
+ console.log('[ux-enrich] done');
486
+ console.log([`scanned=${stats.scanned}`, `target=${stats.target}`, `updated=${stats.updated}`, `skipped_existing=${stats.skippedExisting}`, `failed=${stats.failed}`, `files_changed=${stats.filesChanged}`].join(' '));
487
+ };
488
+
489
+ main().catch((error) => {
490
+ console.error(`[ux-enrich] fatal: ${error?.message || error}`);
491
+ process.exitCode = 1;
492
+ });
@@ -60,6 +60,143 @@ const resolveCategoryMeta = (key) => {
60
60
  return { label, icon: '🧩' };
61
61
  };
62
62
 
63
+ const pickFirstText = (...values) => {
64
+ for (const value of values) {
65
+ const text = String(value || '').trim();
66
+ if (text) return text;
67
+ }
68
+ return '';
69
+ };
70
+
71
+ const ensureSentence = (value) => {
72
+ const text = String(value || '')
73
+ .trim()
74
+ .replace(/\s+/g, ' ');
75
+ if (!text) return '';
76
+ if (/[.!?]$/.test(text)) return text;
77
+ return `${text}.`;
78
+ };
79
+
80
+ const parseOptionalBoolean = (value) => {
81
+ if (value === true) return true;
82
+ if (value === false) return false;
83
+ const normalized = String(value || '')
84
+ .trim()
85
+ .toLowerCase();
86
+ if (!normalized) return null;
87
+ if (['1', 'true', 'yes', 'sim'].includes(normalized)) return true;
88
+ if (['0', 'false', 'no', 'nao', 'não'].includes(normalized)) return false;
89
+ return null;
90
+ };
91
+
92
+ const normalizeUserList = (value) => unique(ensureArray(value).map((item) => String(item || '').trim()));
93
+
94
+ const normalizeExampleCommand = (value, fallback = '') => {
95
+ const raw = String(value || '').trim();
96
+ if (!raw) return String(fallback || '').trim();
97
+ return raw.replaceAll('<prefix>', '/');
98
+ };
99
+
100
+ const normalizeUserExample = (value, { fallbackSituation = '', fallbackCommand = '', fallbackExpected = '', fallbackVariation = '' } = {}) => {
101
+ if (!value) return null;
102
+
103
+ if (typeof value === 'string') {
104
+ const commandText = normalizeExampleCommand(value, fallbackCommand);
105
+ if (!commandText) return null;
106
+ return {
107
+ situacao: ensureSentence(fallbackSituation || 'Exemplo real de uso do comando'),
108
+ comando: commandText,
109
+ resposta_esperada: ensureSentence(fallbackExpected || 'O bot confirma a execução'),
110
+ variacao: ensureSentence(fallbackVariation || ''),
111
+ };
112
+ }
113
+
114
+ if (typeof value === 'object' && !Array.isArray(value)) {
115
+ const situacao = pickFirstText(value.situacao, value.cenario, value.contexto, fallbackSituation);
116
+ const comando = normalizeExampleCommand(pickFirstText(value.comando, value.command, value.uso), fallbackCommand);
117
+ const respostaEsperada = pickFirstText(value.resposta_esperada, value.expected_response, value.resposta, fallbackExpected);
118
+ const variacao = pickFirstText(value.variacao, value.outcome_variation, fallbackVariation);
119
+
120
+ if (!comando) return null;
121
+
122
+ return {
123
+ situacao: ensureSentence(situacao || 'Exemplo real de uso do comando'),
124
+ comando,
125
+ resposta_esperada: ensureSentence(respostaEsperada || 'O bot confirma a execução'),
126
+ variacao: ensureSentence(variacao || ''),
127
+ };
128
+ }
129
+
130
+ return null;
131
+ };
132
+
133
+ const buildUserExperienceContract = ({ commandName = '', description = '', docsSummary = '', usageMethods = [], responses = {}, requirements = {}, premium = false, rawUserExperience = {} } = {}) => {
134
+ const explicitSummary = pickFirstText(rawUserExperience.resumo_usuario, rawUserExperience.summary, rawUserExperience.resumo_ia);
135
+ const explicitSummaryOrigin = pickFirstText(rawUserExperience.resumo_usuario_origem, rawUserExperience.summary_origin);
136
+ const normalizedSummaryOrigin = explicitSummaryOrigin === 'manual' || explicitSummaryOrigin === 'auto_ia_assistida' ? explicitSummaryOrigin : '';
137
+ const summaryBase = pickFirstText(explicitSummary, docsSummary, description, `Use /${commandName} para executar esta ação no bot`);
138
+ const resumoUsuario = ensureSentence(summaryBase);
139
+ const summaryOrigin = normalizedSummaryOrigin || (explicitSummary ? 'manual' : 'auto_ia_assistida');
140
+ const explicitReviewPending = parseOptionalBoolean(rawUserExperience.resumo_usuario_revisao_pendente);
141
+
142
+ const explicitWhenToUse = normalizeUserList(rawUserExperience.quando_usar || rawUserExperience.when_to_use);
143
+ const descriptionNoPunctuation = String(description || '')
144
+ .trim()
145
+ .replace(/[.!?]+$/, '');
146
+ const descriptionSentence = ensureSentence(descriptionNoPunctuation);
147
+ const derivedWhenToUse = unique([descriptionSentence ? `Use quando você precisa desta ação: ${descriptionSentence}` : '', requirements.group ? 'Funciona dentro de grupos.' : 'Pode ser usado no privado e em grupo.', requirements.admin ? 'Você precisa ser admin para executar.' : '', requirements.owner ? 'Esse comando é restrito ao admin principal do sistema.' : '', requirements.google_login ? 'É necessário estar logado no sistema para usar.' : '', premium ? 'Disponível para usuários Premium.' : ''].filter(Boolean));
148
+ const quandoUsar = explicitWhenToUse.length ? explicitWhenToUse : derivedWhenToUse.slice(0, 5);
149
+
150
+ const successResponse = ensureSentence(pickFirstText(rawUserExperience.resposta_sucesso, responses.success, responses.sucesso, 'O bot confirma que executou o comando'));
151
+ const usageErrorResponse = ensureSentence(pickFirstText(rawUserExperience.resposta_uso_incorreto, responses.usage_error, responses.erro_uso, 'Se o formato estiver incorreto, o bot mostra como usar corretamente'));
152
+ const permissionResponse = ensureSentence(pickFirstText(rawUserExperience.resposta_sem_permissao, responses.permission_error, responses.erro_permissao, premium ? 'Sem plano Premium ativo, o bot informa a restrição de acesso' : 'Sem permissão suficiente, o bot informa o motivo'));
153
+
154
+ const explicitExpectedResponses = normalizeUserList(rawUserExperience.resposta_esperada || rawUserExperience.expected_response);
155
+ const respostaEsperada = explicitExpectedResponses.length ? explicitExpectedResponses : unique([`Sucesso: ${successResponse}`, `Uso incorreto: ${usageErrorResponse}`, `Permissão: ${permissionResponse}`]);
156
+
157
+ const explicitExamples = ensureArray(rawUserExperience.exemplos_reais || rawUserExperience.real_examples);
158
+ const defaultSituation = descriptionSentence ? `Cenário comum: ${descriptionSentence}` : `Cenário comum: você quer usar /${commandName} no dia a dia.`;
159
+ const fallbackUsageMethods = usageMethods.length ? usageMethods : [`/${commandName}`];
160
+ let exemplosReais = explicitExamples
161
+ .map((example) =>
162
+ normalizeUserExample(example, {
163
+ fallbackSituation: defaultSituation,
164
+ fallbackCommand: fallbackUsageMethods[0] || `/${commandName}`,
165
+ fallbackExpected: successResponse,
166
+ fallbackVariation: usageErrorResponse,
167
+ }),
168
+ )
169
+ .filter(Boolean);
170
+
171
+ if (!exemplosReais.length) {
172
+ exemplosReais = fallbackUsageMethods.slice(0, 3).map((usageMethod, index) => ({
173
+ situacao: ensureSentence(index === 0 ? defaultSituation : `Variação ${index + 1} de uso do comando no mesmo contexto`),
174
+ comando: normalizeExampleCommand(usageMethod, `/${commandName}`),
175
+ resposta_esperada: successResponse,
176
+ variacao: usageErrorResponse,
177
+ }));
178
+ }
179
+
180
+ const explicitCommonErrors = normalizeUserList(rawUserExperience.erros_comuns_usuario || rawUserExperience.common_user_errors);
181
+ const derivedCommonErrors = unique(['Digitar o comando fora do formato esperado.', requirements.group ? 'Tentar executar fora de um grupo.' : '', requirements.admin ? 'Tentar executar sem ser admin.' : '', requirements.owner ? 'Tentar executar sem ser admin principal do sistema.' : '', requirements.google_login ? 'Tentar usar sem estar logado no sistema.' : '', premium ? 'Tentar usar sem acesso Premium ativo.' : ''].filter(Boolean));
182
+ const errosComunsUsuario = explicitCommonErrors.length ? explicitCommonErrors : derivedCommonErrors.slice(0, 5);
183
+
184
+ const explicitErrorSteps = normalizeUserList(rawUserExperience.passos_se_der_erro || rawUserExperience.error_steps);
185
+ const fallbackErrorSteps = ['Copie e teste um exemplo pronto desta página.', 'Confira se você está no local certo (grupo/privado) e com a permissão necessária.', premium ? 'Verifique se seu acesso Premium está ativo.' : '', 'Se ainda falhar, fale com o admin do sistema no privado.'].filter(Boolean);
186
+ const passosSeDerErro = explicitErrorSteps.length ? explicitErrorSteps : fallbackErrorSteps;
187
+
188
+ return {
189
+ resumo_usuario: resumoUsuario,
190
+ quando_usar: quandoUsar,
191
+ exemplos_reais: exemplosReais,
192
+ resposta_esperada: respostaEsperada,
193
+ erros_comuns_usuario: errosComunsUsuario,
194
+ passos_se_der_erro: passosSeDerErro,
195
+ resumo_usuario_origem: summaryOrigin,
196
+ resumo_usuario_revisao_pendente: explicitReviewPending ?? summaryOrigin !== 'manual',
197
+ };
198
+ };
199
+
63
200
  const deepMerge = (target, source) => {
64
201
  if (!source) return target;
65
202
  const output = { ...target };
@@ -162,6 +299,23 @@ const sanitizeCommand = ({ command: rawCommand, moduleDefaults, moduleDirName, m
162
299
  const sideEffects = unique([...ensureArray(command?.efeitos_colaterais), ...ensureArray(command?.side_effects)]);
163
300
 
164
301
  const responses = deepMerge(moduleDefaults?.responses || moduleDefaults?.respostas_padrao || {}, command?.responses || command?.respostas_padrao || {});
302
+ const userExperienceSeed = deepMerge(moduleDefaults?.user_experience || moduleDefaults?.experiencia_usuario || {}, command?.user_experience || command?.experiencia_usuario || {});
303
+ if (command?.resumo_usuario !== undefined) userExperienceSeed.resumo_usuario = command.resumo_usuario;
304
+ if (command?.quando_usar !== undefined) userExperienceSeed.quando_usar = command.quando_usar;
305
+ if (command?.exemplos_reais !== undefined) userExperienceSeed.exemplos_reais = command.exemplos_reais;
306
+ if (command?.resposta_esperada !== undefined) userExperienceSeed.resposta_esperada = command.resposta_esperada;
307
+ if (command?.erros_comuns_usuario !== undefined) userExperienceSeed.erros_comuns_usuario = command.erros_comuns_usuario;
308
+ if (command?.passos_se_der_erro !== undefined) userExperienceSeed.passos_se_der_erro = command.passos_se_der_erro;
309
+ const userExperience = buildUserExperienceContract({
310
+ commandName,
311
+ description: String(command?.description || command?.descricao || '').trim(),
312
+ docsSummary: String(command?.docs?.summary || '').trim(),
313
+ usageMethods,
314
+ responses,
315
+ requirements,
316
+ premium,
317
+ rawUserExperience: userExperienceSeed,
318
+ });
165
319
 
166
320
  const observability = deepMerge(moduleDefaults?.observability || {}, command?.observability || {});
167
321
  const privacy = deepMerge(moduleDefaults?.privacy || {}, command?.privacy || {});
@@ -185,6 +339,7 @@ const sanitizeCommand = ({ command: rawCommand, moduleDefaults, moduleDirName, m
185
339
  subcomandos: unique(ensureArray(command?.subcomandos).map((item) => String(item).trim())),
186
340
  metodos_de_uso: usageMethods,
187
341
  mensagens_uso: normalizedUsageVariants,
342
+ ...userExperience,
188
343
  arguments: args,
189
344
  responses,
190
345
  technical: {