@omnizap-system/omnizap 2.5.12

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 (425) hide show
  1. package/.clusterfuzzlite/Dockerfile +10 -0
  2. package/.env.example +907 -0
  3. package/.github/codeql/codeql-config.yml +10 -0
  4. package/.github/dependabot.yml +35 -0
  5. package/.github/workflows/ci.yml +73 -0
  6. package/.github/workflows/codeql.yml +106 -0
  7. package/.github/workflows/db-migration-check.yml +98 -0
  8. package/.github/workflows/dependency-review.yml +22 -0
  9. package/.github/workflows/deploy.yml +95 -0
  10. package/.github/workflows/release.yml +106 -0
  11. package/.github/workflows/security-attest-provenance.yml +51 -0
  12. package/.github/workflows/security-gitleaks.yml +34 -0
  13. package/.github/workflows/security-runner-hardening.yml +31 -0
  14. package/.github/workflows/security-scorecard.yml +44 -0
  15. package/.github/workflows/security-zap-baseline.yml +44 -0
  16. package/.github/workflows/security-zap-full-scan.yml +43 -0
  17. package/.github/workflows/security-zizmor.yml +36 -0
  18. package/.github/workflows/wiki-sync.yml +44 -0
  19. package/.gitleaks.toml +15 -0
  20. package/.prettierrc +34 -0
  21. package/CODE_OF_CONDUCT.md +114 -0
  22. package/LICENSE +56 -0
  23. package/README.md +110 -0
  24. package/SECURITY.md +110 -0
  25. package/app/config/index.js +4 -0
  26. package/app/configParts/adminIdentity.js +92 -0
  27. package/app/configParts/baileysConfig.js +1818 -0
  28. package/app/configParts/groupUtils.js +692 -0
  29. package/app/configParts/loggerConfig.js +394 -0
  30. package/app/configParts/messagePersistenceService.js +305 -0
  31. package/app/connection/baileysCompatibility.test.js +40 -0
  32. package/app/connection/baileysDbAuthState.js +344 -0
  33. package/app/connection/socketController.js +2243 -0
  34. package/app/controllers/messageController.js +7 -0
  35. package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
  36. package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
  37. package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
  38. package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
  39. package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
  40. package/app/controllers/messageProcessingPipeline.js +699 -0
  41. package/app/modules/adminModule/AGENT.md +4056 -0
  42. package/app/modules/adminModule/adminAiHelpService.js +56 -0
  43. package/app/modules/adminModule/adminConfigRuntime.js +177 -0
  44. package/app/modules/adminModule/commandConfig.json +7122 -0
  45. package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
  46. package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
  47. package/app/modules/adminModule/groupEventHandlers.js +399 -0
  48. package/app/modules/aiModule/AGENT.md +547 -0
  49. package/app/modules/aiModule/aiAiHelpService.js +14 -0
  50. package/app/modules/aiModule/aiConfigRuntime.js +135 -0
  51. package/app/modules/aiModule/catCommand.js +967 -0
  52. package/app/modules/aiModule/commandConfig.json +981 -0
  53. package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
  54. package/app/modules/gameModule/AGENT.md +196 -0
  55. package/app/modules/gameModule/commandConfig.json +366 -0
  56. package/app/modules/gameModule/diceCommand.js +42 -0
  57. package/app/modules/gameModule/gameAiHelpService.js +14 -0
  58. package/app/modules/gameModule/gameConfigRuntime.js +68 -0
  59. package/app/modules/menuModule/AGENT.md +205 -0
  60. package/app/modules/menuModule/commandConfig.json +366 -0
  61. package/app/modules/menuModule/common.js +316 -0
  62. package/app/modules/menuModule/menuAiHelpService.js +14 -0
  63. package/app/modules/menuModule/menuConfigRuntime.js +68 -0
  64. package/app/modules/menuModule/menus.js +66 -0
  65. package/app/modules/playModule/AGENT.md +321 -0
  66. package/app/modules/playModule/commandConfig.json +584 -0
  67. package/app/modules/playModule/playAiHelpService.js +14 -0
  68. package/app/modules/playModule/playCommand.js +1417 -0
  69. package/app/modules/playModule/playConfigRuntime.js +68 -0
  70. package/app/modules/quoteModule/AGENT.md +199 -0
  71. package/app/modules/quoteModule/commandConfig.json +366 -0
  72. package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
  73. package/app/modules/quoteModule/quoteCommand.js +842 -0
  74. package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
  75. package/app/modules/rpgPokemonModule/AGENT.md +229 -0
  76. package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
  77. package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
  78. package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
  79. package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
  80. package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
  81. package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
  82. package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
  83. package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
  84. package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
  85. package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
  86. package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
  87. package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
  88. package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
  89. package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
  90. package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
  91. package/app/modules/statsModule/AGENT.md +320 -0
  92. package/app/modules/statsModule/commandConfig.json +540 -0
  93. package/app/modules/statsModule/globalRankingCommand.js +64 -0
  94. package/app/modules/statsModule/rankingCommand.js +41 -0
  95. package/app/modules/statsModule/rankingCommon.js +1305 -0
  96. package/app/modules/statsModule/statsAiHelpService.js +14 -0
  97. package/app/modules/statsModule/statsConfigRuntime.js +68 -0
  98. package/app/modules/stickerModule/AGENT.md +692 -0
  99. package/app/modules/stickerModule/addStickerMetadata.js +239 -0
  100. package/app/modules/stickerModule/commandConfig.json +1216 -0
  101. package/app/modules/stickerModule/convertToWebp.js +367 -0
  102. package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
  103. package/app/modules/stickerModule/stickerCommand.js +446 -0
  104. package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
  105. package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
  106. package/app/modules/stickerModule/stickerTextCommand.js +653 -0
  107. package/app/modules/stickerPackModule/AGENT.md +215 -0
  108. package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
  109. package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
  110. package/app/modules/stickerPackModule/commandConfig.json +387 -0
  111. package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
  112. package/app/modules/stickerPackModule/domainEvents.js +52 -0
  113. package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
  114. package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
  115. package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
  116. package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
  117. package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
  118. package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
  119. package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
  120. package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
  121. package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
  122. package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
  123. package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
  124. package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
  125. package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
  126. package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
  127. package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
  128. package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
  129. package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
  130. package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
  131. package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
  132. package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
  133. package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
  134. package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
  135. package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
  136. package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
  137. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
  138. package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
  139. package/app/modules/stickerPackModule/stickerPackService.js +733 -0
  140. package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
  141. package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
  142. package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
  143. package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
  144. package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
  145. package/app/modules/systemMetricsModule/AGENT.md +193 -0
  146. package/app/modules/systemMetricsModule/commandConfig.json +344 -0
  147. package/app/modules/systemMetricsModule/pingCommand.js +399 -0
  148. package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
  149. package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
  150. package/app/modules/tiktokModule/AGENT.md +196 -0
  151. package/app/modules/tiktokModule/commandConfig.json +366 -0
  152. package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
  153. package/app/modules/tiktokModule/tiktokCommand.js +716 -0
  154. package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
  155. package/app/modules/userModule/AGENT.md +200 -0
  156. package/app/modules/userModule/commandConfig.json +386 -0
  157. package/app/modules/userModule/userAiHelpService.js +14 -0
  158. package/app/modules/userModule/userCommand.js +1155 -0
  159. package/app/modules/userModule/userConfigRuntime.js +68 -0
  160. package/app/modules/waifuPicsModule/AGENT.md +431 -0
  161. package/app/modules/waifuPicsModule/commandConfig.json +780 -0
  162. package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
  163. package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
  164. package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
  165. package/app/observability/metrics.js +766 -0
  166. package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
  167. package/app/services/ai/aiLearningRepository.js +400 -0
  168. package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
  169. package/app/services/ai/commandConfigEnrichmentService.js +452 -0
  170. package/app/services/ai/commandConfigValidationService.js +443 -0
  171. package/app/services/ai/commandToolBuilderService.js +192 -0
  172. package/app/services/ai/conversationRouterService.js +516 -0
  173. package/app/services/ai/geminiService.js +115 -0
  174. package/app/services/ai/geminiService.test.js +87 -0
  175. package/app/services/ai/globalModuleAiHelpService.js +1412 -0
  176. package/app/services/ai/globalToolCallingService.js +203 -0
  177. package/app/services/ai/messageCommandExecutionService.js +391 -0
  178. package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
  179. package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
  180. package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
  181. package/app/services/ai/moduleToolExecutorService.js +464 -0
  182. package/app/services/ai/moduleToolRegistryService.js +178 -0
  183. package/app/services/ai/toolCandidateSelectorService.js +781 -0
  184. package/app/services/auth/googleWebLinkService.js +80 -0
  185. package/app/services/auth/whatsappLoginLinkService.js +230 -0
  186. package/app/services/external/pokeApiService.js +398 -0
  187. package/app/services/group/groupMetadataService.js +311 -0
  188. package/app/services/infra/dbWriteQueue.js +874 -0
  189. package/app/services/infra/featureFlagService.js +131 -0
  190. package/app/services/infra/queueUtils.js +55 -0
  191. package/app/services/messaging/captchaService.js +491 -0
  192. package/app/services/messaging/messagePersistenceService.js +1 -0
  193. package/app/services/messaging/newsBroadcastService.js +347 -0
  194. package/app/services/sticker/stickerFocusService.js +347 -0
  195. package/app/services/sticker/stickerFocusService.test.js +43 -0
  196. package/app/store/aiPromptStore.js +38 -0
  197. package/app/store/conversationSessionStore.js +131 -0
  198. package/app/store/groupConfigStore.js +58 -0
  199. package/app/store/premiumUserStore.js +54 -0
  200. package/app/utils/antiLink/antiLinkModule.js +700 -0
  201. package/app/utils/http/getImageBufferModule.js +18 -0
  202. package/app/utils/json/jsonSanitizer.js +113 -0
  203. package/app/utils/json/jsonSanitizer.test.js +40 -0
  204. package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
  205. package/app/workers/aiLearningWorker.js +605 -0
  206. package/app/workers/commandConfigEnrichmentWorker.js +242 -0
  207. package/database/index.js +2075 -0
  208. package/database/init.js +151 -0
  209. package/database/migrations/.gitkeep +0 -0
  210. package/database/migrations/20260307_d0_hardening_down.sql +64 -0
  211. package/database/migrations/20260307_d0_hardening_up.sql +79 -0
  212. package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
  213. package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
  214. package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
  215. package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
  216. package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
  217. package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
  218. package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
  219. package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
  220. package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
  221. package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
  222. package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
  223. package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
  224. package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
  225. package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
  226. package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
  227. package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
  228. package/database/schema.sql +1186 -0
  229. package/docker-compose.yml +104 -0
  230. package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
  231. package/docs/audits/stickerCatalogController-symbols.md +58 -0
  232. package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
  233. package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
  234. package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
  235. package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
  236. package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
  237. package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
  238. package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
  239. package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
  240. package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
  241. package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
  242. package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
  243. package/docs/seo/satellite-page-template.md +116 -0
  244. package/docs/seo/satellite-pages-phase1.json +364 -0
  245. package/docs/wiki/Home.md +120 -0
  246. package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
  247. package/docs/wiki/recent-changes-2026-03-08.md +47 -0
  248. package/ecosystem.prod.config.cjs +135 -0
  249. package/eslint.config.js +89 -0
  250. package/index.js +488 -0
  251. package/ml/clip_classifier/Dockerfile +18 -0
  252. package/ml/clip_classifier/README.md +118 -0
  253. package/ml/clip_classifier/adaptive_scoring.py +40 -0
  254. package/ml/clip_classifier/classifier.py +654 -0
  255. package/ml/clip_classifier/embedding_store.py +481 -0
  256. package/ml/clip_classifier/env_loader.py +15 -0
  257. package/ml/clip_classifier/llm_label_expander.py +144 -0
  258. package/ml/clip_classifier/main.py +213 -0
  259. package/ml/clip_classifier/requirements.txt +10 -0
  260. package/ml/clip_classifier/similarity_engine.py +74 -0
  261. package/new-logo.png +0 -0
  262. package/observability/alert-rules.yml +60 -0
  263. package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
  264. package/observability/grafana/dashboards/omnizap-overview.json +170 -0
  265. package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
  266. package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
  267. package/observability/loki-config.yml +38 -0
  268. package/observability/mysql-setup.sql +46 -0
  269. package/observability/prometheus.yml +35 -0
  270. package/observability/promtail-config.yml +84 -0
  271. package/observability/sticker-catalog-slo.md +83 -0
  272. package/observability/sticker-scale-hardening-rollout.md +128 -0
  273. package/package.json +144 -0
  274. package/public/apple-touch-icon.png +0 -0
  275. package/public/assets/css/commands-react.input.css +71 -0
  276. package/public/assets/css/create-pack-react.input.css +31 -0
  277. package/public/assets/css/home-react.input.css +106 -0
  278. package/public/assets/css/login-react.input.css +58 -0
  279. package/public/assets/css/stickers-react.input.css +18 -0
  280. package/public/assets/css/terms-react.input.css +115 -0
  281. package/public/assets/css/user-react.input.css +57 -0
  282. package/public/assets/images/brand-icon-192.png +0 -0
  283. package/public/assets/images/brand-logo-128.webp +0 -0
  284. package/public/assets/images/hero-banner-1280.jpg +0 -0
  285. package/public/comandos/commands-catalog.json +4517 -0
  286. package/public/css/api-docs.css +161 -0
  287. package/public/css/stickers-admin.css +1288 -0
  288. package/public/css/styles.css +679 -0
  289. package/public/css/systemadm/admin.css +474 -0
  290. package/public/css/systemadm/base.css +73 -0
  291. package/public/css/systemadm/components.css +662 -0
  292. package/public/css/systemadm/layout.css +229 -0
  293. package/public/css/systemadm/tokens.css +56 -0
  294. package/public/favicon-16x16.png +0 -0
  295. package/public/favicon-32x32.png +0 -0
  296. package/public/favicon.ico +0 -0
  297. package/public/js/apps/apiDocsApp.js +235 -0
  298. package/public/js/apps/commandsReactApp.js +528 -0
  299. package/public/js/apps/createPackApp.js +1646 -0
  300. package/public/js/apps/homeReactApp.js +942 -0
  301. package/public/js/apps/loginReactApp.js +496 -0
  302. package/public/js/apps/stickersAdminApp.js +1753 -0
  303. package/public/js/apps/stickersApp.js +3797 -0
  304. package/public/js/apps/termsReactApp.js +528 -0
  305. package/public/js/apps/userApp.js +2540 -0
  306. package/public/js/apps/userProfile/actions.js +66 -0
  307. package/public/js/apps/userReactApp.js +547 -0
  308. package/public/js/catalog.js +950 -0
  309. package/public/pages/api-docs.html +40 -0
  310. package/public/pages/aup.html +158 -0
  311. package/public/pages/comandos.html +41 -0
  312. package/public/pages/dpa.html +227 -0
  313. package/public/pages/home.html +45 -0
  314. package/public/pages/licenca.html +182 -0
  315. package/public/pages/login.html +40 -0
  316. package/public/pages/notice-and-takedown.html +234 -0
  317. package/public/pages/politica-de-privacidade.html +251 -0
  318. package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
  319. package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
  320. package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
  321. package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
  322. package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
  323. package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
  324. package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
  325. package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
  326. package/public/pages/stickers-admin.html +31 -0
  327. package/public/pages/stickers-create.html +41 -0
  328. package/public/pages/stickers.html +45 -0
  329. package/public/pages/suboperadores.html +237 -0
  330. package/public/pages/termos-de-uso-texto-integral.html +241 -0
  331. package/public/pages/termos-de-uso.html +41 -0
  332. package/public/pages/user-password-reset.html +32 -0
  333. package/public/pages/user-systemadm.html +508 -0
  334. package/public/pages/user.html +39 -0
  335. package/public/robots.txt +9 -0
  336. package/public/site.webmanifest +24 -0
  337. package/public/sitemap.xml +98 -0
  338. package/schemas/command-config.schema.json +582 -0
  339. package/scripts/baileys-compat-smoke.mjs +12 -0
  340. package/scripts/cache-bust.mjs +142 -0
  341. package/scripts/deploy.sh +916 -0
  342. package/scripts/email-broadcast-terms-update.mjs +170 -0
  343. package/scripts/enrich-command-discovery-fields.mjs +286 -0
  344. package/scripts/generate-command-config-schema.mjs +273 -0
  345. package/scripts/generate-commands-catalog.mjs +308 -0
  346. package/scripts/generate-module-agents.mjs +631 -0
  347. package/scripts/generate-seo-satellite-pages.mjs +400 -0
  348. package/scripts/github-deploy-notify.mjs +174 -0
  349. package/scripts/github-release-notify.mjs +219 -0
  350. package/scripts/release.sh +599 -0
  351. package/scripts/run-codeql-local.sh +116 -0
  352. package/scripts/run-prettier-all.mjs +25 -0
  353. package/scripts/security-smoketest.mjs +581 -0
  354. package/scripts/sticker-catalog-loadtest.mjs +210 -0
  355. package/scripts/sticker-worker-task.mjs +119 -0
  356. package/scripts/sync-readme-snapshot.mjs +133 -0
  357. package/scripts/validate-command-config-schema.mjs +130 -0
  358. package/scripts/validate-command-configs.mjs +15 -0
  359. package/scripts/wiki-sync.sh +191 -0
  360. package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
  361. package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
  362. package/server/auth/jwt/webJwtService.js +147 -0
  363. package/server/auth/stickerCatalogAuthContext.js +165 -0
  364. package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
  365. package/server/auth/userPassword/index.js +14 -0
  366. package/server/auth/userPassword/userPasswordAuthService.js +422 -0
  367. package/server/auth/userPassword/userPasswordCrypto.js +199 -0
  368. package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
  369. package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
  370. package/server/auth/validation/authSchemas.js +236 -0
  371. package/server/auth/webAccount/webAccountHandlers.js +1434 -0
  372. package/server/controllers/admin/adminBanService.js +138 -0
  373. package/server/controllers/admin/adminPanelHandlers.js +2083 -0
  374. package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
  375. package/server/controllers/admin/systemAdminController.js +201 -0
  376. package/server/controllers/email/emailAutomationController.js +239 -0
  377. package/server/controllers/metricsController.js +21 -0
  378. package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
  379. package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
  380. package/server/controllers/sticker/stickerCatalogController.js +4700 -0
  381. package/server/controllers/system/contactController.js +115 -0
  382. package/server/controllers/system/githubController.js +137 -0
  383. package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
  384. package/server/controllers/system/storageController.js +154 -0
  385. package/server/controllers/system/systemController.js +135 -0
  386. package/server/controllers/system/systemMetricsController.js +156 -0
  387. package/server/controllers/system/visitController.js +90 -0
  388. package/server/controllers/userController.js +145 -0
  389. package/server/email/emailAutomationRuntime.js +225 -0
  390. package/server/email/emailAutomationService.js +125 -0
  391. package/server/email/emailOutboxRepository.js +282 -0
  392. package/server/email/emailTemplateService.js +480 -0
  393. package/server/email/emailTransportService.js +156 -0
  394. package/server/http/clientIp.js +95 -0
  395. package/server/http/httpRequestUtils.js +262 -0
  396. package/server/http/httpRequestUtils.test.js +80 -0
  397. package/server/http/httpServer.js +180 -0
  398. package/server/http/requestContext.js +20 -0
  399. package/server/http/siteRoutingUtils.js +87 -0
  400. package/server/index.js +1 -0
  401. package/server/middleware/cachePolicy.js +26 -0
  402. package/server/middleware/cachePolicyHelpers.js +1 -0
  403. package/server/middleware/endpointRateLimit.js +181 -0
  404. package/server/middleware/rateLimit.js +70 -0
  405. package/server/middleware/requireAdminAuth.js +48 -0
  406. package/server/middleware/securityHeaders.js +97 -0
  407. package/server/routes/admin/systemAdminRouter.js +64 -0
  408. package/server/routes/email/emailAutomationRouter.js +46 -0
  409. package/server/routes/health/healthRouter.js +41 -0
  410. package/server/routes/indexRouter.js +234 -0
  411. package/server/routes/metrics/metricsRouter.js +58 -0
  412. package/server/routes/static/staticPageRouter.js +134 -0
  413. package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
  414. package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
  415. package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
  416. package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
  417. package/server/routes/sticker/catalogRouter.js +77 -0
  418. package/server/routes/sticker/stickerApiRouter.js +84 -0
  419. package/server/routes/sticker/stickerDataRouter.js +145 -0
  420. package/server/routes/sticker/stickerSiteRouter.js +43 -0
  421. package/server/routes/user/userApiPaths.js +66 -0
  422. package/server/routes/user/userRouter.js +65 -0
  423. package/server/utils/safePath.js +26 -0
  424. package/utils/logger/loggerModule.js +35 -0
  425. package/vite.config.mjs +38 -0
@@ -0,0 +1,1099 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import OpenAI from 'openai';
4
+ import { getAiHelpCachedResponse, upsertAiHelpCachedResponse } from './aiHelpResponseCacheRepository.js';
5
+ import { createGeminiTextService, DEFAULT_GEMINI_MODEL } from './geminiService.js';
6
+
7
+ const DEFAULT_FAQ_INTERVAL_MS = 6 * 60 * 60 * 1000;
8
+ const DEFAULT_MAX_RESPONSE_CHARS = 3400;
9
+ const DEFAULT_MAX_AGENT_CONTEXT_CHARS = 12000;
10
+ const DEFAULT_TIMEOUT_MS = 25000;
11
+ const DEFAULT_OPENAI_MODEL = 'gpt-5-nano';
12
+ const CACHE_SCOPE_QUESTION = 'question';
13
+ const CACHE_SCOPE_COMMAND_EXPLAIN = 'command_explain';
14
+
15
+ const normalizeText = (value) =>
16
+ String(value || '')
17
+ .trim()
18
+ .toLowerCase()
19
+ .normalize('NFD')
20
+ .replace(/[\u0300-\u036f]/g, '')
21
+ .replace(/[^a-z0-9\s/_.-]/g, ' ')
22
+ .replace(/\s+/g, ' ')
23
+ .trim();
24
+
25
+ const defaultLogger = {
26
+ info: (...args) => console.info(...args),
27
+ warn: (...args) => console.warn(...args),
28
+ error: (...args) => console.error(...args),
29
+ debug: (...args) => console.debug(...args),
30
+ };
31
+
32
+ const ensureArray = (value) => (Array.isArray(value) ? value.filter(Boolean) : []);
33
+ const ensureObject = (value) => (value && typeof value === 'object' ? value : {});
34
+ const pickFirstText = (...values) => {
35
+ for (const value of values) {
36
+ const text = String(value ?? '').trim();
37
+ if (text) return text;
38
+ }
39
+ return '';
40
+ };
41
+ const pickFirstBoolean = (...values) => {
42
+ for (const value of values) {
43
+ if (typeof value === 'boolean') return value;
44
+ }
45
+ return false;
46
+ };
47
+
48
+ const formatPermissionLabel = (permission) => String(permission || 'nao definido').trim();
49
+
50
+ const formatWhereLabel = (contexts = []) => {
51
+ if (!Array.isArray(contexts) || contexts.length === 0) return 'nao definido';
52
+ return contexts.join(', ');
53
+ };
54
+
55
+ const readEntryDescription = (entry = {}) => pickFirstText(entry?.description, entry?.docs?.summary, entry?.descricao);
56
+
57
+ const readEntryUsage = (entry = {}) => {
58
+ const usageV2 = ensureArray(entry?.usage);
59
+ if (usageV2.length) return usageV2;
60
+ const docsUsage = ensureArray(entry?.docs?.usage_examples);
61
+ if (docsUsage.length) return docsUsage;
62
+ return ensureArray(entry?.metodos_de_uso);
63
+ };
64
+
65
+ const readEntryPermission = (entry = {}) => pickFirstText(entry?.permission, entry?.permissao_necessaria);
66
+
67
+ const readEntryContexts = (entry = {}) => {
68
+ const contextsV2 = ensureArray(entry?.contexts);
69
+ if (contextsV2.length) return contextsV2;
70
+ return ensureArray(entry?.local_de_uso);
71
+ };
72
+
73
+ const readEntryUsageLimit = (entry = {}) => pickFirstText(entry?.limits?.usage_description, entry?.limite_de_uso);
74
+
75
+ const readEntryRequirements = (entry = {}) => {
76
+ const requirements = ensureObject(entry?.requirements);
77
+ const requirementsLegacy = ensureObject(requirements?.legacy);
78
+ const preConditions = ensureObject(entry?.pre_condicoes);
79
+
80
+ return {
81
+ require_group: pickFirstBoolean(requirements.require_group, requirements.requer_grupo, requirementsLegacy.require_group, requirementsLegacy.requer_grupo, preConditions.requer_grupo),
82
+ require_group_admin: pickFirstBoolean(requirements.require_group_admin, requirements.requer_admin, requirementsLegacy.require_group_admin, requirementsLegacy.requer_admin, preConditions.requer_admin),
83
+ require_bot_owner: pickFirstBoolean(requirements.require_bot_owner, requirements.requer_admin_principal, requirementsLegacy.require_bot_owner, requirementsLegacy.requer_admin_principal, preConditions.requer_admin_principal),
84
+ require_google_login: pickFirstBoolean(requirements.require_google_login, requirements.requer_google_login, requirementsLegacy.require_google_login, requirementsLegacy.requer_google_login, preConditions.requer_google_login),
85
+ require_nsfw_enabled: pickFirstBoolean(requirements.require_nsfw_enabled, requirements.requer_nsfw, requirementsLegacy.require_nsfw_enabled, requirementsLegacy.requer_nsfw, preConditions.requer_nsfw),
86
+ require_media: pickFirstBoolean(requirements.require_media, requirements.requer_midia, requirementsLegacy.require_media, requirementsLegacy.requer_midia, preConditions.requer_midia),
87
+ require_reply_message: pickFirstBoolean(requirements.require_reply_message, requirements.requer_mensagem_respondida, requirementsLegacy.require_reply_message, requirementsLegacy.requer_mensagem_respondida, preConditions.requer_mensagem_respondida),
88
+ };
89
+ };
90
+
91
+ const formatPreConditions = (requirements = {}) => {
92
+ const lines = [];
93
+ if (requirements.require_group) lines.push('- Requer ser executado em grupo.');
94
+ if (requirements.require_group_admin) lines.push('- Requer permissao de admin do grupo.');
95
+ if (requirements.require_bot_owner) lines.push('- Requer admin principal do bot.');
96
+ if (requirements.require_google_login) lines.push('- Pode requerer login vinculado ao site.');
97
+ if (requirements.require_nsfw_enabled) lines.push('- Requer NSFW ativo quando aplicavel.');
98
+ if (requirements.require_media) lines.push('- Requer midia anexada/citada quando aplicavel.');
99
+ if (requirements.require_reply_message) lines.push('- Requer resposta/citacao de mensagem quando aplicavel.');
100
+ if (lines.length === 0) lines.push('- Sem pre-condicoes explicitas no modulo.');
101
+ return lines;
102
+ };
103
+
104
+ const evaluatePreConditions = (entry, context = {}) => {
105
+ const requirements = readEntryRequirements(entry);
106
+ const reasons = [];
107
+
108
+ if (requirements.require_group && !context.isGroupMessage) {
109
+ reasons.push('este comando exige uso em grupo');
110
+ }
111
+ if (requirements.require_group_admin && context.isSenderAdmin === false) {
112
+ reasons.push('este comando exige permissao de admin do grupo');
113
+ }
114
+ if (requirements.require_bot_owner && context.isSenderOwner === false) {
115
+ reasons.push('este comando exige admin principal do bot');
116
+ }
117
+ if (requirements.require_nsfw_enabled && context.groupNsfwEnabled === false) {
118
+ reasons.push('NSFW precisa estar ativo no grupo');
119
+ }
120
+
121
+ return {
122
+ allowed: reasons.length === 0,
123
+ reasons,
124
+ };
125
+ };
126
+
127
+ const levenshteinDistance = (left, right) => {
128
+ const a = normalizeText(left);
129
+ const b = normalizeText(right);
130
+ if (!a) return b.length;
131
+ if (!b) return a.length;
132
+
133
+ const matrix = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
134
+ for (let i = 0; i <= a.length; i += 1) matrix[i][0] = i;
135
+ for (let j = 0; j <= b.length; j += 1) matrix[0][j] = j;
136
+
137
+ for (let i = 1; i <= a.length; i += 1) {
138
+ for (let j = 1; j <= b.length; j += 1) {
139
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
140
+ matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
141
+ }
142
+ }
143
+
144
+ return matrix[a.length][b.length];
145
+ };
146
+
147
+ const renderUsage = (method, commandPrefix = '/') => String(method || '').replaceAll('<prefix>', String(commandPrefix || '/'));
148
+
149
+ const toPositiveInt = (value, fallback, min = 1) => {
150
+ const parsed = Number.parseInt(String(value ?? ''), 10);
151
+ if (!Number.isFinite(parsed) || parsed < min) return fallback;
152
+ return parsed;
153
+ };
154
+
155
+ const clampText = (value, maxChars) => {
156
+ const text = String(value || '').trim();
157
+ if (!text) return '';
158
+ if (text.length <= maxChars) return text;
159
+ return `${text.slice(0, Math.max(0, maxChars - 20)).trimEnd()}\n\n[resposta truncada]`;
160
+ };
161
+
162
+ const normalizeLlmProvider = (value, fallback = 'gemini') => {
163
+ const normalized = String(value || '')
164
+ .trim()
165
+ .toLowerCase();
166
+ if (normalized === 'gemini') return 'gemini';
167
+ if (normalized === 'openai') return 'openai';
168
+ return fallback;
169
+ };
170
+
171
+ const looksLikeGeminiModel = (value) =>
172
+ String(value || '')
173
+ .trim()
174
+ .toLowerCase()
175
+ .includes('gemini');
176
+
177
+ const looksLikeOpenAiModel = (value) => {
178
+ const normalized = String(value || '')
179
+ .trim()
180
+ .toLowerCase();
181
+ if (!normalized) return false;
182
+ return normalized.startsWith('gpt-') || normalized.startsWith('o1') || normalized.startsWith('o3') || normalized.startsWith('o4') || normalized.startsWith('text-');
183
+ };
184
+
185
+ const normalizeCacheSource = (value) => {
186
+ const base = String(value || 'deterministic')
187
+ .trim()
188
+ .toLowerCase()
189
+ .replace(/^db_/, '')
190
+ .replace(/[^a-z0-9_-]/g, '_')
191
+ .replace(/_+/g, '_')
192
+ .replace(/^_+|_+$/g, '');
193
+
194
+ return (base || 'deterministic').slice(0, 32);
195
+ };
196
+
197
+ const buildCommandExplainCacheKey = (commandName) => `explicar comando ${normalizeText(commandName).replace(/^\/+/, '')}`;
198
+
199
+ const defaultGuidance = {
200
+ faqSummary: ({ commandCount, faqCount, commandPrefix }) => ['🤖 FAQ atualizada.', `Comandos analisados: ${commandCount}.`, `Perguntas geradas: ${faqCount}.`, '', `Use ${commandPrefix}help <comando> para explicacao detalhada.`, `Use ${commandPrefix}ask <pergunta> para consulta livre.`].join('\n'),
201
+ helpUsage: ({ commandPrefix }) => `Use ${commandPrefix}help <comando>.`,
202
+ askUsage: ({ commandPrefix }) => `Use ${commandPrefix}ask <pergunta>.`,
203
+ unknownCommand: ({ rawCommand, suggestions, commandPrefix }) => [`❓ O comando *${rawCommand}* nao foi encontrado.`, suggestions ? `Talvez voce quis usar: ${suggestions}.` : '', `Use ${commandPrefix}help <comando> ou ${commandPrefix}faq para listar opcoes.`].filter(Boolean).join('\n'),
204
+ missingCommandText: ({ commandPrefix }) => `Nao encontrei esse comando. Use ${commandPrefix}faq para listar opcoes.`,
205
+ questionFallback: ({ commandPrefix, detectedCommand, suggestions }) => {
206
+ if (detectedCommand) {
207
+ return `Posso te ajudar com ${commandPrefix}${detectedCommand}. Tente: ${commandPrefix}help ${detectedCommand}`;
208
+ }
209
+ return ['Nao encontrei resposta pronta para essa pergunta no FAQ.', `Tente ${commandPrefix}ask "como usar <comando>" ou ${commandPrefix}help <comando>.`, suggestions ? `Sugestoes rapidas: ${suggestions}.` : ''].filter(Boolean).join('\n');
210
+ },
211
+ };
212
+
213
+ export const createModuleAiHelpService = ({ moduleKey, moduleLabel = 'modulo', envPrefix = 'MODULE_AI_HELP', getModuleConfig, resolveCommandName, getCommandEntry, listEnabledCommands, agentMdPath, logger = defaultLogger, guidance = {} }) => {
214
+ if (typeof getModuleConfig !== 'function') {
215
+ throw new Error('createModuleAiHelpService: getModuleConfig e obrigatorio');
216
+ }
217
+ if (typeof resolveCommandName !== 'function') {
218
+ throw new Error('createModuleAiHelpService: resolveCommandName e obrigatorio');
219
+ }
220
+ if (typeof getCommandEntry !== 'function') {
221
+ throw new Error('createModuleAiHelpService: getCommandEntry e obrigatorio');
222
+ }
223
+ if (typeof listEnabledCommands !== 'function') {
224
+ throw new Error('createModuleAiHelpService: listEnabledCommands e obrigatorio');
225
+ }
226
+
227
+ const guidanceFns = {
228
+ ...defaultGuidance,
229
+ ...(guidance || {}),
230
+ };
231
+
232
+ const envValue = (name) => process.env[`${envPrefix}_${name}`];
233
+
234
+ const getAiHelpConfig = () => {
235
+ const moduleConfig = getModuleConfig();
236
+ const aiHelp = moduleConfig?.ai_help && typeof moduleConfig.ai_help === 'object' ? moduleConfig.ai_help : {};
237
+ const faq = aiHelp?.faq && typeof aiHelp.faq === 'object' ? aiHelp.faq : {};
238
+ const llm = aiHelp?.llm && typeof aiHelp.llm === 'object' ? aiHelp.llm : {};
239
+
240
+ const cachePathValue = String(faq.cache_file || '').trim();
241
+ const cachePath = cachePathValue ? path.resolve(process.cwd(), cachePathValue) : path.join(process.cwd(), 'data', 'cache', `${moduleKey}-ai-faq-cache.json`);
242
+
243
+ const provider = normalizeLlmProvider(envValue('PROVIDER') || llm.provider || process.env.AI_HELP_LLM_PROVIDER, process.env.GEMINI_API_KEY ? 'gemini' : 'openai');
244
+ const defaultModelByProvider = provider === 'gemini' ? process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL : process.env.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
245
+ const rawModel = String(envValue('MODEL') || llm.model || defaultModelByProvider).trim() || defaultModelByProvider;
246
+ const modelFromEnv = String(envValue('MODEL') || '').trim();
247
+ const hasExplicitModelOverride = Boolean(modelFromEnv);
248
+ let resolvedModel = rawModel;
249
+ if (provider === 'gemini' && !hasExplicitModelOverride && !looksLikeGeminiModel(rawModel) && looksLikeOpenAiModel(rawModel)) {
250
+ resolvedModel = process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL;
251
+ }
252
+ if (provider === 'openai' && !hasExplicitModelOverride && looksLikeGeminiModel(rawModel)) {
253
+ resolvedModel = process.env.OPENAI_MODEL || DEFAULT_OPENAI_MODEL;
254
+ }
255
+
256
+ return {
257
+ enabled: aiHelp.enabled !== false,
258
+ faq: {
259
+ intervalMs: Math.max(60_000, toPositiveInt(envValue('FAQ_INTERVAL_MS') || faq.interval_ms, DEFAULT_FAQ_INTERVAL_MS, 60_000)),
260
+ autoGenerateOnStart:
261
+ String(envValue('SCHEDULER_ENABLED') || (faq.auto_generate_on_start === false ? 'false' : 'true'))
262
+ .trim()
263
+ .toLowerCase() !== 'false',
264
+ cachePath,
265
+ },
266
+ llm: {
267
+ enabled:
268
+ llm.enabled !== false &&
269
+ String(envValue('ENABLE_LLM') || 'true')
270
+ .trim()
271
+ .toLowerCase() !== 'false',
272
+ provider,
273
+ model: resolvedModel,
274
+ maxResponseChars: Math.max(400, toPositiveInt(envValue('MAX_RESPONSE_CHARS') || llm.max_response_chars, DEFAULT_MAX_RESPONSE_CHARS, 400)),
275
+ maxAgentContextChars: Math.max(2_000, toPositiveInt(envValue('MAX_AGENT_CONTEXT_CHARS') || llm.max_agent_context_chars, DEFAULT_MAX_AGENT_CONTEXT_CHARS, 2_000)),
276
+ timeoutMs: Math.max(1_000, toPositiveInt(envValue('TIMEOUT_MS') || llm.timeout_ms, DEFAULT_TIMEOUT_MS, 1_000)),
277
+ },
278
+ };
279
+ };
280
+
281
+ const FAQ_CACHE_VERSION = 1;
282
+ let schedulerStarted = false;
283
+ let schedulerHandle = null;
284
+ let cacheWriteChain = Promise.resolve();
285
+ let faqGenerationPromise = null;
286
+ let cachedOpenAIClient = null;
287
+ let cachedGeminiService = null;
288
+
289
+ const createEmptyCache = () => ({
290
+ version: FAQ_CACHE_VERSION,
291
+ updatedAt: null,
292
+ generatedAt: null,
293
+ faqByCommand: {},
294
+ questionCache: {},
295
+ metrics: {
296
+ faq_hits: 0,
297
+ question_hits: 0,
298
+ misses: 0,
299
+ llm_calls: 0,
300
+ llm_errors: 0,
301
+ generated_count: 0,
302
+ unknown_suggestions: 0,
303
+ last_generated_at: null,
304
+ },
305
+ });
306
+
307
+ const ensureCacheDir = async () => {
308
+ const { faq } = getAiHelpConfig();
309
+ await fs.mkdir(path.dirname(faq.cachePath), { recursive: true });
310
+ };
311
+
312
+ const withCacheWrite = async (writer) => {
313
+ cacheWriteChain = cacheWriteChain
314
+ .then(async () => {
315
+ await ensureCacheDir();
316
+ return writer();
317
+ })
318
+ .catch((error) => {
319
+ logger.warn(`${moduleKey}_ai_help: falha ao persistir cache.`, {
320
+ error: error?.message,
321
+ });
322
+ });
323
+
324
+ return cacheWriteChain;
325
+ };
326
+
327
+ const readCache = async () => {
328
+ const { faq } = getAiHelpConfig();
329
+ try {
330
+ const raw = await fs.readFile(faq.cachePath, 'utf8');
331
+ const parsed = JSON.parse(raw);
332
+ if (!parsed || typeof parsed !== 'object') return createEmptyCache();
333
+ return {
334
+ ...createEmptyCache(),
335
+ ...parsed,
336
+ faqByCommand: parsed.faqByCommand && typeof parsed.faqByCommand === 'object' ? parsed.faqByCommand : {},
337
+ questionCache: parsed.questionCache && typeof parsed.questionCache === 'object' ? parsed.questionCache : {},
338
+ metrics: parsed.metrics && typeof parsed.metrics === 'object' ? { ...createEmptyCache().metrics, ...parsed.metrics } : createEmptyCache().metrics,
339
+ };
340
+ } catch {
341
+ return createEmptyCache();
342
+ }
343
+ };
344
+
345
+ const writeCache = async (cache) => {
346
+ const { faq } = getAiHelpConfig();
347
+ await withCacheWrite(async () => {
348
+ await fs.writeFile(faq.cachePath, `${JSON.stringify(cache, null, 2)}\n`, 'utf8');
349
+ });
350
+ };
351
+
352
+ const withFaqGenerationLock = async (generator) => {
353
+ if (faqGenerationPromise) return faqGenerationPromise;
354
+ faqGenerationPromise = generator()
355
+ .catch((error) => {
356
+ logger.error(`${moduleKey}_ai_help: erro ao gerar FAQ.`, {
357
+ error: error?.message,
358
+ });
359
+ return {
360
+ ok: false,
361
+ error: error?.message || 'faq_generation_failed',
362
+ };
363
+ })
364
+ .finally(() => {
365
+ faqGenerationPromise = null;
366
+ });
367
+ return faqGenerationPromise;
368
+ };
369
+
370
+ const buildCommandFaqItems = (entry, commandPrefix = '/') => {
371
+ const commandToken = `${commandPrefix}${entry.name}`;
372
+ const usageLines = readEntryUsage(entry).map((method) => renderUsage(method, commandPrefix));
373
+ const firstUsage = usageLines[0] || commandToken;
374
+ const whereLabel = formatWhereLabel(readEntryContexts(entry));
375
+ const permissionLabel = formatPermissionLabel(readEntryPermission(entry));
376
+
377
+ return [
378
+ {
379
+ question: `Como usar ${commandToken}?`,
380
+ answer: [`Use ${commandToken} desta forma:`, ...usageLines.map((line) => `- ${line}`), '', `Permissao: ${permissionLabel}.`, `Local de uso: ${whereLabel}.`, 'A IA apenas orienta; nao executa comando.'].join('\n'),
381
+ source: 'deterministic',
382
+ },
383
+ {
384
+ question: `Quem pode usar ${commandToken}?`,
385
+ answer: [`${commandToken} exige: ${permissionLabel}.`, `Onde pode ser usado: ${whereLabel}.`, `Uso base: ${firstUsage}.`].join('\n'),
386
+ source: 'deterministic',
387
+ },
388
+ {
389
+ question: `Onde posso usar ${commandToken}?`,
390
+ answer: [`Local permitido para ${commandToken}: ${whereLabel}.`, `Permissao necessaria: ${permissionLabel}.`].join('\n'),
391
+ source: 'deterministic',
392
+ },
393
+ ];
394
+ };
395
+
396
+ const buildDeterministicCommandExplanation = ({ entry, commandPrefix = '/', context = {}, includeSecurity = true }) => {
397
+ const { llm } = getAiHelpConfig();
398
+ const commandToken = `${commandPrefix}${entry.name}`;
399
+ const usage = readEntryUsage(entry).map((method) => renderUsage(method, commandPrefix));
400
+ const preconditions = formatPreConditions(readEntryRequirements(entry));
401
+ const gate = evaluatePreConditions(entry, context);
402
+ const whereLabel = formatWhereLabel(readEntryContexts(entry));
403
+ const permissionLabel = formatPermissionLabel(readEntryPermission(entry));
404
+
405
+ const lines = [`📘 *Comando:* ${commandToken}`, `📝 ${readEntryDescription(entry) || 'Sem descricao cadastrada.'}`, '', `👤 *Quem pode usar:* ${permissionLabel}`, `📍 *Onde pode usar:* ${whereLabel}`, `⏱️ *Limite:* ${readEntryUsageLimit(entry) || 'nao informado'}`, '', '*Como usar:*', ...(usage.length ? usage.map((line) => `- ${line}`) : ['- Uso nao configurado no JSON.']), '', '*Pre-condicoes:*', ...preconditions];
406
+
407
+ if (!gate.allowed) {
408
+ lines.push('');
409
+ lines.push('⚠️ *Contexto atual:* voce nao atende todas as pre-condicoes.');
410
+ lines.push(...gate.reasons.map((reason) => `- ${reason}`));
411
+ }
412
+
413
+ if (includeSecurity) {
414
+ lines.push('');
415
+ lines.push('🔒 A IA apenas orienta. Nenhuma acao administrativa foi executada automaticamente.');
416
+ }
417
+
418
+ return clampText(lines.join('\n'), llm.maxResponseChars);
419
+ };
420
+
421
+ const readAgentExcerpt = async () => {
422
+ const { llm } = getAiHelpConfig();
423
+ if (!agentMdPath) return '';
424
+ try {
425
+ const raw = await fs.readFile(agentMdPath, 'utf8');
426
+ return raw.slice(0, llm.maxAgentContextChars);
427
+ } catch {
428
+ return '';
429
+ }
430
+ };
431
+
432
+ const summarizeConfigForPrompt = () => {
433
+ const entries = listEnabledCommands();
434
+ return entries
435
+ .map((entry) => {
436
+ const methods = readEntryUsage(entry).slice(0, 2).join(' | ');
437
+ return `- ${entry.name} | permissao=${readEntryPermission(entry) || 'n/a'} | local=${formatWhereLabel(readEntryContexts(entry))} | uso=${methods}`;
438
+ })
439
+ .join('\n');
440
+ };
441
+
442
+ const isGeminiReady = () => Boolean(String(process.env.GEMINI_API_KEY || '').trim());
443
+ const isOpenAIReady = () => Boolean(String(process.env.OPENAI_API_KEY || '').trim());
444
+ const isProviderReady = (provider) => (provider === 'gemini' ? isGeminiReady() : isOpenAIReady());
445
+
446
+ const canUseLLM = () => {
447
+ const config = getAiHelpConfig();
448
+ return config.enabled && config.llm.enabled && (isGeminiReady() || isOpenAIReady());
449
+ };
450
+
451
+ const getGeminiService = () => {
452
+ if (!isGeminiReady()) return null;
453
+ const config = getAiHelpConfig();
454
+ if (!cachedGeminiService) {
455
+ cachedGeminiService = createGeminiTextService({
456
+ apiKey: process.env.GEMINI_API_KEY,
457
+ defaultModel: config.llm.model || process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL,
458
+ timeoutMs: config.llm.timeoutMs,
459
+ });
460
+ }
461
+ return cachedGeminiService;
462
+ };
463
+
464
+ const getOpenAIClient = () => {
465
+ if (!isOpenAIReady()) return null;
466
+ const config = getAiHelpConfig();
467
+ if (!cachedOpenAIClient) {
468
+ cachedOpenAIClient = new OpenAI({
469
+ apiKey: process.env.OPENAI_API_KEY,
470
+ timeout: config.llm.timeoutMs,
471
+ maxRetries: 0,
472
+ });
473
+ }
474
+ return cachedOpenAIClient;
475
+ };
476
+
477
+ const callGeminiLLM = async ({ instructions, userPrompt, model }) => {
478
+ const service = getGeminiService();
479
+ if (!service) return null;
480
+
481
+ const response = await service.generateText({
482
+ instructions,
483
+ userPrompt,
484
+ model,
485
+ });
486
+
487
+ const text = String(response?.text || '').trim();
488
+ if (!text) return null;
489
+ return {
490
+ provider: 'gemini',
491
+ model: String(response?.model || model || '').trim() || model,
492
+ text,
493
+ };
494
+ };
495
+
496
+ const callOpenAiLLM = async ({ instructions, userPrompt, model }) => {
497
+ const client = getOpenAIClient();
498
+ if (!client) return null;
499
+
500
+ const response = await client.responses.create({
501
+ model,
502
+ instructions,
503
+ input: [
504
+ {
505
+ role: 'user',
506
+ content: [{ type: 'input_text', text: userPrompt }],
507
+ },
508
+ ],
509
+ });
510
+
511
+ const text = String(response?.output_text || '').trim();
512
+ if (!text) return null;
513
+ return {
514
+ provider: 'openai',
515
+ model,
516
+ text,
517
+ };
518
+ };
519
+
520
+ const resolveLlmCallOrder = (provider = 'gemini') => {
521
+ const primary = normalizeLlmProvider(provider, 'gemini');
522
+ const fallback = primary === 'gemini' ? 'openai' : 'gemini';
523
+ return [primary, fallback];
524
+ };
525
+
526
+ const askLLM = async ({ mode, question, commandName, commandPrefix = '/', context = {}, deterministicDraft }) => {
527
+ const config = getAiHelpConfig();
528
+ if (!canUseLLM()) return null;
529
+
530
+ const agentExcerpt = await readAgentExcerpt();
531
+ const configSummary = summarizeConfigForPrompt();
532
+ const contextSummary = [`is_group_message=${Boolean(context.isGroupMessage)}`, `is_sender_admin=${Boolean(context.isSenderAdmin)}`, `is_sender_owner=${Boolean(context.isSenderOwner)}`, `command_prefix=${commandPrefix}`].join(' | ');
533
+
534
+ const instructions = [`Voce e um assistente de ajuda para comandos do modulo ${moduleLabel}.`, 'Responda em PT-BR, de forma objetiva e acionavel.', 'Nunca execute acao; apenas explique.', 'Sempre informe quem pode usar e onde pode usar o comando.', 'Se houver restricao de pre-condicao, destaque no texto.', 'Nao invente comandos que nao estao no contexto.'].join(' ');
535
+
536
+ const userPrompt = [`Modo: ${mode}`, commandName ? `Comando alvo: ${commandName}` : '', question ? `Pergunta: ${question}` : '', `Contexto: ${contextSummary}`, '', 'Rascunho deterministico:', deterministicDraft || '(vazio)', '', 'Resumo de comandos:', configSummary, '', 'Trecho do AGENT.md:', agentExcerpt].filter(Boolean).join('\n');
537
+
538
+ const providerOrder = resolveLlmCallOrder(config.llm.provider);
539
+ for (const provider of providerOrder) {
540
+ if (!isProviderReady(provider)) continue;
541
+
542
+ try {
543
+ const result =
544
+ provider === 'gemini'
545
+ ? await callGeminiLLM({
546
+ instructions,
547
+ userPrompt,
548
+ model: config.llm.model,
549
+ })
550
+ : await callOpenAiLLM({
551
+ instructions,
552
+ userPrompt,
553
+ model: config.llm.model,
554
+ });
555
+
556
+ if (!result?.text) continue;
557
+ return {
558
+ provider,
559
+ model: result.model,
560
+ text: clampText(result.text, config.llm.maxResponseChars),
561
+ };
562
+ } catch (error) {
563
+ logger.warn(`${moduleKey}_ai_help: falha no LLM.`, {
564
+ mode,
565
+ provider,
566
+ commandName,
567
+ error: error?.message,
568
+ });
569
+ }
570
+ }
571
+
572
+ return null;
573
+ };
574
+
575
+ const incrementCacheMetric = async (metricKey) => {
576
+ const cache = await readCache();
577
+ cache.metrics = {
578
+ ...cache.metrics,
579
+ [metricKey]: Number(cache.metrics?.[metricKey] || 0) + 1,
580
+ };
581
+ cache.updatedAt = new Date().toISOString();
582
+ await writeCache(cache);
583
+ };
584
+
585
+ const saveQuestionCacheEntry = async ({ question, answer, source = 'deterministic', command = null, scope = CACHE_SCOPE_QUESTION, modelName = null, metadata = null, persistDb = true }) => {
586
+ const config = getAiHelpConfig();
587
+ const key = normalizeText(question);
588
+ if (!key) return;
589
+ const safeScope = scope === CACHE_SCOPE_COMMAND_EXPLAIN ? CACHE_SCOPE_COMMAND_EXPLAIN : CACHE_SCOPE_QUESTION;
590
+ const normalizedSource = normalizeCacheSource(source);
591
+ const normalizedAnswer = clampText(answer, config.llm.maxResponseChars);
592
+ if (!normalizedAnswer) return;
593
+ const now = new Date().toISOString();
594
+
595
+ const cache = await readCache();
596
+ cache.questionCache[key] = {
597
+ question: String(question || '').trim(),
598
+ answer: normalizedAnswer,
599
+ source: normalizedSource,
600
+ command,
601
+ scope: safeScope,
602
+ createdAt: now,
603
+ };
604
+ cache.updatedAt = now;
605
+ await writeCache(cache);
606
+
607
+ if (!persistDb) return;
608
+
609
+ await upsertAiHelpCachedResponse({
610
+ moduleKey,
611
+ scope: safeScope,
612
+ question: String(question || '').trim() || key,
613
+ normalizedQuestion: key,
614
+ answer: normalizedAnswer,
615
+ source: normalizedSource,
616
+ commandName: command,
617
+ modelName,
618
+ metadata,
619
+ });
620
+ };
621
+
622
+ const flattenFaq = (faqByCommand = {}) => {
623
+ const rows = [];
624
+ for (const [commandName, list] of Object.entries(faqByCommand || {})) {
625
+ if (!Array.isArray(list)) continue;
626
+ for (const item of list) {
627
+ rows.push({ commandName, ...(item || {}) });
628
+ }
629
+ }
630
+ return rows;
631
+ };
632
+
633
+ const detectCommandInText = (value) => {
634
+ const normalized = normalizeText(value);
635
+ if (!normalized) return null;
636
+
637
+ const tokens = normalized.split(/\s+/).filter(Boolean);
638
+ for (const token of tokens) {
639
+ const raw = token.replace(/^\/+/, '');
640
+ const canonical = resolveCommandName(raw);
641
+ if (canonical) return canonical;
642
+ }
643
+
644
+ for (const token of tokens) {
645
+ const canonical = resolveCommandName(token);
646
+ if (canonical) return canonical;
647
+ }
648
+
649
+ return null;
650
+ };
651
+
652
+ const lookupQuestionCacheEntry = async ({ question, scope = CACHE_SCOPE_QUESTION } = {}) => {
653
+ const normalizedQuestion = normalizeText(question);
654
+ if (!normalizedQuestion) {
655
+ return { answer: null, source: 'none', commandName: null };
656
+ }
657
+
658
+ const safeScope = scope === CACHE_SCOPE_COMMAND_EXPLAIN ? CACHE_SCOPE_COMMAND_EXPLAIN : CACHE_SCOPE_QUESTION;
659
+
660
+ const dbCached = await getAiHelpCachedResponse({
661
+ moduleKey,
662
+ scope: safeScope,
663
+ question,
664
+ normalizedQuestion,
665
+ updateUsage: true,
666
+ });
667
+
668
+ if (dbCached?.answer_text) {
669
+ return {
670
+ answer: dbCached.answer_text,
671
+ source: safeScope === CACHE_SCOPE_COMMAND_EXPLAIN ? 'db_command_cache' : 'db_question_cache',
672
+ commandName: dbCached.command_name || null,
673
+ };
674
+ }
675
+
676
+ const cache = await readCache();
677
+ const cachedQuestion = cache.questionCache?.[normalizedQuestion];
678
+ if (cachedQuestion?.answer) {
679
+ return {
680
+ answer: cachedQuestion.answer,
681
+ source: 'question_cache',
682
+ commandName: cachedQuestion.command || null,
683
+ };
684
+ }
685
+
686
+ return { answer: null, source: 'none', commandName: null };
687
+ };
688
+
689
+ const lookupFaqAnswer = async (question) => {
690
+ const normalizedQuestion = normalizeText(question);
691
+ if (!normalizedQuestion) return { answer: null, source: 'none', commandName: null };
692
+
693
+ const cachedQuestion = await lookupQuestionCacheEntry({
694
+ question,
695
+ scope: CACHE_SCOPE_QUESTION,
696
+ });
697
+ if (cachedQuestion.answer) return cachedQuestion;
698
+
699
+ const cache = await readCache();
700
+ const allFaq = flattenFaq(cache.faqByCommand || {});
701
+ const exact = allFaq.find((item) => normalizeText(item.question) === normalizedQuestion);
702
+ if (exact?.answer) {
703
+ return {
704
+ answer: exact.answer,
705
+ source: 'faq_exact',
706
+ commandName: exact.commandName || null,
707
+ };
708
+ }
709
+
710
+ const fuzzy = allFaq.find((item) => {
711
+ const itemQuestion = normalizeText(item.question);
712
+ if (!itemQuestion) return false;
713
+ return itemQuestion.includes(normalizedQuestion) || normalizedQuestion.includes(itemQuestion);
714
+ });
715
+
716
+ if (fuzzy?.answer) {
717
+ return {
718
+ answer: fuzzy.answer,
719
+ source: 'faq_fuzzy',
720
+ commandName: fuzzy.commandName || null,
721
+ };
722
+ }
723
+
724
+ return { answer: null, source: 'none', commandName: null };
725
+ };
726
+
727
+ const generateFaq = async ({ commandPrefix = '/', force = false, reason = 'manual' } = {}) =>
728
+ withFaqGenerationLock(async () => {
729
+ const config = getAiHelpConfig();
730
+ if (!config.enabled) {
731
+ return {
732
+ ok: false,
733
+ disabled: true,
734
+ commandCount: 0,
735
+ faqCount: 0,
736
+ text: 'Ajuda IA desativada na configuracao do modulo.',
737
+ };
738
+ }
739
+
740
+ const cache = await readCache();
741
+ const now = new Date().toISOString();
742
+
743
+ if (!force && cache.generatedAt) {
744
+ const ageMs = Date.now() - new Date(cache.generatedAt).getTime();
745
+ if (Number.isFinite(ageMs) && ageMs >= 0 && ageMs < config.faq.intervalMs) {
746
+ const commandCount = Object.keys(cache.faqByCommand || {}).length;
747
+ const faqCount = Object.values(cache.faqByCommand || {}).reduce((acc, list) => acc + (Array.isArray(list) ? list.length : 0), 0);
748
+ return {
749
+ ok: true,
750
+ commandCount,
751
+ faqCount,
752
+ cached: true,
753
+ text: guidanceFns.faqSummary({ commandCount, faqCount, commandPrefix }),
754
+ };
755
+ }
756
+ }
757
+
758
+ const entries = listEnabledCommands();
759
+ const faqByCommand = {};
760
+ let faqCount = 0;
761
+
762
+ for (const entry of entries) {
763
+ const canonicalName = String(entry?.name || '')
764
+ .trim()
765
+ .toLowerCase();
766
+ if (!canonicalName) continue;
767
+ const items = buildCommandFaqItems(entry, commandPrefix).map((item) => ({
768
+ ...item,
769
+ createdAt: now,
770
+ }));
771
+ faqByCommand[canonicalName] = items;
772
+ faqCount += items.length;
773
+ }
774
+
775
+ const updated = {
776
+ ...cache,
777
+ version: FAQ_CACHE_VERSION,
778
+ faqByCommand,
779
+ updatedAt: now,
780
+ generatedAt: now,
781
+ metrics: {
782
+ ...cache.metrics,
783
+ generated_count: Number(cache.metrics?.generated_count || 0) + 1,
784
+ last_generated_at: now,
785
+ },
786
+ };
787
+
788
+ await writeCache(updated);
789
+
790
+ return {
791
+ ok: true,
792
+ commandCount: entries.length,
793
+ faqCount,
794
+ cached: false,
795
+ reason,
796
+ text: guidanceFns.faqSummary({ commandCount: entries.length, faqCount, commandPrefix }),
797
+ };
798
+ });
799
+
800
+ const explainCommand = async (command, context = {}) => {
801
+ await generateFaq({ commandPrefix: context.commandPrefix || '/', reason: 'warmup' });
802
+
803
+ const canonical = resolveCommandName(command);
804
+ if (!canonical) {
805
+ const suggestion = buildUnknownCommandSuggestion(command, {
806
+ commandPrefix: context.commandPrefix || '/',
807
+ });
808
+ await incrementCacheMetric('misses');
809
+ return {
810
+ ok: false,
811
+ commandName: null,
812
+ source: 'none',
813
+ text:
814
+ suggestion ||
815
+ guidanceFns.missingCommandText({
816
+ commandPrefix: context.commandPrefix || '/',
817
+ }),
818
+ };
819
+ }
820
+
821
+ const entry = getCommandEntry(canonical);
822
+ if (!entry || entry.enabled === false) {
823
+ await incrementCacheMetric('misses');
824
+ return {
825
+ ok: false,
826
+ commandName: canonical,
827
+ source: 'none',
828
+ text: `O comando ${context.commandPrefix || '/'}${canonical} esta desativado no momento.`,
829
+ };
830
+ }
831
+
832
+ const explainCacheKey = buildCommandExplainCacheKey(canonical);
833
+ const cachedExplanation = await lookupQuestionCacheEntry({
834
+ question: explainCacheKey,
835
+ scope: CACHE_SCOPE_COMMAND_EXPLAIN,
836
+ });
837
+ if (cachedExplanation.answer) {
838
+ await incrementCacheMetric('question_hits');
839
+ return {
840
+ ok: true,
841
+ commandName: canonical,
842
+ source: cachedExplanation.source,
843
+ text: clampText(cachedExplanation.answer, getAiHelpConfig().llm.maxResponseChars),
844
+ };
845
+ }
846
+
847
+ const deterministic = buildDeterministicCommandExplanation({
848
+ entry,
849
+ commandPrefix: context.commandPrefix || '/',
850
+ context,
851
+ });
852
+
853
+ const llmAnswer = await askLLM({
854
+ mode: 'explain_command',
855
+ commandName: canonical,
856
+ commandPrefix: context.commandPrefix || '/',
857
+ context,
858
+ deterministicDraft: deterministic,
859
+ });
860
+
861
+ if (llmAnswer?.text) {
862
+ const llmSource = `llm_${llmAnswer.provider || 'unknown'}`;
863
+ await incrementCacheMetric('llm_calls');
864
+ await saveQuestionCacheEntry({
865
+ question: explainCacheKey,
866
+ answer: llmAnswer.text,
867
+ source: llmSource,
868
+ command: canonical,
869
+ scope: CACHE_SCOPE_COMMAND_EXPLAIN,
870
+ modelName: llmAnswer.model || getAiHelpConfig().llm.model,
871
+ metadata: { mode: 'explain_command', module: moduleKey, provider: llmAnswer.provider || 'unknown' },
872
+ });
873
+ return {
874
+ ok: true,
875
+ commandName: canonical,
876
+ source: llmSource,
877
+ text: llmAnswer.text,
878
+ };
879
+ }
880
+
881
+ if (canUseLLM()) {
882
+ await incrementCacheMetric('llm_errors');
883
+ }
884
+
885
+ await saveQuestionCacheEntry({
886
+ question: explainCacheKey,
887
+ answer: deterministic,
888
+ source: 'deterministic',
889
+ command: canonical,
890
+ scope: CACHE_SCOPE_COMMAND_EXPLAIN,
891
+ metadata: { mode: 'explain_command', module: moduleKey },
892
+ });
893
+
894
+ return {
895
+ ok: true,
896
+ commandName: canonical,
897
+ source: 'deterministic',
898
+ text: deterministic,
899
+ };
900
+ };
901
+
902
+ const answerQuestion = async (question, context = {}) => {
903
+ const rawQuestion = String(question || '').trim();
904
+ if (!rawQuestion) {
905
+ return {
906
+ ok: false,
907
+ source: 'none',
908
+ text: guidanceFns.askUsage({ commandPrefix: context.commandPrefix || '/' }),
909
+ commandName: null,
910
+ };
911
+ }
912
+
913
+ await generateFaq({ commandPrefix: context.commandPrefix || '/', reason: 'warmup' });
914
+
915
+ const explicitCommand = detectCommandInText(rawQuestion);
916
+ if (explicitCommand) {
917
+ const explanation = await explainCommand(explicitCommand, context);
918
+ if (explanation?.ok) {
919
+ await incrementCacheMetric('question_hits');
920
+ await saveQuestionCacheEntry({
921
+ question: rawQuestion,
922
+ answer: explanation.text,
923
+ source: explanation.source,
924
+ command: explanation.commandName,
925
+ });
926
+ return {
927
+ ok: true,
928
+ source: explanation.source,
929
+ commandName: explanation.commandName,
930
+ text: explanation.text,
931
+ };
932
+ }
933
+ }
934
+
935
+ const faqLookup = await lookupFaqAnswer(rawQuestion);
936
+ if (faqLookup.answer) {
937
+ const isQuestionCacheSource = ['question_cache', 'db_question_cache', 'db_command_cache'].includes(faqLookup.source);
938
+ await incrementCacheMetric(isQuestionCacheSource ? 'question_hits' : 'faq_hits');
939
+ return {
940
+ ok: true,
941
+ source: faqLookup.source,
942
+ commandName: faqLookup.commandName,
943
+ text: clampText(faqLookup.answer, getAiHelpConfig().llm.maxResponseChars),
944
+ };
945
+ }
946
+
947
+ const suggestions = listEnabledCommands()
948
+ .slice(0, 6)
949
+ .map((entry) => `${context.commandPrefix || '/'}${entry.name}`)
950
+ .join(', ');
951
+
952
+ const deterministicFallback = guidanceFns.questionFallback({
953
+ question: rawQuestion,
954
+ commandPrefix: context.commandPrefix || '/',
955
+ detectedCommand: explicitCommand,
956
+ suggestions,
957
+ });
958
+
959
+ const llmAnswer = await askLLM({
960
+ mode: 'answer_question',
961
+ question: rawQuestion,
962
+ commandName: explicitCommand,
963
+ commandPrefix: context.commandPrefix || '/',
964
+ context,
965
+ deterministicDraft: deterministicFallback,
966
+ });
967
+
968
+ if (llmAnswer?.text) {
969
+ const llmSource = `llm_${llmAnswer.provider || 'unknown'}`;
970
+ await incrementCacheMetric('llm_calls');
971
+ await saveQuestionCacheEntry({
972
+ question: rawQuestion,
973
+ answer: llmAnswer.text,
974
+ source: llmSource,
975
+ command: explicitCommand,
976
+ modelName: llmAnswer.model || getAiHelpConfig().llm.model,
977
+ metadata: { mode: 'answer_question', module: moduleKey, provider: llmAnswer.provider || 'unknown' },
978
+ });
979
+ return {
980
+ ok: true,
981
+ source: llmSource,
982
+ commandName: explicitCommand,
983
+ text: llmAnswer.text,
984
+ };
985
+ }
986
+
987
+ if (canUseLLM()) {
988
+ await incrementCacheMetric('llm_errors');
989
+ }
990
+
991
+ await incrementCacheMetric('misses');
992
+ await saveQuestionCacheEntry({
993
+ question: rawQuestion,
994
+ answer: deterministicFallback,
995
+ source: 'deterministic',
996
+ command: explicitCommand,
997
+ metadata: { mode: 'answer_question', module: moduleKey },
998
+ });
999
+
1000
+ return {
1001
+ ok: true,
1002
+ source: 'deterministic',
1003
+ commandName: explicitCommand,
1004
+ text: deterministicFallback,
1005
+ };
1006
+ };
1007
+
1008
+ const buildUnknownCommandSuggestion = (rawCommand, { commandPrefix = '/' } = {}) => {
1009
+ const command = normalizeText(rawCommand).replace(/^\/+/, '');
1010
+ if (!command) return null;
1011
+
1012
+ const entries = listEnabledCommands();
1013
+ const tokens = [];
1014
+
1015
+ for (const entry of entries) {
1016
+ const canonical = String(entry.name || '')
1017
+ .trim()
1018
+ .toLowerCase();
1019
+ if (!canonical) continue;
1020
+ tokens.push({ token: canonical, canonical });
1021
+
1022
+ const aliases = Array.isArray(entry.aliases) ? entry.aliases : [];
1023
+ for (const alias of aliases) {
1024
+ const normalizedAlias = normalizeText(alias);
1025
+ if (!normalizedAlias) continue;
1026
+ tokens.push({ token: normalizedAlias, canonical });
1027
+ }
1028
+ }
1029
+
1030
+ const ranked = tokens
1031
+ .map((item) => ({
1032
+ ...item,
1033
+ distance: levenshteinDistance(command, item.token),
1034
+ }))
1035
+ .sort((a, b) => a.distance - b.distance)
1036
+ .slice(0, 3);
1037
+
1038
+ if (!ranked.length) return null;
1039
+
1040
+ const bestDistance = ranked[0].distance;
1041
+ const tolerance = Math.max(2, Math.floor(command.length * 0.45));
1042
+ if (bestDistance > tolerance) return null;
1043
+
1044
+ const uniqueCanonical = [];
1045
+ for (const item of ranked) {
1046
+ if (!uniqueCanonical.includes(item.canonical)) uniqueCanonical.push(item.canonical);
1047
+ }
1048
+
1049
+ if (!uniqueCanonical.length) return null;
1050
+
1051
+ const suggestions = uniqueCanonical.map((value) => `${commandPrefix}${value}`).join(', ');
1052
+ return guidanceFns.unknownCommand({
1053
+ rawCommand,
1054
+ suggestions,
1055
+ commandPrefix,
1056
+ });
1057
+ };
1058
+
1059
+ const startScheduler = () => {
1060
+ if (schedulerStarted) return;
1061
+ schedulerStarted = true;
1062
+
1063
+ const config = getAiHelpConfig();
1064
+ if (!config.enabled || !config.faq.autoGenerateOnStart) return;
1065
+
1066
+ const runScheduledGeneration = async () => {
1067
+ try {
1068
+ await generateFaq({ reason: 'scheduler', force: true });
1069
+ } catch (error) {
1070
+ logger.warn(`${moduleKey}_ai_help: scheduler falhou ao gerar FAQ.`, {
1071
+ error: error?.message,
1072
+ });
1073
+ }
1074
+ };
1075
+
1076
+ runScheduledGeneration();
1077
+ schedulerHandle = setInterval(runScheduledGeneration, config.faq.intervalMs);
1078
+ if (typeof schedulerHandle?.unref === 'function') {
1079
+ schedulerHandle.unref();
1080
+ }
1081
+ };
1082
+
1083
+ const stopSchedulerForTests = () => {
1084
+ if (schedulerHandle) {
1085
+ clearInterval(schedulerHandle);
1086
+ schedulerHandle = null;
1087
+ }
1088
+ schedulerStarted = false;
1089
+ };
1090
+
1091
+ return {
1092
+ gerarFaqAutomatica: generateFaq,
1093
+ explicarComando: explainCommand,
1094
+ responderPergunta: answerQuestion,
1095
+ buildUnknownCommandSuggestion,
1096
+ startScheduler,
1097
+ stopSchedulerForTests,
1098
+ };
1099
+ };