@kaikybrofc/omnizap-system 2.1.8

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 (166) hide show
  1. package/.env.example +534 -0
  2. package/LICENSE +21 -0
  3. package/README.md +431 -0
  4. package/RELEASE-v2.1.2.md +83 -0
  5. package/app/config/adminIdentity.js +87 -0
  6. package/app/config/baileysConfig.js +693 -0
  7. package/app/config/groupUtils.js +388 -0
  8. package/app/connection/socketController.js +992 -0
  9. package/app/controllers/messageController.js +354 -0
  10. package/app/modules/adminModule/groupCommandHandlers.js +1294 -0
  11. package/app/modules/adminModule/groupEventHandlers.js +355 -0
  12. package/app/modules/aiModule/catCommand.js +1006 -0
  13. package/app/modules/broadcastModule/noticeCommand.js +416 -0
  14. package/app/modules/gameModule/diceCommand.js +67 -0
  15. package/app/modules/menuModule/common.js +311 -0
  16. package/app/modules/menuModule/menus.js +59 -0
  17. package/app/modules/playModule/playCommand.js +1615 -0
  18. package/app/modules/quoteModule/quoteCommand.js +851 -0
  19. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +786 -0
  20. package/app/modules/rpgPokemonModule/rpgBattleService.js +2082 -0
  21. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +760 -0
  22. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  23. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +172 -0
  24. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  25. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  26. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  27. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  28. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1859 -0
  29. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6738 -0
  30. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  31. package/app/modules/statsModule/globalRankingCommand.js +65 -0
  32. package/app/modules/statsModule/noMessageCommand.js +288 -0
  33. package/app/modules/statsModule/rankingCommand.js +60 -0
  34. package/app/modules/statsModule/rankingCommon.js +889 -0
  35. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  36. package/app/modules/stickerModule/convertToWebp.js +390 -0
  37. package/app/modules/stickerModule/stickerCommand.js +454 -0
  38. package/app/modules/stickerModule/stickerConvertCommand.js +156 -0
  39. package/app/modules/stickerModule/stickerTextCommand.js +657 -0
  40. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  41. package/app/modules/stickerPackModule/autoPackCollectorService.js +284 -0
  42. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +466 -0
  43. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +88 -0
  44. package/app/modules/stickerPackModule/semanticThemeClusterService.js +571 -0
  45. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +449 -0
  46. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  47. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +180 -0
  48. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +4078 -0
  49. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +598 -0
  50. package/app/modules/stickerPackModule/stickerClassificationService.js +588 -0
  51. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +102 -0
  52. package/app/modules/stickerPackModule/stickerPackCatalogHttp.js +7506 -0
  53. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1095 -0
  54. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +108 -0
  55. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  56. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +110 -0
  57. package/app/modules/stickerPackModule/stickerPackItemRepository.js +440 -0
  58. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +337 -0
  59. package/app/modules/stickerPackModule/stickerPackMessageService.js +296 -0
  60. package/app/modules/stickerPackModule/stickerPackRepository.js +442 -0
  61. package/app/modules/stickerPackModule/stickerPackService.js +788 -0
  62. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +51 -0
  63. package/app/modules/stickerPackModule/stickerPackUtils.js +97 -0
  64. package/app/modules/stickerPackModule/stickerStorageService.js +507 -0
  65. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +233 -0
  66. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +205 -0
  67. package/app/modules/systemMetricsModule/pingCommand.js +421 -0
  68. package/app/modules/tiktokModule/tiktokCommand.js +798 -0
  69. package/app/modules/userModule/userCommand.js +1217 -0
  70. package/app/modules/waifuPicsModule/waifuPicsCommand.js +177 -0
  71. package/app/observability/metrics.js +734 -0
  72. package/app/services/captchaService.js +492 -0
  73. package/app/services/dbWriteQueue.js +572 -0
  74. package/app/services/groupMetadataService.js +279 -0
  75. package/app/services/lidMapService.js +663 -0
  76. package/app/services/messagePersistenceService.js +56 -0
  77. package/app/services/newsBroadcastService.js +351 -0
  78. package/app/services/pokeApiService.js +398 -0
  79. package/app/services/queueUtils.js +57 -0
  80. package/app/services/socketState.js +7 -0
  81. package/app/store/aiPromptStore.js +38 -0
  82. package/app/store/groupConfigStore.js +58 -0
  83. package/app/store/premiumUserStore.js +36 -0
  84. package/app/utils/antiLink/antiLinkModule.js +804 -0
  85. package/app/utils/http/getImageBufferModule.js +18 -0
  86. package/app/utils/json/jsonSanitizer.js +113 -0
  87. package/app/utils/json/jsonSanitizer.test.js +40 -0
  88. package/app/utils/logger/loggerModule.js +262 -0
  89. package/app/utils/systemMetrics/systemMetricsModule.js +91 -0
  90. package/database/index.js +2052 -0
  91. package/database/init.js +516 -0
  92. package/database/migrations/20260203_0001_sticker_packs.sql +54 -0
  93. package/database/migrations/20260210_0003_rpg_pokemon.sql +58 -0
  94. package/database/migrations/20260210_0004_rpg_shiny_biome.sql +9 -0
  95. package/database/migrations/20260210_0005_rpg_missions.sql +14 -0
  96. package/database/migrations/20260210_0006_rpg_world_pokedex_traits.sql +27 -0
  97. package/database/migrations/20260210_0007_rpg_raid_pvp.sql +56 -0
  98. package/database/migrations/20260210_0008_rpg_social_system.sql +195 -0
  99. package/database/migrations/20260211_0009_rpg_social_xp.sql +36 -0
  100. package/database/migrations/20260222_0010_remove_message_xp.sql +2 -0
  101. package/database/migrations/20260226_0011_sticker_asset_classification.sql +17 -0
  102. package/database/migrations/20260226_0012_sticker_pack_engagement.sql +16 -0
  103. package/database/migrations/20260226_0013_sticker_marketplace_intelligence.sql +19 -0
  104. package/database/migrations/20260226_0014_sticker_pack_publish_flow.sql +30 -0
  105. package/database/migrations/20260226_0014_sticker_worker_queues.sql +42 -0
  106. package/database/migrations/20260226_0015_sticker_auto_pack_curation_integrity.sql +18 -0
  107. package/database/migrations/20260226_0016_sticker_web_google_auth_persistence.sql +34 -0
  108. package/database/migrations/20260226_0017_sticker_web_admin_ban.sql +22 -0
  109. package/database/migrations/20260226_0018_sticker_web_admin_moderator.sql +18 -0
  110. package/database/migrations/20260227_0019_sticker_classification_v2_signals.sql +12 -0
  111. package/database/migrations/20260227_0020_semantic_theme_clusters.sql +35 -0
  112. package/docker-compose.yml +103 -0
  113. package/ecosystem.prod.config.cjs +35 -0
  114. package/eslint.config.js +61 -0
  115. package/index.js +437 -0
  116. package/ml/clip_classifier/Dockerfile +16 -0
  117. package/ml/clip_classifier/README.md +120 -0
  118. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  119. package/ml/clip_classifier/classifier.py +654 -0
  120. package/ml/clip_classifier/embedding_store.py +481 -0
  121. package/ml/clip_classifier/env_loader.py +15 -0
  122. package/ml/clip_classifier/llm_label_expander.py +144 -0
  123. package/ml/clip_classifier/main.py +213 -0
  124. package/ml/clip_classifier/requirements.txt +10 -0
  125. package/ml/clip_classifier/similarity_engine.py +74 -0
  126. package/observability/alert-rules.yml +60 -0
  127. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  128. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  129. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  130. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  131. package/observability/loki-config.yml +38 -0
  132. package/observability/mysql-exporter.cnf +5 -0
  133. package/observability/mysql-setup.sql +46 -0
  134. package/observability/prometheus.yml +32 -0
  135. package/observability/promtail-config.yml +84 -0
  136. package/package.json +109 -0
  137. package/public/api-docs/index.html +144 -0
  138. package/public/css/github-project-panel.css +297 -0
  139. package/public/css/stickers-admin.css +1272 -0
  140. package/public/css/styles.css +671 -0
  141. package/public/index.html +1311 -0
  142. package/public/js/apps/apiDocsApp.js +310 -0
  143. package/public/js/apps/createPackApp.js +2069 -0
  144. package/public/js/apps/homeApp.js +396 -0
  145. package/public/js/apps/stickersAdminApp.js +1744 -0
  146. package/public/js/apps/stickersApp.js +4830 -0
  147. package/public/js/catalog.js +1019 -0
  148. package/public/js/github-panel/components/CommitList.js +34 -0
  149. package/public/js/github-panel/components/ErrorState.js +16 -0
  150. package/public/js/github-panel/components/GithubProjectPanel.js +106 -0
  151. package/public/js/github-panel/components/ReleaseList.js +38 -0
  152. package/public/js/github-panel/components/SkeletonPanel.js +22 -0
  153. package/public/js/github-panel/components/StatCard.js +15 -0
  154. package/public/js/github-panel/index.js +15 -0
  155. package/public/js/github-panel/useGithubRepoData.js +154 -0
  156. package/public/js/github-panel/vendor/react.js +11 -0
  157. package/public/js/runtime/react-runtime.js +19 -0
  158. package/public/licenca/index.html +106 -0
  159. package/public/stickers/admin/index.html +23 -0
  160. package/public/stickers/create/index.html +47 -0
  161. package/public/stickers/index.html +48 -0
  162. package/public/termos-de-uso/index.html +125 -0
  163. package/scripts/cache-bust.mjs +107 -0
  164. package/scripts/deploy.sh +458 -0
  165. package/scripts/github-deploy-notify.mjs +174 -0
  166. package/scripts/release.sh +129 -0
@@ -0,0 +1,1006 @@
1
+ import OpenAI from 'openai';
2
+ import NodeCache from 'node-cache';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+
6
+ import logger from '../../utils/logger/loggerModule.js';
7
+ import premiumUserStore from '../../store/premiumUserStore.js';
8
+ import aiPromptStore from '../../store/aiPromptStore.js';
9
+ import { downloadMediaMessage, extractAllMediaDetails, getJidUser, normalizeJid } from '../../config/baileysConfig.js';
10
+ import { sendAndStore } from '../../services/messagePersistenceService.js';
11
+ import { getAdminJid, resolveAdminJid } from '../../config/adminIdentity.js';
12
+
13
+ const OPENAI_MODEL = process.env.OPENAI_MODEL || 'gpt-5-nano';
14
+ const OPENAI_IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || OPENAI_MODEL;
15
+ const OPENAI_TTS_MODEL = process.env.OPENAI_TTS_MODEL || 'gpt-4o-mini-tts';
16
+ const OPENAI_TTS_VOICE = process.env.OPENAI_TTS_VOICE || 'alloy';
17
+ const OPENAI_TTS_FORMAT_RAW = (process.env.OPENAI_TTS_FORMAT || 'mp3').toLowerCase();
18
+ const OPENAI_TTS_PTT = process.env.OPENAI_TTS_PTT === 'true';
19
+ const OPENAI_TTS_MAX_CHARS = Number.parseInt(process.env.OPENAI_TTS_MAX_CHARS || '4096', 10);
20
+ const OPENAI_MAX_IMAGE_MB = Number.parseFloat(process.env.OPENAI_MAX_IMAGE_MB || '50');
21
+ const OPENAI_TIMEOUT_MS = Number.parseInt(process.env.OPENAI_TIMEOUT_MS || '30000', 10);
22
+ const OPENAI_IMAGE_TIMEOUT_MS = Number.parseInt(process.env.OPENAI_IMAGE_TIMEOUT_MS || '120000', 10);
23
+ const OPENAI_MAX_RETRIES = Number.parseInt(process.env.OPENAI_MAX_RETRIES || '2', 10);
24
+ const OPENAI_RETRY_BASE_MS = Number.parseInt(process.env.OPENAI_RETRY_BASE_MS || '500', 10);
25
+ const OPENAI_RETRY_MAX_MS = Number.parseInt(process.env.OPENAI_RETRY_MAX_MS || '4000', 10);
26
+ const DEFAULT_SYSTEM_PROMPT = `Responda em PT-BR:`.trim();
27
+ const DEFAULT_IMAGE_PROMPT = 'Responda em PT-BR:';
28
+ const TEMP_DIR = path.join(process.cwd(), 'temp', 'ai');
29
+
30
+ const BASE_SYSTEM_PROMPT = process.env.OPENAI_SYSTEM_PROMPT?.trim() || DEFAULT_SYSTEM_PROMPT;
31
+ const DEFAULT_COMMAND_PREFIX = process.env.COMMAND_PREFIX || '/';
32
+ const OWNER_JID = getAdminJid();
33
+
34
+ const SESSION_TTL_SECONDS = Number.parseInt(process.env.OPENAI_SESSION_TTL_SECONDS || '21600', 10);
35
+ const sessionCache = new NodeCache({
36
+ stdTTL: SESSION_TTL_SECONDS,
37
+ checkperiod: Math.max(60, Math.floor(SESSION_TTL_SECONDS / 4)),
38
+ });
39
+ let cachedClient = null;
40
+
41
+ const AUDIO_FLAG_ALIASES = new Set(['--audio', '--voz', '--voice', '--tts', '-a']);
42
+ const TEXT_FLAG_ALIASES = new Set(['--texto', '--text', '--txt']);
43
+ const IMAGE_DETAIL_ALIASES = new Map([
44
+ ['low', 'low'],
45
+ ['high', 'high'],
46
+ ['auto', 'auto'],
47
+ ['baixo', 'low'],
48
+ ['baixa', 'low'],
49
+ ['alto', 'high'],
50
+ ['alta', 'high'],
51
+ ['automatico', 'auto'],
52
+ ['automático', 'auto'],
53
+ ]);
54
+ const IMAGE_GEN_SIZE_OPTIONS = new Set(['auto', '1024x1024', '1024x1536', '1536x1024']);
55
+ const IMAGE_GEN_SIZE_ALIASES = new Map([
56
+ ['1024', '1024x1024'],
57
+ ['square', '1024x1024'],
58
+ ['quadrado', '1024x1024'],
59
+ ['portrait', '1024x1536'],
60
+ ['retrato', '1024x1536'],
61
+ ['landscape', '1536x1024'],
62
+ ['paisagem', '1536x1024'],
63
+ ['auto', 'auto'],
64
+ ]);
65
+ const IMAGE_GEN_QUALITY_OPTIONS = new Set(['auto', 'low', 'medium', 'high']);
66
+ const IMAGE_GEN_QUALITY_ALIASES = new Map([
67
+ ['baixa', 'low'],
68
+ ['baixo', 'low'],
69
+ ['media', 'medium'],
70
+ ['média', 'medium'],
71
+ ['medio', 'medium'],
72
+ ['médio', 'medium'],
73
+ ['alta', 'high'],
74
+ ['alto', 'high'],
75
+ ['auto', 'auto'],
76
+ ]);
77
+ const IMAGE_GEN_FORMAT_OPTIONS = new Set(['png', 'jpeg', 'webp']);
78
+ const IMAGE_GEN_FORMAT_ALIASES = new Map([
79
+ ['jpg', 'jpeg'],
80
+ ['jpeg', 'jpeg'],
81
+ ['png', 'png'],
82
+ ['webp', 'webp'],
83
+ ]);
84
+ const IMAGE_GEN_BACKGROUND_OPTIONS = new Set(['auto', 'transparent', 'opaque']);
85
+ const IMAGE_GEN_BACKGROUND_ALIASES = new Map([
86
+ ['auto', 'auto'],
87
+ ['transparent', 'transparent'],
88
+ ['transparente', 'transparent'],
89
+ ['opaque', 'opaque'],
90
+ ['opaco', 'opaque'],
91
+ ['opaca', 'opaque'],
92
+ ]);
93
+ const IMAGE_GEN_FLAG_ALIASES = {
94
+ size: new Set(['--size', '--tamanho']),
95
+ quality: new Set(['--quality', '--qualidade']),
96
+ format: new Set(['--format', '--formato']),
97
+ background: new Set(['--background', '--fundo']),
98
+ compression: new Set(['--compression', '--compressao', '--compressão']),
99
+ };
100
+ const AUDIO_MIME_BY_FORMAT = {
101
+ mp3: 'audio/mpeg',
102
+ wav: 'audio/wav',
103
+ opus: 'audio/ogg; codecs=opus',
104
+ aac: 'audio/aac',
105
+ flac: 'audio/flac',
106
+ pcm: 'audio/pcm',
107
+ };
108
+ const SAFE_TTS_FORMAT = AUDIO_MIME_BY_FORMAT[OPENAI_TTS_FORMAT_RAW] ? OPENAI_TTS_FORMAT_RAW : 'mp3';
109
+ const TTS_OUTPUT_FORMAT = OPENAI_TTS_PTT ? 'opus' : SAFE_TTS_FORMAT;
110
+ const TTS_MIME_TYPE = AUDIO_MIME_BY_FORMAT[TTS_OUTPUT_FORMAT] || 'audio/mpeg';
111
+ const TTS_MAX_CHARS = Number.isFinite(OPENAI_TTS_MAX_CHARS) && OPENAI_TTS_MAX_CHARS > 0 ? OPENAI_TTS_MAX_CHARS : 4096;
112
+ const OPENAI_TIMEOUT = Number.isFinite(OPENAI_TIMEOUT_MS) && OPENAI_TIMEOUT_MS > 0 ? OPENAI_TIMEOUT_MS : 30000;
113
+ const OPENAI_IMAGE_TIMEOUT =
114
+ Number.isFinite(OPENAI_IMAGE_TIMEOUT_MS) && OPENAI_IMAGE_TIMEOUT_MS > 0 ? OPENAI_IMAGE_TIMEOUT_MS : 120000;
115
+ const OPENAI_CLIENT_TIMEOUT = Math.max(OPENAI_TIMEOUT, OPENAI_IMAGE_TIMEOUT);
116
+ const OPENAI_RETRIES = Number.isFinite(OPENAI_MAX_RETRIES) && OPENAI_MAX_RETRIES >= 0 ? OPENAI_MAX_RETRIES : 2;
117
+ const OPENAI_RETRY_BASE =
118
+ Number.isFinite(OPENAI_RETRY_BASE_MS) && OPENAI_RETRY_BASE_MS > 0 ? OPENAI_RETRY_BASE_MS : 500;
119
+ const OPENAI_RETRY_MAX = Number.isFinite(OPENAI_RETRY_MAX_MS) && OPENAI_RETRY_MAX_MS > 0 ? OPENAI_RETRY_MAX_MS : 4000;
120
+ const MAX_IMAGE_BYTES =
121
+ Number.isFinite(OPENAI_MAX_IMAGE_MB) && OPENAI_MAX_IMAGE_MB > 0
122
+ ? OPENAI_MAX_IMAGE_MB * 1024 * 1024
123
+ : 50 * 1024 * 1024;
124
+
125
+ const getClient = () => {
126
+ if (cachedClient) return cachedClient;
127
+ cachedClient = new OpenAI({
128
+ apiKey: process.env.OPENAI_API_KEY,
129
+ timeout: OPENAI_CLIENT_TIMEOUT,
130
+ maxRetries: 0,
131
+ });
132
+ return cachedClient;
133
+ };
134
+
135
+ const buildSessionKey = (remoteJid, senderJid, scope) => {
136
+ const base = `${remoteJid}:${senderJid}`;
137
+ return scope ? `${base}:${scope}` : base;
138
+ };
139
+
140
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
141
+
142
+ const isRetryableOpenAIError = (error) => {
143
+ const status = error?.status || error?.statusCode || error?.response?.status;
144
+ if ([408, 409, 429, 500, 502, 503, 504].includes(status)) return true;
145
+ const code = error?.code || error?.cause?.code;
146
+ if (['ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN', 'ENOTFOUND', 'ECONNREFUSED', 'EPIPE'].includes(code)) {
147
+ return true;
148
+ }
149
+ if (error?.name === 'AbortError') return true;
150
+ if (typeof error?.message === 'string' && /timeout/i.test(error.message)) return true;
151
+ return false;
152
+ };
153
+
154
+ const runWithTimeout = async (operation, label, timeoutMs = OPENAI_TIMEOUT) => {
155
+ if (!timeoutMs || timeoutMs <= 0) {
156
+ return operation;
157
+ }
158
+ let timeoutId;
159
+ let didTimeout = false;
160
+ const timeoutPromise = new Promise((_, reject) => {
161
+ timeoutId = setTimeout(() => {
162
+ didTimeout = true;
163
+ const timeoutError = new Error(`OpenAI ${label} excedeu ${timeoutMs}ms`);
164
+ timeoutError.code = 'OPENAI_TIMEOUT';
165
+ reject(timeoutError);
166
+ }, timeoutMs);
167
+ });
168
+ try {
169
+ return await Promise.race([operation, timeoutPromise]);
170
+ } catch (error) {
171
+ if (didTimeout && operation?.catch) {
172
+ operation.catch(() => {});
173
+ }
174
+ throw error;
175
+ } finally {
176
+ clearTimeout(timeoutId);
177
+ }
178
+ };
179
+
180
+ const callOpenAI = async (operationFactory, label, timeoutMs) => {
181
+ let attempt = 0;
182
+ while (true) {
183
+ try {
184
+ const operation = operationFactory();
185
+ return await runWithTimeout(operation, label, timeoutMs);
186
+ } catch (error) {
187
+ attempt += 1;
188
+ if (attempt > OPENAI_RETRIES || !isRetryableOpenAIError(error)) {
189
+ throw error;
190
+ }
191
+ const backoff = Math.min(OPENAI_RETRY_MAX, OPENAI_RETRY_BASE * 2 ** (attempt - 1));
192
+ const jitter = Math.round(backoff * (0.8 + Math.random() * 0.4));
193
+ logger.warn(`OpenAI ${label} falhou. Retry ${attempt}/${OPENAI_RETRIES} em ${jitter}ms.`, {
194
+ error: error.message,
195
+ status: error?.status || error?.statusCode || error?.response?.status || null,
196
+ });
197
+ await sleep(jitter);
198
+ }
199
+ }
200
+ };
201
+
202
+ const sendUsage = async (sock, remoteJid, messageInfo, expirationMessage, commandPrefix = DEFAULT_COMMAND_PREFIX) => {
203
+ await sendAndStore(
204
+ sock,
205
+ remoteJid,
206
+ {
207
+ text: [
208
+ '🤖 *Comando CAT*',
209
+ '',
210
+ 'Use assim:',
211
+ `*${commandPrefix}cat* [--audio] sua pergunta`,
212
+ `*${commandPrefix}cat* (responda ou envie uma imagem com legenda)`,
213
+ '',
214
+ 'Opções:',
215
+ '--audio | --texto',
216
+ '--detail low | high | auto',
217
+ '',
218
+ 'Exemplo:',
219
+ `*${commandPrefix}cat* Explique como funciona a fotossíntese.`,
220
+ `*${commandPrefix}cat* --audio Resuma a imagem.`,
221
+ ].join('\n'),
222
+ },
223
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
224
+ );
225
+ };
226
+
227
+ const reactToMessage = async (sock, remoteJid, messageInfo) => {
228
+ try {
229
+ if (!messageInfo?.key) return;
230
+ await sendAndStore(sock, remoteJid, {
231
+ react: {
232
+ text: '🐈‍⬛',
233
+ key: messageInfo.key,
234
+ },
235
+ });
236
+ } catch (error) {
237
+ logger.warn('handleCatCommand: falha ao reagir à mensagem.', error);
238
+ }
239
+ };
240
+
241
+ const isPremiumAllowed = async (senderJid) => {
242
+ const adminJid = (await resolveAdminJid()) || OWNER_JID;
243
+ if (!adminJid) return true;
244
+ const normalizedSender = normalizeJid(senderJid);
245
+ if (normalizedSender && normalizedSender === adminJid) return true;
246
+ const premiumUsers = await premiumUserStore.getPremiumUsers();
247
+ if (!Array.isArray(premiumUsers) || premiumUsers.length === 0) return false;
248
+ return premiumUsers.map((jid) => normalizeJid(jid)).includes(normalizedSender);
249
+ };
250
+
251
+ const sendPremiumOnly = async (sock, remoteJid, messageInfo, expirationMessage) => {
252
+ await sendAndStore(
253
+ sock,
254
+ remoteJid,
255
+ {
256
+ text: [
257
+ '⭐ *Comando Premium*',
258
+ '',
259
+ 'Este comando é exclusivo para usuários premium.',
260
+ 'Fale com o administrador para liberar o acesso.',
261
+ ].join('\n'),
262
+ },
263
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
264
+ );
265
+ };
266
+
267
+ const sendPromptUsage = async (
268
+ sock,
269
+ remoteJid,
270
+ messageInfo,
271
+ expirationMessage,
272
+ commandPrefix = DEFAULT_COMMAND_PREFIX,
273
+ ) => {
274
+ await sendAndStore(
275
+ sock,
276
+ remoteJid,
277
+ {
278
+ text: [
279
+ '🧠 *Prompt da IA*',
280
+ '',
281
+ 'Use assim:',
282
+ `*${commandPrefix}catprompt* seu novo prompt`,
283
+ '',
284
+ 'Para voltar ao padrão:',
285
+ `*${commandPrefix}catprompt reset*`,
286
+ ].join('\n'),
287
+ },
288
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
289
+ );
290
+ };
291
+
292
+ const sendImageUsage = async (
293
+ sock,
294
+ remoteJid,
295
+ messageInfo,
296
+ expirationMessage,
297
+ commandPrefix = DEFAULT_COMMAND_PREFIX,
298
+ ) => {
299
+ await sendAndStore(
300
+ sock,
301
+ remoteJid,
302
+ {
303
+ text: [
304
+ '🖼️ *Imagem IA*',
305
+ '',
306
+ 'Use assim:',
307
+ `*${commandPrefix}catimg* seu prompt`,
308
+ `*${commandPrefix}catimg* (responda uma imagem com legenda para editar)`,
309
+ '',
310
+ 'Opções:',
311
+ '--size 1024x1024 | 1024x1536 | 1536x1024 | auto',
312
+ '--quality low | medium | high | auto',
313
+ '--format png | jpeg | webp',
314
+ '--background transparent | opaque | auto',
315
+ '--compression 0-100',
316
+ '',
317
+ 'Exemplo:',
318
+ `*${commandPrefix}catimg* --size 1536x1024 Um gato astronauta em aquarela.`,
319
+ ].join('\n'),
320
+ },
321
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
322
+ );
323
+ };
324
+
325
+ const normalizeImageDetail = (value) => {
326
+ if (!value) return null;
327
+ const normalized = IMAGE_DETAIL_ALIASES.get(String(value).toLowerCase());
328
+ return normalized || null;
329
+ };
330
+
331
+ const normalizeImageGenSize = (value) => {
332
+ if (!value) return null;
333
+ const raw = String(value).toLowerCase();
334
+ if (IMAGE_GEN_SIZE_OPTIONS.has(raw)) return raw;
335
+ const alias = IMAGE_GEN_SIZE_ALIASES.get(raw);
336
+ if (alias && IMAGE_GEN_SIZE_OPTIONS.has(alias)) return alias;
337
+ return null;
338
+ };
339
+
340
+ const normalizeImageGenQuality = (value) => {
341
+ if (!value) return null;
342
+ const raw = String(value).toLowerCase();
343
+ if (IMAGE_GEN_QUALITY_OPTIONS.has(raw)) return raw;
344
+ const alias = IMAGE_GEN_QUALITY_ALIASES.get(raw);
345
+ if (alias && IMAGE_GEN_QUALITY_OPTIONS.has(alias)) return alias;
346
+ return null;
347
+ };
348
+
349
+ const normalizeImageGenFormat = (value) => {
350
+ if (!value) return null;
351
+ const raw = String(value).toLowerCase();
352
+ const alias = IMAGE_GEN_FORMAT_ALIASES.get(raw);
353
+ if (alias && IMAGE_GEN_FORMAT_OPTIONS.has(alias)) return alias;
354
+ if (IMAGE_GEN_FORMAT_OPTIONS.has(raw)) return raw;
355
+ return null;
356
+ };
357
+
358
+ const normalizeImageGenBackground = (value) => {
359
+ if (!value) return null;
360
+ const raw = String(value).toLowerCase();
361
+ if (IMAGE_GEN_BACKGROUND_OPTIONS.has(raw)) return raw;
362
+ const alias = IMAGE_GEN_BACKGROUND_ALIASES.get(raw);
363
+ if (alias && IMAGE_GEN_BACKGROUND_OPTIONS.has(alias)) return alias;
364
+ return null;
365
+ };
366
+
367
+ const normalizeImageGenCompression = (value) => {
368
+ if (value === null || value === undefined || value === '') return null;
369
+ const numeric = Number.parseInt(value, 10);
370
+ if (!Number.isFinite(numeric)) return null;
371
+ if (numeric < 0 || numeric > 100) return null;
372
+ return numeric;
373
+ };
374
+
375
+ const parseCatOptions = (rawText = '') => {
376
+ const tokens = rawText.trim().split(/\s+/).filter(Boolean);
377
+ let wantsAudio = false;
378
+ let imageDetail = null;
379
+ const filtered = [];
380
+
381
+ for (let i = 0; i < tokens.length; i += 1) {
382
+ const token = tokens[i];
383
+ const lower = token.toLowerCase();
384
+
385
+ if (AUDIO_FLAG_ALIASES.has(lower)) {
386
+ wantsAudio = true;
387
+ continue;
388
+ }
389
+ if (TEXT_FLAG_ALIASES.has(lower)) {
390
+ wantsAudio = false;
391
+ continue;
392
+ }
393
+
394
+ if (lower.startsWith('--detail=') || lower.startsWith('--detalhe=')) {
395
+ const value = token.split('=')[1];
396
+ const detail = normalizeImageDetail(value);
397
+ if (detail) {
398
+ imageDetail = detail;
399
+ continue;
400
+ }
401
+ }
402
+
403
+ if (lower === '--detail' || lower === '--detalhe') {
404
+ const value = tokens[i + 1];
405
+ const detail = normalizeImageDetail(value);
406
+ if (detail) {
407
+ imageDetail = detail;
408
+ i += 1;
409
+ continue;
410
+ }
411
+ }
412
+
413
+ filtered.push(token);
414
+ }
415
+
416
+ return {
417
+ prompt: filtered.join(' ').trim(),
418
+ wantsAudio,
419
+ imageDetail,
420
+ };
421
+ };
422
+
423
+ const parseImageGenOptions = (rawText = '') => {
424
+ const tokens = rawText.trim().split(/\s+/).filter(Boolean);
425
+ const promptParts = [];
426
+ const toolOptions = {};
427
+ const errors = [];
428
+
429
+ const setOption = (key, rawValue, normalizedValue) => {
430
+ if (!normalizedValue) {
431
+ errors.push(`${key}=${rawValue}`);
432
+ return;
433
+ }
434
+ toolOptions[key] = normalizedValue;
435
+ };
436
+
437
+ for (let i = 0; i < tokens.length; i += 1) {
438
+ const token = tokens[i];
439
+ const lower = token.toLowerCase();
440
+
441
+ if (lower.startsWith('--size=')) {
442
+ const value = token.split('=')[1];
443
+ setOption('size', value, normalizeImageGenSize(value));
444
+ continue;
445
+ }
446
+ if (IMAGE_GEN_FLAG_ALIASES.size.has(lower)) {
447
+ const value = tokens[i + 1];
448
+ if (value) {
449
+ setOption('size', value, normalizeImageGenSize(value));
450
+ i += 1;
451
+ continue;
452
+ }
453
+ }
454
+
455
+ if (lower.startsWith('--quality=')) {
456
+ const value = token.split('=')[1];
457
+ setOption('quality', value, normalizeImageGenQuality(value));
458
+ continue;
459
+ }
460
+ if (IMAGE_GEN_FLAG_ALIASES.quality.has(lower)) {
461
+ const value = tokens[i + 1];
462
+ if (value) {
463
+ setOption('quality', value, normalizeImageGenQuality(value));
464
+ i += 1;
465
+ continue;
466
+ }
467
+ }
468
+
469
+ if (lower.startsWith('--format=')) {
470
+ const value = token.split('=')[1];
471
+ setOption('output_format', value, normalizeImageGenFormat(value));
472
+ continue;
473
+ }
474
+ if (IMAGE_GEN_FLAG_ALIASES.format.has(lower)) {
475
+ const value = tokens[i + 1];
476
+ if (value) {
477
+ setOption('output_format', value, normalizeImageGenFormat(value));
478
+ i += 1;
479
+ continue;
480
+ }
481
+ }
482
+
483
+ if (lower.startsWith('--background=')) {
484
+ const value = token.split('=')[1];
485
+ setOption('background', value, normalizeImageGenBackground(value));
486
+ continue;
487
+ }
488
+ if (IMAGE_GEN_FLAG_ALIASES.background.has(lower)) {
489
+ const value = tokens[i + 1];
490
+ if (value) {
491
+ setOption('background', value, normalizeImageGenBackground(value));
492
+ i += 1;
493
+ continue;
494
+ }
495
+ }
496
+
497
+ if (lower.startsWith('--compression=')) {
498
+ const value = token.split('=')[1];
499
+ setOption('output_compression', value, normalizeImageGenCompression(value));
500
+ continue;
501
+ }
502
+ if (IMAGE_GEN_FLAG_ALIASES.compression.has(lower)) {
503
+ const value = tokens[i + 1];
504
+ if (value) {
505
+ setOption('output_compression', value, normalizeImageGenCompression(value));
506
+ i += 1;
507
+ continue;
508
+ }
509
+ }
510
+
511
+ if (lower === '--transparent' || lower === '--transparente') {
512
+ toolOptions.background = 'transparent';
513
+ continue;
514
+ }
515
+ if (lower === '--opaque' || lower === '--opaco' || lower === '--opaca') {
516
+ toolOptions.background = 'opaque';
517
+ continue;
518
+ }
519
+
520
+ promptParts.push(token);
521
+ }
522
+
523
+ if (toolOptions.output_compression !== undefined) {
524
+ const format = toolOptions.output_format;
525
+ if (!format || !['jpeg', 'webp'].includes(format)) {
526
+ errors.push('output_compression');
527
+ delete toolOptions.output_compression;
528
+ }
529
+ }
530
+
531
+ return {
532
+ prompt: promptParts.join(' ').trim(),
533
+ toolOptions,
534
+ errors,
535
+ };
536
+ };
537
+
538
+ const buildUserTempDir = (senderJid) => {
539
+ const userId = getJidUser(senderJid) || senderJid || 'anon';
540
+ const sanitizedUserId = String(userId).replace(/[^a-zA-Z0-9.-]/g, '_');
541
+ return path.join(TEMP_DIR, sanitizedUserId);
542
+ };
543
+
544
+ const findImageMedia = (messageInfo) => {
545
+ const mediaEntries = extractAllMediaDetails(messageInfo, {
546
+ includeAllTypes: true,
547
+ includeQuoted: true,
548
+ includeUnknown: false,
549
+ });
550
+ return mediaEntries.find((entry) => entry.mediaType === 'image') || null;
551
+ };
552
+
553
+ const buildImageDataUrl = async (imageMedia, senderJid) => {
554
+ if (!imageMedia) {
555
+ return { dataUrl: null };
556
+ }
557
+
558
+ const fileLength = imageMedia.fileLength || imageMedia.mediaKey?.fileLength || 0;
559
+ if (fileLength && fileLength > MAX_IMAGE_BYTES) {
560
+ return { error: 'too_large', fileLength };
561
+ }
562
+
563
+ const userDir = buildUserTempDir(senderJid);
564
+ await fs.mkdir(userDir, { recursive: true });
565
+
566
+ let downloadedPath = null;
567
+ try {
568
+ downloadedPath = await downloadMediaMessage(imageMedia.mediaKey, 'image', userDir);
569
+ if (!downloadedPath) {
570
+ return { error: 'download_failed' };
571
+ }
572
+
573
+ const buffer = await fs.readFile(downloadedPath);
574
+ const base64 = buffer.toString('base64');
575
+ const mimeType = imageMedia.mimetype || imageMedia.mediaKey?.mimetype || 'image/jpeg';
576
+ return { dataUrl: `data:${mimeType};base64,${base64}` };
577
+ } finally {
578
+ if (downloadedPath) {
579
+ fs.unlink(downloadedPath).catch(() => {});
580
+ }
581
+ }
582
+ };
583
+
584
+ export async function handleCatCommand({
585
+ sock,
586
+ remoteJid,
587
+ messageInfo,
588
+ expirationMessage,
589
+ senderJid,
590
+ text,
591
+ commandPrefix = DEFAULT_COMMAND_PREFIX,
592
+ }) {
593
+ const { prompt: rawPrompt, wantsAudio, imageDetail } = parseCatOptions(text || '');
594
+
595
+ if (!process.env.OPENAI_API_KEY) {
596
+ logger.warn('handleCatCommand: OPENAI_API_KEY não configurada.');
597
+ await sendAndStore(
598
+ sock,
599
+ remoteJid,
600
+ {
601
+ text: [
602
+ '⚠️ *OpenAI não configurada*',
603
+ '',
604
+ 'Defina a variável *OPENAI_API_KEY* no `.env` para usar o comando *cat*.',
605
+ ].join('\n'),
606
+ },
607
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
608
+ );
609
+ return;
610
+ }
611
+
612
+ await reactToMessage(sock, remoteJid, messageInfo);
613
+
614
+ if (!(await isPremiumAllowed(senderJid))) {
615
+ await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage);
616
+ return;
617
+ }
618
+
619
+ const imageMedia = findImageMedia(messageInfo);
620
+ const imageResult = await buildImageDataUrl(imageMedia, senderJid);
621
+ if (imageResult.error === 'too_large') {
622
+ const limitMb = Math.round((MAX_IMAGE_BYTES / (1024 * 1024)) * 10) / 10;
623
+ await sendAndStore(
624
+ sock,
625
+ remoteJid,
626
+ {
627
+ text: `⚠️ A imagem enviada ultrapassa o limite de ${limitMb} MB. Envie uma imagem menor.`,
628
+ },
629
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
630
+ );
631
+ return;
632
+ }
633
+
634
+ if (imageResult.error === 'download_failed') {
635
+ await sendAndStore(
636
+ sock,
637
+ remoteJid,
638
+ { text: '⚠️ Não consegui baixar a imagem. Tente reenviar.' },
639
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
640
+ );
641
+ return;
642
+ }
643
+
644
+ const sessionKey = buildSessionKey(remoteJid, senderJid);
645
+ const session = sessionCache.get(sessionKey);
646
+ const userPrompt = await aiPromptStore.getPrompt(senderJid);
647
+ const userPreference = typeof userPrompt === 'string' ? userPrompt.trim() : '';
648
+ const effectiveSystemPrompt = userPreference || BASE_SYSTEM_PROMPT;
649
+ const effectiveImagePrompt = userPreference || DEFAULT_IMAGE_PROMPT;
650
+
651
+ const effectivePrompt = rawPrompt || (imageResult.dataUrl ? effectiveImagePrompt : '');
652
+ if (!effectivePrompt && !imageResult.dataUrl) {
653
+ await sendUsage(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
654
+ return;
655
+ }
656
+
657
+ const content = [];
658
+ if (effectivePrompt) {
659
+ content.push({ type: 'input_text', text: effectivePrompt });
660
+ }
661
+ if (imageResult.dataUrl) {
662
+ const imagePayload = { type: 'input_image', image_url: imageResult.dataUrl };
663
+ if (imageDetail) {
664
+ imagePayload.detail = imageDetail;
665
+ }
666
+ content.push(imagePayload);
667
+ }
668
+
669
+ const payload = {
670
+ model: OPENAI_MODEL,
671
+ input: [
672
+ {
673
+ role: 'user',
674
+ content,
675
+ },
676
+ ],
677
+ };
678
+
679
+ if (effectiveSystemPrompt) {
680
+ payload.instructions = effectiveSystemPrompt;
681
+ }
682
+
683
+ if (session?.previousResponseId) {
684
+ payload.previous_response_id = session.previousResponseId;
685
+ }
686
+
687
+ try {
688
+ const client = getClient();
689
+ const response = await callOpenAI(() => client.responses.create(payload), 'responses.create', OPENAI_TIMEOUT);
690
+ const outputText = response.output_text?.trim();
691
+
692
+ sessionCache.set(sessionKey, {
693
+ previousResponseId: response.id,
694
+ updatedAt: Date.now(),
695
+ });
696
+
697
+ if (!outputText) {
698
+ await sendAndStore(
699
+ sock,
700
+ remoteJid,
701
+ { text: '⚠️ Não consegui gerar uma resposta agora. Tente novamente.' },
702
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
703
+ );
704
+ return;
705
+ }
706
+
707
+ if (wantsAudio) {
708
+ if (outputText.length > TTS_MAX_CHARS) {
709
+ await sendAndStore(
710
+ sock,
711
+ remoteJid,
712
+ { text: '⚠️ A resposta ficou longa demais para áudio. Enviando em texto.' },
713
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
714
+ );
715
+ } else {
716
+ try {
717
+ const audioResponse = await callOpenAI(
718
+ () =>
719
+ client.audio.speech.create({
720
+ model: OPENAI_TTS_MODEL,
721
+ voice: OPENAI_TTS_VOICE,
722
+ input: outputText,
723
+ response_format: TTS_OUTPUT_FORMAT,
724
+ }),
725
+ 'audio.speech.create',
726
+ OPENAI_TIMEOUT,
727
+ );
728
+ const audioBuffer = Buffer.from(await audioResponse.arrayBuffer());
729
+ await sendAndStore(
730
+ sock,
731
+ remoteJid,
732
+ {
733
+ audio: audioBuffer,
734
+ mimetype: TTS_MIME_TYPE,
735
+ ptt: OPENAI_TTS_PTT,
736
+ },
737
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
738
+ );
739
+ return;
740
+ } catch (audioError) {
741
+ logger.error('handleCatCommand: erro ao gerar audio.', audioError);
742
+ await sendAndStore(
743
+ sock,
744
+ remoteJid,
745
+ { text: '⚠️ Não consegui gerar o áudio agora. Enviando texto.' },
746
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
747
+ );
748
+ }
749
+ }
750
+ }
751
+
752
+ await sendAndStore(
753
+ sock,
754
+ remoteJid,
755
+ { text: `🐈‍⬛ ${outputText}` },
756
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
757
+ );
758
+ } catch (error) {
759
+ logger.error('handleCatCommand: erro ao chamar OpenAI.', error);
760
+ await sendAndStore(
761
+ sock,
762
+ remoteJid,
763
+ {
764
+ text: ['❌ *Erro ao falar com a IA*', 'Tente novamente em alguns instantes.'].join('\n'),
765
+ },
766
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
767
+ );
768
+ }
769
+ }
770
+
771
+ export async function handleCatImageCommand({
772
+ sock,
773
+ remoteJid,
774
+ messageInfo,
775
+ expirationMessage,
776
+ senderJid,
777
+ text,
778
+ commandPrefix = DEFAULT_COMMAND_PREFIX,
779
+ }) {
780
+ const { prompt, toolOptions, errors } = parseImageGenOptions(text || '');
781
+
782
+ if (!process.env.OPENAI_API_KEY) {
783
+ logger.warn('handleCatImageCommand: OPENAI_API_KEY não configurada.');
784
+ await sendAndStore(
785
+ sock,
786
+ remoteJid,
787
+ {
788
+ text: [
789
+ '⚠️ *OpenAI não configurada*',
790
+ '',
791
+ 'Defina a variável *OPENAI_API_KEY* no `.env` para usar o comando *catimg*.',
792
+ ].join('\n'),
793
+ },
794
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
795
+ );
796
+ return;
797
+ }
798
+
799
+ await reactToMessage(sock, remoteJid, messageInfo);
800
+
801
+ if (!(await isPremiumAllowed(senderJid))) {
802
+ await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage);
803
+ return;
804
+ }
805
+
806
+ const imageMedia = findImageMedia(messageInfo);
807
+ const imageResult = await buildImageDataUrl(imageMedia, senderJid);
808
+ if (imageResult.error === 'too_large') {
809
+ const limitMb = Math.round((MAX_IMAGE_BYTES / (1024 * 1024)) * 10) / 10;
810
+ await sendAndStore(
811
+ sock,
812
+ remoteJid,
813
+ {
814
+ text: `⚠️ A imagem enviada ultrapassa o limite de ${limitMb} MB. Envie uma imagem menor.`,
815
+ },
816
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
817
+ );
818
+ return;
819
+ }
820
+
821
+ if (imageResult.error === 'download_failed') {
822
+ await sendAndStore(
823
+ sock,
824
+ remoteJid,
825
+ { text: '⚠️ Não consegui baixar a imagem. Tente reenviar.' },
826
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
827
+ );
828
+ return;
829
+ }
830
+
831
+ if (!prompt) {
832
+ await sendImageUsage(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
833
+ return;
834
+ }
835
+
836
+ if (errors.length) {
837
+ await sendAndStore(
838
+ sock,
839
+ remoteJid,
840
+ {
841
+ text: [
842
+ '⚠️ Opções inválidas no comando.',
843
+ `Detalhes: ${errors.join(', ')}`,
844
+ '',
845
+ `Use *${commandPrefix}catimg* sem opções para ver o formato correto.`,
846
+ ].join('\n'),
847
+ },
848
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
849
+ );
850
+ return;
851
+ }
852
+
853
+ const userPrompt = await aiPromptStore.getPrompt(senderJid);
854
+ const userPreference = typeof userPrompt === 'string' ? userPrompt.trim() : '';
855
+ const effectiveSystemPrompt = userPreference || BASE_SYSTEM_PROMPT;
856
+
857
+ const content = [];
858
+ if (prompt) {
859
+ content.push({ type: 'input_text', text: prompt });
860
+ }
861
+ if (imageResult.dataUrl) {
862
+ content.push({ type: 'input_image', image_url: imageResult.dataUrl });
863
+ }
864
+
865
+ const imageTool = { type: 'image_generation', ...toolOptions };
866
+
867
+ const payload = {
868
+ model: OPENAI_IMAGE_MODEL,
869
+ input: [
870
+ {
871
+ role: 'user',
872
+ content,
873
+ },
874
+ ],
875
+ tools: [imageTool],
876
+ tool_choice: { type: 'image_generation' },
877
+ };
878
+
879
+ if (effectiveSystemPrompt) {
880
+ payload.instructions = effectiveSystemPrompt;
881
+ }
882
+
883
+ const sessionKey = buildSessionKey(remoteJid, senderJid, 'image');
884
+ const session = sessionCache.get(sessionKey);
885
+ if (session?.previousResponseId) {
886
+ payload.previous_response_id = session.previousResponseId;
887
+ }
888
+
889
+ try {
890
+ const client = getClient();
891
+ const response = await callOpenAI(
892
+ () => client.responses.create(payload),
893
+ 'responses.create.image',
894
+ OPENAI_IMAGE_TIMEOUT,
895
+ );
896
+ const outputText = response.output_text?.trim();
897
+
898
+ sessionCache.set(sessionKey, {
899
+ previousResponseId: response.id,
900
+ updatedAt: Date.now(),
901
+ });
902
+
903
+ const imageOutputs = Array.isArray(response.output)
904
+ ? response.output.filter((output) => output.type === 'image_generation_call' && output.result)
905
+ : [];
906
+ const imageBase64 = imageOutputs[0]?.result;
907
+
908
+ if (!imageBase64) {
909
+ if (outputText) {
910
+ await sendAndStore(
911
+ sock,
912
+ remoteJid,
913
+ { text: `🖼️ ${outputText}` },
914
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
915
+ );
916
+ return;
917
+ }
918
+
919
+ await sendAndStore(
920
+ sock,
921
+ remoteJid,
922
+ { text: '⚠️ Não consegui gerar a imagem agora. Tente novamente.' },
923
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
924
+ );
925
+ return;
926
+ }
927
+
928
+ const outputFormat = toolOptions.output_format || 'png';
929
+ const mimeByFormat = {
930
+ png: 'image/png',
931
+ jpeg: 'image/jpeg',
932
+ webp: 'image/webp',
933
+ };
934
+ const mimetype = mimeByFormat[outputFormat] || 'image/png';
935
+ const imageBuffer = Buffer.from(imageBase64, 'base64');
936
+ const caption = outputText ? `🖼️ ${outputText}` : '🖼️ Imagem gerada.';
937
+
938
+ await sendAndStore(
939
+ sock,
940
+ remoteJid,
941
+ { image: imageBuffer, caption, mimetype },
942
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
943
+ );
944
+ } catch (error) {
945
+ logger.error('handleCatImageCommand: erro ao chamar OpenAI.', error);
946
+ await sendAndStore(
947
+ sock,
948
+ remoteJid,
949
+ {
950
+ text: ['❌ *Erro ao falar com a IA*', 'Tente novamente em alguns instantes.'].join('\n'),
951
+ },
952
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
953
+ );
954
+ }
955
+ }
956
+
957
+ export async function handleCatPromptCommand({
958
+ sock,
959
+ remoteJid,
960
+ messageInfo,
961
+ expirationMessage,
962
+ senderJid,
963
+ text,
964
+ commandPrefix = DEFAULT_COMMAND_PREFIX,
965
+ }) {
966
+ const promptText = text?.trim();
967
+ if (!promptText) {
968
+ await sendPromptUsage(sock, remoteJid, messageInfo, expirationMessage, commandPrefix);
969
+ return;
970
+ }
971
+
972
+ if (!(await isPremiumAllowed(senderJid))) {
973
+ await sendPremiumOnly(sock, remoteJid, messageInfo, expirationMessage);
974
+ return;
975
+ }
976
+
977
+ const lower = promptText.toLowerCase();
978
+ if (lower === 'reset' || lower === 'default' || lower === 'padrao' || lower === 'padrão') {
979
+ await aiPromptStore.clearPrompt(senderJid);
980
+ await sendAndStore(
981
+ sock,
982
+ remoteJid,
983
+ { text: '✅ Prompt da IA restaurado para o padrão.' },
984
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
985
+ );
986
+ return;
987
+ }
988
+
989
+ if (promptText.length > 2000) {
990
+ await sendAndStore(
991
+ sock,
992
+ remoteJid,
993
+ { text: '⚠️ Prompt muito longo. Limite: 2000 caracteres.' },
994
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
995
+ );
996
+ return;
997
+ }
998
+
999
+ await aiPromptStore.setPrompt(senderJid, promptText);
1000
+ await sendAndStore(
1001
+ sock,
1002
+ remoteJid,
1003
+ { text: '✅ Prompt da IA atualizado para você.' },
1004
+ { quoted: messageInfo, ephemeralExpiration: expirationMessage },
1005
+ );
1006
+ }