@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
@@ -7,7 +7,7 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
7
7
  - arquivo_base: `app/modules/aiModule/commandConfig.json`
8
8
  - schema_version: `2.0.0`
9
9
  - module_enabled: `true`
10
- - generated_at: `2026-03-11T02:35:17.177Z`
10
+ - generated_at: `2026-03-17T04:04:14.195Z`
11
11
 
12
12
  ## Escopo do Modulo
13
13
 
@@ -77,7 +77,7 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
77
77
  - enabled: true
78
78
  - categoria: ia
79
79
  - descricao: Perguntas para IA com suporte opcional a resposta em audio.
80
- - permissao_necessaria: usuario comum
80
+ - permissao_necessaria: usuario premium
81
81
  - version: 1.0.0
82
82
  - stability: stable
83
83
  - deprecated: nao
@@ -116,8 +116,8 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
116
116
  - janela_ms: null
117
117
  - escopo: sem_rate_limit_explicito
118
118
  - acesso:
119
- - somente_premium: nao
120
- - planos_permitidos: comum, premium
119
+ - somente_premium: sim
120
+ - planos_permitidos: premium
121
121
  - limite_uso_por_plano:
122
122
  - comum: max=8, janela_ms=300000, escopo=usuario
123
123
  - premium: max=40, janela_ms=300000, escopo=usuario
@@ -142,22 +142,29 @@ Este arquivo e destinado a agentes de IA para gerar respostas no contexto dos co
142
142
  - erro_uso: Formato de uso inválido. Consulte metodos_de_uso.
143
143
  - erro_permissao: Permissão insuficiente para executar este comando.
144
144
  - mensagens_sistema:
145
- - premium*only: ⭐ \_Comando Premium*
145
+ - premium*only: ⭐ \_Recurso Premium*
146
146
 
147
- Este comando é exclusivo para usuários premium.
148
- Fale com o administrador para liberar o acesso.
147
+ Este comando é exclusivo para usuários Premium.
148
+ Para liberar o acesso, fale com o admin do sistema no privado.
149
+
150
+ - openai*nao_configurada: ⚠️ \_IA indisponível no momento*
151
+
152
+ Este recurso está em manutenção.
153
+ Se precisar de ajuda, fale com o admin do sistema no privado.
149
154
 
150
- - openai*nao_configurada: ⚠️ \_OpenAI não configurada*
155
+ - imagem_muito_grande: ⚠️ A imagem está muito grande para análise (limite {{limite_mb}} MB). Envie uma imagem menor.
156
+ - imagem_download_falhou: ⚠️ Não consegui ler sua imagem agora. Reenvie a imagem.
157
+ Se o erro continuar, fale com o admin do sistema no privado.
158
+ - resposta_vazia: ⚠️ Não consegui montar uma resposta agora. Tente novamente em instantes.
159
+ - audio_muito_longo: ⚠️ A resposta ficou grande para áudio. Vou te enviar em texto.
160
+ - audio_falhou: ⚠️ Não consegui gerar o áudio agora. Vou te responder em texto.
161
+ - erro*openai: ❌ \_Não consegui responder agora*
151
162
 
152
- Defina a variável _OPENAI_API_KEY_ no `.env` para usar o comando _cat_.
163
+ Tente novamente em instantes.
164
+ Se o erro continuar, fale com o admin do sistema no privado.
153
165
 
154
- - imagem_muito_grande: ⚠️ A imagem enviada ultrapassa o limite de {{limite_mb}} MB. Envie uma imagem menor.
155
- - imagem_download_falhou: ⚠️ Não consegui baixar a imagem. Tente reenviar.
156
- - resposta_vazia: ⚠️ Não consegui gerar uma resposta agora. Tente novamente.
157
- - audio_muito_longo: ⚠️ A resposta ficou longa demais para áudio. Enviando em texto.
158
- - audio_falhou: ⚠️ Não consegui gerar o áudio agora. Enviando texto.
159
- - erro*openai: ❌ \_Erro ao falar com a IA*
160
- Tente novamente em alguns instantes.
166
+ - usage*header: 🤖 \_Comando CAT*
167
+ - resposta_prefixo_texto: 🐈‍⬛
161
168
  - limites_operacionais:
162
169
  - (nao informado)
163
170
  - opcoes:
@@ -209,8 +216,8 @@ Defina a variável _OPENAI_API_KEY_ no `.env` para usar o comando _cat_.
209
216
  - rate_limit.max: null
210
217
  - rate_limit.janela_ms: null
211
218
  - rate_limit.escopo: sem_rate_limit_explicito
212
- - access.somente_premium: false
213
- - access.planos_permitidos: comum, premium
219
+ - access.somente_premium: true
220
+ - access.planos_permitidos: premium
214
221
  - plan_limits.comum.max: 8
215
222
  - plan_limits.comum.janela_ms: 300000
216
223
  - plan_limits.comum.escopo: usuario
@@ -234,7 +241,7 @@ Defina a variável _OPENAI_API_KEY_ no `.env` para usar o comando _cat_.
234
241
  - enabled: true
235
242
  - categoria: ia
236
243
  - descricao: Gera/edita imagem com IA por prompt.
237
- - permissao_necessaria: usuario comum
244
+ - permissao_necessaria: usuario premium
238
245
  - version: 1.0.0
239
246
  - stability: stable
240
247
  - deprecated: nao
@@ -302,25 +309,33 @@ Defina a variável _OPENAI_API_KEY_ no `.env` para usar o comando _cat_.
302
309
  - erro_uso: Formato de uso inválido. Consulte metodos_de_uso.
303
310
  - erro_permissao: Permissão insuficiente para executar este comando.
304
311
  - mensagens_sistema:
305
- - premium*only: ⭐ \_Comando Premium*
312
+ - premium*only: ⭐ \_Recurso Premium*
306
313
 
307
- Este comando é exclusivo para usuários premium.
308
- Fale com o administrador para liberar o acesso.
314
+ Este comando é exclusivo para usuários Premium.
315
+ Para liberar o acesso, fale com o admin do sistema no privado.
309
316
 
310
- - openai*nao_configurada: ⚠️ \_OpenAI não configurada*
317
+ - openai*nao_configurada: ⚠️ \_Gerador de imagem indisponível*
311
318
 
312
- Defina a variável _OPENAI_API_KEY_ no `.env` para usar o comando _catimg_.
319
+ Este recurso está em manutenção.
320
+ Se precisar de ajuda, fale com o admin do sistema no privado.
313
321
 
314
- - imagem_muito_grande: ⚠️ A imagem enviada ultrapassa o limite de {{limite_mb}} MB. Envie uma imagem menor.
315
- - imagem_download_falhou: ⚠️ Não consegui baixar a imagem. Tente reenviar.
316
- - opcoes_invalidas: ⚠️ Opções inválidas no comando.
322
+ - imagem_muito_grande: ⚠️ A imagem está muito grande para edição (limite {{limite_mb}} MB). Envie uma imagem menor.
323
+ - imagem_download_falhou: ⚠️ Não consegui ler sua imagem agora. Reenvie a imagem.
324
+ Se o erro continuar, fale com o admin do sistema no privado.
325
+ - opcoes_invalidas: ⚠️ Algumas opções do comando estão inválidas.
317
326
  Detalhes: {{detalhes}}
318
327
 
319
328
  Use _{{prefix}}catimg_ sem opções para ver o formato correto.
320
329
 
321
- - resposta_vazia: ⚠️ Não consegui gerar a imagem agora. Tente novamente.
322
- - erro*openai: ❌ \_Erro ao falar com a IA*
323
- Tente novamente em alguns instantes.
330
+ - resposta_vazia: ⚠️ Não consegui gerar a imagem agora. Tente novamente em instantes.
331
+ - erro*openai: ❌ \_Não consegui gerar sua imagem agora*
332
+
333
+ Tente novamente em instantes.
334
+ Se o erro continuar, fale com o admin do sistema no privado.
335
+
336
+ - usage*header: 🖼️ \_Imagem IA*
337
+ - resposta_prefixo_texto_imagem: 🖼️
338
+ - imagem_caption_sucesso: 🖼️ Imagem gerada.
324
339
  - limites_operacionais:
325
340
  - (nao informado)
326
341
  - opcoes:
@@ -489,6 +504,7 @@ Fale com o administrador para liberar o acesso.
489
504
  - prompt_muito_longo: ⚠️ Prompt muito longo. Limite: {{max_chars}} caracteres.
490
505
  - prompt_reset_sucesso: ✅ Prompt da IA restaurado para o padrão.
491
506
  - prompt_update_sucesso: ✅ Prompt da IA atualizado para você.
507
+ - usage*header: 🧠 \_Prompt da IA*
492
508
  - limites_operacionais:
493
509
  - prompt_max_chars: 2000
494
510
  - opcoes:
@@ -505,6 +521,7 @@ Fale com o administrador para liberar o acesso.
505
521
  - set_status_reset.type: configuration_window
506
522
  - set_status_reset.allowed_actions: set, status, reset
507
523
  - set_status_reset.action_argument: valor
524
+ - parse.reset_aliases: reset, default, padrao, padrão
508
525
  - observabilidade:
509
526
  - event_name: command.executed
510
527
  - analytics_event: whatsapp_command_catprompt
@@ -114,6 +114,7 @@ export const getAiCommandOptionConfig = (command) => {
114
114
  parse: {
115
115
  audio_flags: Array.isArray(parse.audio_flags) ? parse.audio_flags.map((item) => String(item || '')).filter(Boolean) : [],
116
116
  text_flags: Array.isArray(parse.text_flags) ? parse.text_flags.map((item) => String(item || '')).filter(Boolean) : [],
117
+ reset_aliases: Array.isArray(parse.reset_aliases) ? parse.reset_aliases.map((item) => String(item || '')).filter(Boolean) : [],
117
118
  image_detail_aliases: normalizeMapKeys(parse.image_detail_aliases || {}),
118
119
  },
119
120
  geracao_imagem: {
@@ -35,14 +35,18 @@ const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
35
35
  const OWNER_JID = getAdminJid();
36
36
 
37
37
  const SESSION_TTL_SECONDS = Number.parseInt(process.env.OPENAI_SESSION_TTL_SECONDS || '21600', 10);
38
+ const ADMIN_ALERT_DEDUPE_WINDOW_MS_RAW = Number.parseInt(process.env.AI_ADMIN_ALERT_DEDUPE_WINDOW_MS || '120000', 10);
38
39
  const sessionCache = new NodeCache({
39
40
  stdTTL: SESSION_TTL_SECONDS,
40
41
  checkperiod: Math.max(60, Math.floor(SESSION_TTL_SECONDS / 4)),
41
42
  });
43
+ const ADMIN_ALERT_DEDUPE_WINDOW_MS = Number.isFinite(ADMIN_ALERT_DEDUPE_WINDOW_MS_RAW) && ADMIN_ALERT_DEDUPE_WINDOW_MS_RAW > 0 ? ADMIN_ALERT_DEDUPE_WINDOW_MS_RAW : 120000;
44
+ const adminAlertDedupCache = new Map();
42
45
  let cachedClient = null;
43
46
 
44
47
  const AUDIO_FLAG_ALIASES = new Set(['--audio', '--voz', '--voice', '--tts', '-a']);
45
48
  const TEXT_FLAG_ALIASES = new Set(['--texto', '--text', '--txt']);
49
+ const CATPROMPT_RESET_ALIASES = new Set(['reset', 'default', 'padrao', 'padrão']);
46
50
  const IMAGE_DETAIL_ALIASES = new Map([
47
51
  ['low', 'low'],
48
52
  ['high', 'high'],
@@ -152,6 +156,10 @@ const resolveAiMessages = (commandName) => {
152
156
  const mergeMessage = (key, fallback) => String(commandMessages?.[key] || '').trim() || String(fallback || '').trim();
153
157
 
154
158
  return {
159
+ usageHeader: mergeMessage('usage_header', '🤖 *Comando*'),
160
+ textResponsePrefix: mergeMessage('resposta_prefixo_texto', ''),
161
+ imageTextResponsePrefix: mergeMessage('resposta_prefixo_texto_imagem', '🖼️ '),
162
+ imageSuccessCaption: mergeMessage('imagem_caption_sucesso', '🖼️ Imagem gerada.'),
155
163
  premiumOnly: mergeMessage('premium_only', ['⭐ *Comando Premium*', '', 'Este comando é exclusivo para usuários premium.', 'Fale com o administrador para liberar o acesso.'].join('\n')),
156
164
  openAiNotConfigured: mergeMessage('openai_nao_configurada', ['⚠️ *OpenAI não configurada*', '', `Defina a variável *OPENAI_API_KEY* no \`.env\` para usar o comando *${commandName}*.`].join('\n')),
157
165
  imageTooLarge: mergeMessage('imagem_muito_grande', '⚠️ A imagem enviada ultrapassa o limite de {{limite_mb}} MB. Envie uma imagem menor.'),
@@ -232,6 +240,11 @@ const resolveCatPromptMaxChars = () => {
232
240
  return value;
233
241
  };
234
242
 
243
+ const resolveCatPromptResetAliases = () => {
244
+ const config = getAiCommandOptionConfig('catprompt');
245
+ return toSet(config?.parse?.reset_aliases, [...CATPROMPT_RESET_ALIASES]);
246
+ };
247
+
235
248
  const getClient = () => {
236
249
  if (cachedClient) return cachedClient;
237
250
  cachedClient = new OpenAI({
@@ -310,12 +323,13 @@ const callOpenAI = async (operationFactory, label, timeoutMs) => {
310
323
  };
311
324
 
312
325
  const sendUsage = async (sock, remoteJid, messageInfo, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX) => {
326
+ const commandMessages = resolveAiMessages('cat');
313
327
  const usageText =
314
328
  getAiUsageText('cat', {
315
329
  commandPrefix,
316
- header: '🤖 *Comando CAT*',
330
+ header: commandMessages.usageHeader,
317
331
  variant: 'default',
318
- }) || ['🤖 *Comando CAT*', '', 'Use assim:', `*${commandPrefix}cat* [--audio] sua pergunta`, `*${commandPrefix}cat* (responda ou envie uma imagem com legenda)`, '', 'Opções:', '--audio | --texto', '--detail low | high | auto', '', 'Exemplo:', `*${commandPrefix}cat* Explique como funciona a fotossíntese.`, `*${commandPrefix}cat* --audio Resuma a imagem.`].join('\n');
332
+ }) || `${commandMessages.usageHeader}\n*${commandPrefix}cat*`;
319
333
 
320
334
  await sendAndStore(sock, remoteJid, { text: usageText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
321
335
  };
@@ -364,24 +378,75 @@ const sendPremiumOnly = async (sock, remoteJid, messageInfo, expirationMessage,
364
378
  await sendAndStore(sock, remoteJid, { text: messages.premiumOnly }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
365
379
  };
366
380
 
381
+ const shouldSendAdminAlert = ({ commandName = '', stage = '', remoteJid = '', senderJid = '', errorMessage = '' } = {}) => {
382
+ const dedupeKey = `${normalizeText(commandName)}|${normalizeText(stage)}|${normalizeText(remoteJid)}|${normalizeText(senderJid)}|${normalizeText(errorMessage)}`;
383
+ const nowMs = __timeNowMs();
384
+ const lastSentAt = adminAlertDedupCache.get(dedupeKey);
385
+ if (Number.isFinite(lastSentAt) && nowMs - lastSentAt < ADMIN_ALERT_DEDUPE_WINDOW_MS) {
386
+ return false;
387
+ }
388
+
389
+ adminAlertDedupCache.set(dedupeKey, nowMs);
390
+ for (const [key, ts] of adminAlertDedupCache.entries()) {
391
+ if (!Number.isFinite(ts) || nowMs - ts > ADMIN_ALERT_DEDUPE_WINDOW_MS) {
392
+ adminAlertDedupCache.delete(key);
393
+ }
394
+ }
395
+ return true;
396
+ };
397
+
398
+ const notifyAdminAiError = async (sock, { commandName = 'cat', stage = 'unknown', remoteJid = '', senderJid = '', messageInfo = null, error = null } = {}) => {
399
+ try {
400
+ const adminJid = normalizeJid((await resolveAdminJid().catch(() => null)) || OWNER_JID || '');
401
+ if (!adminJid) return;
402
+
403
+ const normalizedRemote = normalizeJid(remoteJid || '') || String(remoteJid || '').trim() || 'desconhecido';
404
+ const normalizedSender = normalizeJid(senderJid || '') || String(senderJid || '').trim() || 'desconhecido';
405
+ const errorMessage = String(error?.message || error || 'erro desconhecido').trim() || 'erro desconhecido';
406
+ const errorStatus = error?.status || error?.statusCode || error?.response?.status || null;
407
+ const messageId = messageInfo?.key?.id || 'sem_id';
408
+ const shouldSend = shouldSendAdminAlert({
409
+ commandName,
410
+ stage,
411
+ remoteJid: normalizedRemote,
412
+ senderJid: normalizedSender,
413
+ errorMessage,
414
+ });
415
+ if (!shouldSend) return;
416
+
417
+ const statusLine = errorStatus ? `\n- Status: ${errorStatus}` : '';
418
+ await sendAndStore(sock, adminJid, {
419
+ text: `🚨 *Alerta IA*\n\n- Comando: ${commandName}\n- Etapa: ${stage}\n- Chat: ${normalizedRemote}\n- Usuário: ${normalizedSender}\n- MsgID: ${messageId}${statusLine}\n- Erro: ${errorMessage}\n- Horário (UTC): ${__timeNowIso()}`,
420
+ });
421
+ } catch (notifyError) {
422
+ logger.warn('notifyAdminAiError: falha ao notificar admin no privado.', {
423
+ error: notifyError?.message || String(notifyError),
424
+ commandName,
425
+ stage,
426
+ });
427
+ }
428
+ };
429
+
367
430
  const sendPromptUsage = async (sock, remoteJid, messageInfo, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX) => {
431
+ const commandMessages = resolveAiMessages('catprompt');
368
432
  const usageText =
369
433
  getAiUsageText('catprompt', {
370
434
  commandPrefix,
371
- header: '🧠 *Prompt da IA*',
435
+ header: commandMessages.usageHeader,
372
436
  variant: 'default',
373
- }) || ['🧠 *Prompt da IA*', '', 'Use assim:', `*${commandPrefix}catprompt* seu novo prompt`, '', 'Para voltar ao padrão:', `*${commandPrefix}catprompt reset*`].join('\n');
437
+ }) || `${commandMessages.usageHeader}\n*${commandPrefix}catprompt*`;
374
438
 
375
439
  await sendAndStore(sock, remoteJid, { text: usageText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
376
440
  };
377
441
 
378
442
  const sendImageUsage = async (sock, remoteJid, messageInfo, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX) => {
443
+ const commandMessages = resolveAiMessages('catimg');
379
444
  const usageText =
380
445
  getAiUsageText('catimg', {
381
446
  commandPrefix,
382
- header: '🖼️ *Imagem IA*',
447
+ header: commandMessages.usageHeader,
383
448
  variant: 'default',
384
- }) || ['🖼️ *Imagem IA*', '', 'Use assim:', `*${commandPrefix}catimg* seu prompt`, `*${commandPrefix}catimg* (responda uma imagem com legenda para editar)`, '', 'Opções:', '--size 1024x1024 | 1024x1536 | 1536x1024 | auto', '--quality low | medium | high | auto', '--format png | jpeg | webp', '--background transparent | opaque | auto', '--compression 0-100', '', 'Exemplo:', `*${commandPrefix}catimg* --size 1536x1024 Um gato astronauta em aquarela.`].join('\n');
449
+ }) || `${commandMessages.usageHeader}\n*${commandPrefix}catimg*`;
385
450
 
386
451
  await sendAndStore(sock, remoteJid, { text: usageText }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
387
452
  };
@@ -665,21 +730,29 @@ export async function handleCatCommand({ sock, remoteJid, messageInfo, expiratio
665
730
  const commandMessages = resolveAiMessages(commandName);
666
731
  const { prompt: rawPrompt, wantsAudio, imageDetail } = parseCatOptions(text || '', resolveCatParseOptions());
667
732
 
733
+ if (isAiCommandPremiumOnly(commandName)) {
734
+ if (!(await isPremiumAllowed(senderJid))) {
735
+ await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage, { commandName });
736
+ return;
737
+ }
738
+ }
739
+
668
740
  if (!process.env.OPENAI_API_KEY) {
669
741
  logger.warn('handleCatCommand: OPENAI_API_KEY não configurada.');
670
742
  await sendAndStore(sock, remoteJid, { text: commandMessages.openAiNotConfigured }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
743
+ await notifyAdminAiError(sock, {
744
+ commandName,
745
+ stage: 'openai_api_key_missing',
746
+ remoteJid,
747
+ senderJid,
748
+ messageInfo,
749
+ error: new Error('OPENAI_API_KEY não configurada para comando cat'),
750
+ });
671
751
  return;
672
752
  }
673
753
 
674
754
  await reactToMessage(sock, remoteJid, messageInfo);
675
755
 
676
- if (isAiCommandPremiumOnly(commandName)) {
677
- if (!(await isPremiumAllowed(senderJid))) {
678
- await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage, { commandName });
679
- return;
680
- }
681
- }
682
-
683
756
  const imageMedia = findImageMedia(messageInfo);
684
757
  const imageResult = await buildImageDataUrl(imageMedia, senderJid);
685
758
  if (imageResult.error === 'too_large') {
@@ -782,11 +855,20 @@ export async function handleCatCommand({ sock, remoteJid, messageInfo, expiratio
782
855
  } catch (audioError) {
783
856
  logger.error('handleCatCommand: erro ao gerar audio.', audioError);
784
857
  await sendAndStore(sock, remoteJid, { text: commandMessages.audioFailedFallback }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
858
+ await notifyAdminAiError(sock, {
859
+ commandName,
860
+ stage: 'audio_speech_create',
861
+ remoteJid,
862
+ senderJid,
863
+ messageInfo,
864
+ error: audioError,
865
+ });
785
866
  }
786
867
  }
787
868
  }
788
869
 
789
- await sendAndStore(sock, remoteJid, { text: `🐈‍⬛ ${outputText}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
870
+ const responsePrefix = commandMessages.textResponsePrefix;
871
+ await sendAndStore(sock, remoteJid, { text: `${responsePrefix}${outputText}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
790
872
  } catch (error) {
791
873
  logger.error('handleCatCommand: erro ao chamar OpenAI.', error);
792
874
  await sendAndStore(
@@ -797,6 +879,14 @@ export async function handleCatCommand({ sock, remoteJid, messageInfo, expiratio
797
879
  },
798
880
  { quoted: messageInfo, ephemeralExpiration: expirationMessage },
799
881
  );
882
+ await notifyAdminAiError(sock, {
883
+ commandName,
884
+ stage: 'responses_create',
885
+ remoteJid,
886
+ senderJid,
887
+ messageInfo,
888
+ error,
889
+ });
800
890
  }
801
891
  }
802
892
 
@@ -805,21 +895,29 @@ export async function handleCatImageCommand({ sock, remoteJid, messageInfo, expi
805
895
  const commandMessages = resolveAiMessages(commandName);
806
896
  const { prompt, toolOptions, errors } = parseImageGenOptions(text || '', resolveCatImageGenerationOptions());
807
897
 
898
+ if (isAiCommandPremiumOnly(commandName)) {
899
+ if (!(await isPremiumAllowed(senderJid))) {
900
+ await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage, { commandName });
901
+ return;
902
+ }
903
+ }
904
+
808
905
  if (!process.env.OPENAI_API_KEY) {
809
906
  logger.warn('handleCatImageCommand: OPENAI_API_KEY não configurada.');
810
907
  await sendAndStore(sock, remoteJid, { text: commandMessages.openAiNotConfigured }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
908
+ await notifyAdminAiError(sock, {
909
+ commandName,
910
+ stage: 'openai_api_key_missing',
911
+ remoteJid,
912
+ senderJid,
913
+ messageInfo,
914
+ error: new Error('OPENAI_API_KEY não configurada para comando catimg'),
915
+ });
811
916
  return;
812
917
  }
813
918
 
814
919
  await reactToMessage(sock, remoteJid, messageInfo);
815
920
 
816
- if (isAiCommandPremiumOnly(commandName)) {
817
- if (!(await isPremiumAllowed(senderJid))) {
818
- await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage, { commandName });
819
- return;
820
- }
821
- }
822
-
823
921
  const imageMedia = findImageMedia(messageInfo);
824
922
  const imageResult = await buildImageDataUrl(imageMedia, senderJid);
825
923
  if (imageResult.error === 'too_large') {
@@ -902,7 +1000,7 @@ export async function handleCatImageCommand({ sock, remoteJid, messageInfo, expi
902
1000
 
903
1001
  if (!imageBase64) {
904
1002
  if (outputText) {
905
- await sendAndStore(sock, remoteJid, { text: `🖼️ ${outputText}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
1003
+ await sendAndStore(sock, remoteJid, { text: `${commandMessages.imageTextResponsePrefix}${outputText}` }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
906
1004
  return;
907
1005
  }
908
1006
 
@@ -918,7 +1016,7 @@ export async function handleCatImageCommand({ sock, remoteJid, messageInfo, expi
918
1016
  };
919
1017
  const mimetype = mimeByFormat[outputFormat] || 'image/png';
920
1018
  const imageBuffer = Buffer.from(imageBase64, 'base64');
921
- const caption = outputText ? `🖼️ ${outputText}` : '🖼️ Imagem gerada.';
1019
+ const caption = outputText ? `${commandMessages.imageTextResponsePrefix}${outputText}` : commandMessages.imageSuccessCaption;
922
1020
 
923
1021
  await sendAndStore(sock, remoteJid, { image: imageBuffer, caption, mimetype }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
924
1022
  } catch (error) {
@@ -931,6 +1029,14 @@ export async function handleCatImageCommand({ sock, remoteJid, messageInfo, expi
931
1029
  },
932
1030
  { quoted: messageInfo, ephemeralExpiration: expirationMessage },
933
1031
  );
1032
+ await notifyAdminAiError(sock, {
1033
+ commandName,
1034
+ stage: 'responses_create_image',
1035
+ remoteJid,
1036
+ senderJid,
1037
+ messageInfo,
1038
+ error,
1039
+ });
934
1040
  }
935
1041
  }
936
1042
 
@@ -938,6 +1044,7 @@ export async function handleCatPromptCommand({ sock, remoteJid, messageInfo, exp
938
1044
  const commandName = 'catprompt';
939
1045
  const commandMessages = resolveAiMessages(commandName);
940
1046
  const promptMaxChars = resolveCatPromptMaxChars();
1047
+ const promptResetAliases = resolveCatPromptResetAliases();
941
1048
  const promptText = text?.trim();
942
1049
  if (!promptText) {
943
1050
  await sendPromptUsage(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
@@ -951,8 +1058,8 @@ export async function handleCatPromptCommand({ sock, remoteJid, messageInfo, exp
951
1058
  }
952
1059
  }
953
1060
 
954
- const lower = promptText.toLowerCase();
955
- if (lower === 'reset' || lower === 'default' || lower === 'padrao' || lower === 'padrão') {
1061
+ const lower = normalizeText(promptText);
1062
+ if (promptResetAliases.has(lower)) {
956
1063
  await aiPromptStore.clearPrompt(senderJid);
957
1064
  await sendAndStore(sock, remoteJid, { text: commandMessages.promptResetSuccess }, { quoted: messageInfo, ephemeralExpiration: expirationMessage });
958
1065
  return;