@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.
- package/.clusterfuzzlite/Dockerfile +10 -0
- package/.env.example +907 -0
- package/.github/codeql/codeql-config.yml +10 -0
- package/.github/dependabot.yml +35 -0
- package/.github/workflows/ci.yml +73 -0
- package/.github/workflows/codeql.yml +106 -0
- package/.github/workflows/db-migration-check.yml +98 -0
- package/.github/workflows/dependency-review.yml +22 -0
- package/.github/workflows/deploy.yml +95 -0
- package/.github/workflows/release.yml +106 -0
- package/.github/workflows/security-attest-provenance.yml +51 -0
- package/.github/workflows/security-gitleaks.yml +34 -0
- package/.github/workflows/security-runner-hardening.yml +31 -0
- package/.github/workflows/security-scorecard.yml +44 -0
- package/.github/workflows/security-zap-baseline.yml +44 -0
- package/.github/workflows/security-zap-full-scan.yml +43 -0
- package/.github/workflows/security-zizmor.yml +36 -0
- package/.github/workflows/wiki-sync.yml +44 -0
- package/.gitleaks.toml +15 -0
- package/.prettierrc +34 -0
- package/CODE_OF_CONDUCT.md +114 -0
- package/LICENSE +56 -0
- package/README.md +110 -0
- package/SECURITY.md +110 -0
- package/app/config/index.js +4 -0
- package/app/configParts/adminIdentity.js +92 -0
- package/app/configParts/baileysConfig.js +1818 -0
- package/app/configParts/groupUtils.js +692 -0
- package/app/configParts/loggerConfig.js +394 -0
- package/app/configParts/messagePersistenceService.js +305 -0
- package/app/connection/baileysCompatibility.test.js +40 -0
- package/app/connection/baileysDbAuthState.js +344 -0
- package/app/connection/socketController.js +2243 -0
- package/app/controllers/messageController.js +7 -0
- package/app/controllers/messagePipeline/commandMiddleware.js +146 -0
- package/app/controllers/messagePipeline/conversationMiddleware.js +183 -0
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +522 -0
- package/app/controllers/messagePipeline/postProcessingMiddleware.js +41 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +166 -0
- package/app/controllers/messageProcessingPipeline.js +699 -0
- package/app/modules/adminModule/AGENT.md +4056 -0
- package/app/modules/adminModule/adminAiHelpService.js +56 -0
- package/app/modules/adminModule/adminConfigRuntime.js +177 -0
- package/app/modules/adminModule/commandConfig.json +7122 -0
- package/app/modules/adminModule/groupCommandHandlers.js +1823 -0
- package/app/modules/adminModule/groupCommandHandlers.test.js +350 -0
- package/app/modules/adminModule/groupEventHandlers.js +399 -0
- package/app/modules/aiModule/AGENT.md +547 -0
- package/app/modules/aiModule/aiAiHelpService.js +14 -0
- package/app/modules/aiModule/aiConfigRuntime.js +135 -0
- package/app/modules/aiModule/catCommand.js +967 -0
- package/app/modules/aiModule/commandConfig.json +981 -0
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +83 -0
- package/app/modules/gameModule/AGENT.md +196 -0
- package/app/modules/gameModule/commandConfig.json +366 -0
- package/app/modules/gameModule/diceCommand.js +42 -0
- package/app/modules/gameModule/gameAiHelpService.js +14 -0
- package/app/modules/gameModule/gameConfigRuntime.js +68 -0
- package/app/modules/menuModule/AGENT.md +205 -0
- package/app/modules/menuModule/commandConfig.json +366 -0
- package/app/modules/menuModule/common.js +316 -0
- package/app/modules/menuModule/menuAiHelpService.js +14 -0
- package/app/modules/menuModule/menuConfigRuntime.js +68 -0
- package/app/modules/menuModule/menus.js +66 -0
- package/app/modules/playModule/AGENT.md +321 -0
- package/app/modules/playModule/commandConfig.json +584 -0
- package/app/modules/playModule/playAiHelpService.js +14 -0
- package/app/modules/playModule/playCommand.js +1417 -0
- package/app/modules/playModule/playConfigRuntime.js +68 -0
- package/app/modules/quoteModule/AGENT.md +199 -0
- package/app/modules/quoteModule/commandConfig.json +366 -0
- package/app/modules/quoteModule/quoteAiHelpService.js +14 -0
- package/app/modules/quoteModule/quoteCommand.js +842 -0
- package/app/modules/quoteModule/quoteConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/AGENT.md +229 -0
- package/app/modules/rpgPokemonModule/commandConfig.json +386 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +795 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.js +2110 -0
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +770 -0
- package/app/modules/rpgPokemonModule/rpgEvolutionUtils.js +22 -0
- package/app/modules/rpgPokemonModule/rpgPokemonAiHelpService.js +14 -0
- package/app/modules/rpgPokemonModule/rpgPokemonCommand.js +174 -0
- package/app/modules/rpgPokemonModule/rpgPokemonConfigRuntime.js +68 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +192 -0
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.test.js +93 -0
- package/app/modules/rpgPokemonModule/rpgPokemonEvolution.test.js +46 -0
- package/app/modules/rpgPokemonModule/rpgPokemonMessages.js +746 -0
- package/app/modules/rpgPokemonModule/rpgPokemonRepository.js +1847 -0
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +6839 -0
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +354 -0
- package/app/modules/statsModule/AGENT.md +320 -0
- package/app/modules/statsModule/commandConfig.json +540 -0
- package/app/modules/statsModule/globalRankingCommand.js +64 -0
- package/app/modules/statsModule/rankingCommand.js +41 -0
- package/app/modules/statsModule/rankingCommon.js +1305 -0
- package/app/modules/statsModule/statsAiHelpService.js +14 -0
- package/app/modules/statsModule/statsConfigRuntime.js +68 -0
- package/app/modules/stickerModule/AGENT.md +692 -0
- package/app/modules/stickerModule/addStickerMetadata.js +239 -0
- package/app/modules/stickerModule/commandConfig.json +1216 -0
- package/app/modules/stickerModule/convertToWebp.js +367 -0
- package/app/modules/stickerModule/stickerAiHelpService.js +14 -0
- package/app/modules/stickerModule/stickerCommand.js +446 -0
- package/app/modules/stickerModule/stickerConfigRuntime.js +68 -0
- package/app/modules/stickerModule/stickerConvertCommand.js +159 -0
- package/app/modules/stickerModule/stickerTextCommand.js +653 -0
- package/app/modules/stickerPackModule/AGENT.md +215 -0
- package/app/modules/stickerPackModule/autoPackCollectorRuntime.js +20 -0
- package/app/modules/stickerPackModule/autoPackCollectorService.js +357 -0
- package/app/modules/stickerPackModule/commandConfig.json +387 -0
- package/app/modules/stickerPackModule/domainEventOutboxRepository.js +227 -0
- package/app/modules/stickerPackModule/domainEvents.js +52 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.js +429 -0
- package/app/modules/stickerPackModule/semanticReclassificationEngine.test.js +75 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +544 -0
- package/app/modules/stickerPackModule/stickerAssetClassificationRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetRepository.js +400 -0
- package/app/modules/stickerPackModule/stickerAssetReprocessQueueRepository.js +175 -0
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +3702 -0
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +559 -0
- package/app/modules/stickerPackModule/stickerClassificationService.js +557 -0
- package/app/modules/stickerPackModule/stickerDedicatedTaskWorkerRuntime.js +249 -0
- package/app/modules/stickerPackModule/stickerDomainEventBus.js +65 -0
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +208 -0
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +99 -0
- package/app/modules/stickerPackModule/stickerObjectStorageService.js +285 -0
- package/app/modules/stickerPackModule/stickerPackAiHelpService.js +14 -0
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +1148 -0
- package/app/modules/stickerPackModule/stickerPackConfigRuntime.js +68 -0
- package/app/modules/stickerPackModule/stickerPackEngagementRepository.js +152 -0
- package/app/modules/stickerPackModule/stickerPackErrors.js +30 -0
- package/app/modules/stickerPackModule/stickerPackInteractionEventRepository.js +101 -0
- package/app/modules/stickerPackModule/stickerPackItemRepository.js +432 -0
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +313 -0
- package/app/modules/stickerPackModule/stickerPackMessageService.js +268 -0
- package/app/modules/stickerPackModule/stickerPackRepository.js +450 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRepository.js +179 -0
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +271 -0
- package/app/modules/stickerPackModule/stickerPackService.js +733 -0
- package/app/modules/stickerPackModule/stickerPackServiceRuntime.js +32 -0
- package/app/modules/stickerPackModule/stickerPackUtils.js +107 -0
- package/app/modules/stickerPackModule/stickerStorageService.js +559 -0
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +242 -0
- package/app/modules/stickerPackModule/stickerWorkerTaskQueueRepository.js +242 -0
- package/app/modules/systemMetricsModule/AGENT.md +193 -0
- package/app/modules/systemMetricsModule/commandConfig.json +344 -0
- package/app/modules/systemMetricsModule/pingCommand.js +399 -0
- package/app/modules/systemMetricsModule/systemMetricsAiHelpService.js +14 -0
- package/app/modules/systemMetricsModule/systemMetricsConfigRuntime.js +68 -0
- package/app/modules/tiktokModule/AGENT.md +196 -0
- package/app/modules/tiktokModule/commandConfig.json +366 -0
- package/app/modules/tiktokModule/tiktokAiHelpService.js +14 -0
- package/app/modules/tiktokModule/tiktokCommand.js +716 -0
- package/app/modules/tiktokModule/tiktokConfigRuntime.js +68 -0
- package/app/modules/userModule/AGENT.md +200 -0
- package/app/modules/userModule/commandConfig.json +386 -0
- package/app/modules/userModule/userAiHelpService.js +14 -0
- package/app/modules/userModule/userCommand.js +1155 -0
- package/app/modules/userModule/userConfigRuntime.js +68 -0
- package/app/modules/waifuPicsModule/AGENT.md +431 -0
- package/app/modules/waifuPicsModule/commandConfig.json +780 -0
- package/app/modules/waifuPicsModule/waifuPicsAiHelpService.js +14 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +586 -0
- package/app/modules/waifuPicsModule/waifuPicsConfigRuntime.js +68 -0
- package/app/observability/metrics.js +766 -0
- package/app/services/ai/aiHelpResponseCacheRepository.js +280 -0
- package/app/services/ai/aiLearningRepository.js +400 -0
- package/app/services/ai/commandConfigEnrichmentRepository.js +769 -0
- package/app/services/ai/commandConfigEnrichmentService.js +452 -0
- package/app/services/ai/commandConfigValidationService.js +443 -0
- package/app/services/ai/commandToolBuilderService.js +192 -0
- package/app/services/ai/conversationRouterService.js +516 -0
- package/app/services/ai/geminiService.js +115 -0
- package/app/services/ai/geminiService.test.js +87 -0
- package/app/services/ai/globalModuleAiHelpService.js +1412 -0
- package/app/services/ai/globalToolCallingService.js +203 -0
- package/app/services/ai/messageCommandExecutionService.js +391 -0
- package/app/services/ai/moduleAiHelpCoreService.js +1099 -0
- package/app/services/ai/moduleAiHelpWrapperFactory.js +65 -0
- package/app/services/ai/moduleCommandConfigRuntimeService.js +113 -0
- package/app/services/ai/moduleToolExecutorService.js +464 -0
- package/app/services/ai/moduleToolRegistryService.js +178 -0
- package/app/services/ai/toolCandidateSelectorService.js +781 -0
- package/app/services/auth/googleWebLinkService.js +80 -0
- package/app/services/auth/whatsappLoginLinkService.js +230 -0
- package/app/services/external/pokeApiService.js +398 -0
- package/app/services/group/groupMetadataService.js +311 -0
- package/app/services/infra/dbWriteQueue.js +874 -0
- package/app/services/infra/featureFlagService.js +131 -0
- package/app/services/infra/queueUtils.js +55 -0
- package/app/services/messaging/captchaService.js +491 -0
- package/app/services/messaging/messagePersistenceService.js +1 -0
- package/app/services/messaging/newsBroadcastService.js +347 -0
- package/app/services/sticker/stickerFocusService.js +347 -0
- package/app/services/sticker/stickerFocusService.test.js +43 -0
- package/app/store/aiPromptStore.js +38 -0
- package/app/store/conversationSessionStore.js +131 -0
- package/app/store/groupConfigStore.js +58 -0
- package/app/store/premiumUserStore.js +54 -0
- package/app/utils/antiLink/antiLinkModule.js +700 -0
- package/app/utils/http/getImageBufferModule.js +18 -0
- package/app/utils/json/jsonSanitizer.js +113 -0
- package/app/utils/json/jsonSanitizer.test.js +40 -0
- package/app/utils/systemMetrics/systemMetricsModule.js +88 -0
- package/app/workers/aiLearningWorker.js +605 -0
- package/app/workers/commandConfigEnrichmentWorker.js +242 -0
- package/database/index.js +2075 -0
- package/database/init.js +151 -0
- package/database/migrations/.gitkeep +0 -0
- package/database/migrations/20260307_d0_hardening_down.sql +64 -0
- package/database/migrations/20260307_d0_hardening_up.sql +79 -0
- package/database/migrations/20260307_d1_terms_acceptance_down.sql +11 -0
- package/database/migrations/20260307_d1_terms_acceptance_up.sql +37 -0
- package/database/migrations/20260307_d2_auth_hardening_down.sql +75 -0
- package/database/migrations/20260307_d2_auth_hardening_up.sql +100 -0
- package/database/migrations/20260314_d7_canonical_sender_down.sql +53 -0
- package/database/migrations/20260314_d7_canonical_sender_up.sql +114 -0
- package/database/migrations/20260406_d30_security_analytics_down.sql +95 -0
- package/database/migrations/20260406_d30_security_analytics_up.sql +292 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_down.sql +2 -0
- package/database/migrations/20260407_d31_web_google_session_token_hardening_up.sql +17 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_down.sql +1 -0
- package/database/migrations/20260408_d32_ai_help_response_cache_up.sql +22 -0
- package/database/migrations/20260409_d33_ai_learning_tables_down.sql +4 -0
- package/database/migrations/20260409_d33_ai_learning_tables_up.sql +52 -0
- package/database/migrations/20260410_d34_command_config_enrichment_down.sql +3 -0
- package/database/migrations/20260410_d34_command_config_enrichment_up.sql +48 -0
- package/database/schema.sql +1186 -0
- package/docker-compose.yml +104 -0
- package/docs/audits/stickerCatalogController-out-of-scope.md +103 -0
- package/docs/audits/stickerCatalogController-symbols.md +58 -0
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +35 -0
- package/docs/compliance/dpa-b2b-standard-2026-03-07.md +80 -0
- package/docs/compliance/monthly-compliance-checklist-2026-03-07.md +88 -0
- package/docs/compliance/notice-and-takedown-policy-2026-03-07.md +34 -0
- package/docs/compliance/privacy-policy-2026-03-07.md +75 -0
- package/docs/compliance/subprocessors-inventory-2026-03-07.md +16 -0
- package/docs/database/production-db-evolution-runbook-2026q1.md +365 -0
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +86 -0
- package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +77 -0
- package/docs/security/network-hardening-runbook-2026-03-07.md +137 -0
- package/docs/seo/omnizap-seo-playbook-br-2026-02-28.md +238 -0
- package/docs/seo/satellite-page-template.md +116 -0
- package/docs/seo/satellite-pages-phase1.json +364 -0
- package/docs/wiki/Home.md +120 -0
- package/docs/wiki/pair-extraordinaire-2026-03-08.md +3 -0
- package/docs/wiki/recent-changes-2026-03-08.md +47 -0
- package/ecosystem.prod.config.cjs +135 -0
- package/eslint.config.js +89 -0
- package/index.js +488 -0
- package/ml/clip_classifier/Dockerfile +18 -0
- package/ml/clip_classifier/README.md +118 -0
- package/ml/clip_classifier/adaptive_scoring.py +40 -0
- package/ml/clip_classifier/classifier.py +654 -0
- package/ml/clip_classifier/embedding_store.py +481 -0
- package/ml/clip_classifier/env_loader.py +15 -0
- package/ml/clip_classifier/llm_label_expander.py +144 -0
- package/ml/clip_classifier/main.py +213 -0
- package/ml/clip_classifier/requirements.txt +10 -0
- package/ml/clip_classifier/similarity_engine.py +74 -0
- package/new-logo.png +0 -0
- package/observability/alert-rules.yml +60 -0
- package/observability/grafana/dashboards/omnizap-mysql.json +136 -0
- package/observability/grafana/dashboards/omnizap-overview.json +170 -0
- package/observability/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/observability/grafana/provisioning/datasources/datasources.yml +15 -0
- package/observability/loki-config.yml +38 -0
- package/observability/mysql-setup.sql +46 -0
- package/observability/prometheus.yml +35 -0
- package/observability/promtail-config.yml +84 -0
- package/observability/sticker-catalog-slo.md +83 -0
- package/observability/sticker-scale-hardening-rollout.md +128 -0
- package/package.json +144 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/assets/css/commands-react.input.css +71 -0
- package/public/assets/css/create-pack-react.input.css +31 -0
- package/public/assets/css/home-react.input.css +106 -0
- package/public/assets/css/login-react.input.css +58 -0
- package/public/assets/css/stickers-react.input.css +18 -0
- package/public/assets/css/terms-react.input.css +115 -0
- package/public/assets/css/user-react.input.css +57 -0
- package/public/assets/images/brand-icon-192.png +0 -0
- package/public/assets/images/brand-logo-128.webp +0 -0
- package/public/assets/images/hero-banner-1280.jpg +0 -0
- package/public/comandos/commands-catalog.json +4517 -0
- package/public/css/api-docs.css +161 -0
- package/public/css/stickers-admin.css +1288 -0
- package/public/css/styles.css +679 -0
- package/public/css/systemadm/admin.css +474 -0
- package/public/css/systemadm/base.css +73 -0
- package/public/css/systemadm/components.css +662 -0
- package/public/css/systemadm/layout.css +229 -0
- package/public/css/systemadm/tokens.css +56 -0
- package/public/favicon-16x16.png +0 -0
- package/public/favicon-32x32.png +0 -0
- package/public/favicon.ico +0 -0
- package/public/js/apps/apiDocsApp.js +235 -0
- package/public/js/apps/commandsReactApp.js +528 -0
- package/public/js/apps/createPackApp.js +1646 -0
- package/public/js/apps/homeReactApp.js +942 -0
- package/public/js/apps/loginReactApp.js +496 -0
- package/public/js/apps/stickersAdminApp.js +1753 -0
- package/public/js/apps/stickersApp.js +3797 -0
- package/public/js/apps/termsReactApp.js +528 -0
- package/public/js/apps/userApp.js +2540 -0
- package/public/js/apps/userProfile/actions.js +66 -0
- package/public/js/apps/userReactApp.js +547 -0
- package/public/js/catalog.js +950 -0
- package/public/pages/api-docs.html +40 -0
- package/public/pages/aup.html +158 -0
- package/public/pages/comandos.html +41 -0
- package/public/pages/dpa.html +227 -0
- package/public/pages/home.html +45 -0
- package/public/pages/licenca.html +182 -0
- package/public/pages/login.html +40 -0
- package/public/pages/notice-and-takedown.html +234 -0
- package/public/pages/politica-de-privacidade.html +251 -0
- package/public/pages/seo-bot-whatsapp-para-grupo.html +350 -0
- package/public/pages/seo-bot-whatsapp-sem-programar.html +350 -0
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +350 -0
- package/public/pages/seo-como-criar-comandos-whatsapp.html +350 -0
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +350 -0
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +350 -0
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +350 -0
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +350 -0
- package/public/pages/stickers-admin.html +31 -0
- package/public/pages/stickers-create.html +41 -0
- package/public/pages/stickers.html +45 -0
- package/public/pages/suboperadores.html +237 -0
- package/public/pages/termos-de-uso-texto-integral.html +241 -0
- package/public/pages/termos-de-uso.html +41 -0
- package/public/pages/user-password-reset.html +32 -0
- package/public/pages/user-systemadm.html +508 -0
- package/public/pages/user.html +39 -0
- package/public/robots.txt +9 -0
- package/public/site.webmanifest +24 -0
- package/public/sitemap.xml +98 -0
- package/schemas/command-config.schema.json +582 -0
- package/scripts/baileys-compat-smoke.mjs +12 -0
- package/scripts/cache-bust.mjs +142 -0
- package/scripts/deploy.sh +916 -0
- package/scripts/email-broadcast-terms-update.mjs +170 -0
- package/scripts/enrich-command-discovery-fields.mjs +286 -0
- package/scripts/generate-command-config-schema.mjs +273 -0
- package/scripts/generate-commands-catalog.mjs +308 -0
- package/scripts/generate-module-agents.mjs +631 -0
- package/scripts/generate-seo-satellite-pages.mjs +400 -0
- package/scripts/github-deploy-notify.mjs +174 -0
- package/scripts/github-release-notify.mjs +219 -0
- package/scripts/release.sh +599 -0
- package/scripts/run-codeql-local.sh +116 -0
- package/scripts/run-prettier-all.mjs +25 -0
- package/scripts/security-smoketest.mjs +581 -0
- package/scripts/sticker-catalog-loadtest.mjs +210 -0
- package/scripts/sticker-worker-task.mjs +119 -0
- package/scripts/sync-readme-snapshot.mjs +133 -0
- package/scripts/validate-command-config-schema.mjs +130 -0
- package/scripts/validate-command-configs.mjs +15 -0
- package/scripts/wiki-sync.sh +191 -0
- package/server/auth/googleWebAuth/googleWebAuthRuntime.js +62 -0
- package/server/auth/googleWebAuth/googleWebAuthService.js +807 -0
- package/server/auth/jwt/webJwtService.js +147 -0
- package/server/auth/stickerCatalogAuthContext.js +165 -0
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +189 -0
- package/server/auth/userPassword/index.js +14 -0
- package/server/auth/userPassword/userPasswordAuthService.js +422 -0
- package/server/auth/userPassword/userPasswordCrypto.js +199 -0
- package/server/auth/userPassword/userPasswordCrypto.test.js +76 -0
- package/server/auth/userPassword/userPasswordRecoveryService.js +728 -0
- package/server/auth/validation/authSchemas.js +236 -0
- package/server/auth/webAccount/webAccountHandlers.js +1434 -0
- package/server/controllers/admin/adminBanService.js +138 -0
- package/server/controllers/admin/adminPanelHandlers.js +2083 -0
- package/server/controllers/admin/stickerCatalogAdminContext.js +17 -0
- package/server/controllers/admin/systemAdminController.js +201 -0
- package/server/controllers/email/emailAutomationController.js +239 -0
- package/server/controllers/metricsController.js +21 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +514 -0
- package/server/controllers/sticker/nonCatalogHandlers.js +303 -0
- package/server/controllers/sticker/stickerCatalogController.js +4700 -0
- package/server/controllers/system/contactController.js +115 -0
- package/server/controllers/system/githubController.js +137 -0
- package/server/controllers/system/stickerCatalogSystemContext.js +758 -0
- package/server/controllers/system/storageController.js +154 -0
- package/server/controllers/system/systemController.js +135 -0
- package/server/controllers/system/systemMetricsController.js +156 -0
- package/server/controllers/system/visitController.js +90 -0
- package/server/controllers/userController.js +145 -0
- package/server/email/emailAutomationRuntime.js +225 -0
- package/server/email/emailAutomationService.js +125 -0
- package/server/email/emailOutboxRepository.js +282 -0
- package/server/email/emailTemplateService.js +480 -0
- package/server/email/emailTransportService.js +156 -0
- package/server/http/clientIp.js +95 -0
- package/server/http/httpRequestUtils.js +262 -0
- package/server/http/httpRequestUtils.test.js +80 -0
- package/server/http/httpServer.js +180 -0
- package/server/http/requestContext.js +20 -0
- package/server/http/siteRoutingUtils.js +87 -0
- package/server/index.js +1 -0
- package/server/middleware/cachePolicy.js +26 -0
- package/server/middleware/cachePolicyHelpers.js +1 -0
- package/server/middleware/endpointRateLimit.js +181 -0
- package/server/middleware/rateLimit.js +70 -0
- package/server/middleware/requireAdminAuth.js +48 -0
- package/server/middleware/securityHeaders.js +97 -0
- package/server/routes/admin/systemAdminRouter.js +64 -0
- package/server/routes/email/emailAutomationRouter.js +46 -0
- package/server/routes/health/healthRouter.js +41 -0
- package/server/routes/indexRouter.js +234 -0
- package/server/routes/metrics/metricsRouter.js +58 -0
- package/server/routes/static/staticPageRouter.js +134 -0
- package/server/routes/sticker/catalogHandlers/catalogAdminHttp.js +105 -0
- package/server/routes/sticker/catalogHandlers/catalogAuthHttp.js +77 -0
- package/server/routes/sticker/catalogHandlers/catalogPublicHttp.js +120 -0
- package/server/routes/sticker/catalogHandlers/catalogUploadHttp.js +83 -0
- package/server/routes/sticker/catalogRouter.js +77 -0
- package/server/routes/sticker/stickerApiRouter.js +84 -0
- package/server/routes/sticker/stickerDataRouter.js +145 -0
- package/server/routes/sticker/stickerSiteRouter.js +43 -0
- package/server/routes/user/userApiPaths.js +66 -0
- package/server/routes/user/userRouter.js +65 -0
- package/server/utils/safePath.js +26 -0
- package/utils/logger/loggerModule.js +35 -0
- package/vite.config.mjs +38 -0
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import logger from '#logger';
|
|
5
|
+
import { adminAiHelpWrapper } from '../../modules/adminModule/adminAiHelpService.js';
|
|
6
|
+
import { aiAiHelpWrapper } from '../../modules/aiModule/aiAiHelpService.js';
|
|
7
|
+
import { gameAiHelpWrapper } from '../../modules/gameModule/gameAiHelpService.js';
|
|
8
|
+
import { menuAiHelpWrapper } from '../../modules/menuModule/menuAiHelpService.js';
|
|
9
|
+
import { playAiHelpWrapper } from '../../modules/playModule/playAiHelpService.js';
|
|
10
|
+
import { quoteAiHelpWrapper } from '../../modules/quoteModule/quoteAiHelpService.js';
|
|
11
|
+
import { rpgPokemonAiHelpWrapper } from '../../modules/rpgPokemonModule/rpgPokemonAiHelpService.js';
|
|
12
|
+
import { statsAiHelpWrapper } from '../../modules/statsModule/statsAiHelpService.js';
|
|
13
|
+
import { stickerAiHelpWrapper } from '../../modules/stickerModule/stickerAiHelpService.js';
|
|
14
|
+
import { stickerPackAiHelpWrapper } from '../../modules/stickerPackModule/stickerPackAiHelpService.js';
|
|
15
|
+
import { systemMetricsAiHelpWrapper } from '../../modules/systemMetricsModule/systemMetricsAiHelpService.js';
|
|
16
|
+
import { tiktokAiHelpWrapper } from '../../modules/tiktokModule/tiktokAiHelpService.js';
|
|
17
|
+
import { userAiHelpWrapper } from '../../modules/userModule/userAiHelpService.js';
|
|
18
|
+
import { waifuPicsAiHelpWrapper } from '../../modules/waifuPicsModule/waifuPicsAiHelpService.js';
|
|
19
|
+
import { getAiHelpCachedResponse, listAiHelpCachedResponses, upsertAiHelpCachedResponse } from './aiHelpResponseCacheRepository.js';
|
|
20
|
+
import { maybeResolveAndExecuteToolCall } from './globalToolCallingService.js';
|
|
21
|
+
import { getConversationSession, setConversationSessionIntent } from '../../store/conversationSessionStore.js';
|
|
22
|
+
|
|
23
|
+
const GLOBAL_HELP_WRAPPERS = [menuAiHelpWrapper, stickerAiHelpWrapper, stickerPackAiHelpWrapper, playAiHelpWrapper, aiAiHelpWrapper, quoteAiHelpWrapper, waifuPicsAiHelpWrapper, statsAiHelpWrapper, systemMetricsAiHelpWrapper, gameAiHelpWrapper, userAiHelpWrapper, rpgPokemonAiHelpWrapper, tiktokAiHelpWrapper, adminAiHelpWrapper];
|
|
24
|
+
|
|
25
|
+
const GLOBAL_HELP_CACHE_MODULE_KEY = 'global';
|
|
26
|
+
const GLOBAL_HELP_CACHE_SCOPE_QUESTION = 'question';
|
|
27
|
+
const GLOBAL_HELP_CACHE_SCOPE_COMMAND = 'command';
|
|
28
|
+
const DEFAULT_GLOBAL_DB_CACHE_FUZZY_LIMIT = 120;
|
|
29
|
+
const DEFAULT_GLOBAL_DB_CACHE_FUZZY_THRESHOLD = 0.82;
|
|
30
|
+
const DEFAULT_GLOBAL_HELP_CONFIDENCE_THRESHOLD = 0.46;
|
|
31
|
+
const DEFAULT_GLOBAL_HELP_LLM_FALLBACK_THRESHOLD = 0.31;
|
|
32
|
+
const GLOBAL_HELP_FEEDBACK_CACHE_VERSION = 1;
|
|
33
|
+
const DEFAULT_GLOBAL_HELP_FEEDBACK_SESSION_TTL_MS = 20 * 60 * 1000;
|
|
34
|
+
const DEFAULT_GLOBAL_HELP_OFFLINE_FAQ_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
35
|
+
const BM25_K1 = 1.2;
|
|
36
|
+
const BM25_B = 0.75;
|
|
37
|
+
const BM25_WEIGHT = 0.45;
|
|
38
|
+
const OVERLAP_WEIGHT = 0.25;
|
|
39
|
+
const FUZZY_WEIGHT = 0.2;
|
|
40
|
+
const FEEDBACK_WEIGHT = 0.1;
|
|
41
|
+
|
|
42
|
+
const parseEnvBool = (value, fallback) => {
|
|
43
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
44
|
+
const normalized = String(value).trim().toLowerCase();
|
|
45
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(normalized)) return true;
|
|
46
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(normalized)) return false;
|
|
47
|
+
return fallback;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const parseEnvInt = (value, fallback, min, max) => {
|
|
51
|
+
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
52
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
53
|
+
return Math.max(min, Math.min(max, parsed));
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const parseEnvFloat = (value, fallback, min, max) => {
|
|
57
|
+
const parsed = Number.parseFloat(String(value ?? ''));
|
|
58
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
59
|
+
return Math.max(min, Math.min(max, parsed));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const GLOBAL_HELP_DB_CACHE_FUZZY_LIMIT = parseEnvInt(process.env.GLOBAL_HELP_DB_CACHE_FUZZY_LIMIT, DEFAULT_GLOBAL_DB_CACHE_FUZZY_LIMIT, 20, 400);
|
|
63
|
+
const GLOBAL_HELP_DB_CACHE_FUZZY_THRESHOLD = parseEnvFloat(process.env.GLOBAL_HELP_DB_CACHE_FUZZY_THRESHOLD, DEFAULT_GLOBAL_DB_CACHE_FUZZY_THRESHOLD, 0.5, 0.99);
|
|
64
|
+
const GLOBAL_HELP_CONFIDENCE_THRESHOLD = parseEnvFloat(process.env.GLOBAL_HELP_CONFIDENCE_THRESHOLD, DEFAULT_GLOBAL_HELP_CONFIDENCE_THRESHOLD, 0.2, 0.95);
|
|
65
|
+
const GLOBAL_HELP_LLM_FALLBACK_THRESHOLD = parseEnvFloat(process.env.GLOBAL_HELP_LLM_FALLBACK_THRESHOLD, DEFAULT_GLOBAL_HELP_LLM_FALLBACK_THRESHOLD, 0.1, 0.9);
|
|
66
|
+
const GLOBAL_HELP_ENABLE_WRAPPER_LLM_FALLBACK = parseEnvBool(process.env.GLOBAL_HELP_ENABLE_WRAPPER_LLM_FALLBACK, true);
|
|
67
|
+
const GLOBAL_HELP_FEEDBACK_FILE_PATH = path.resolve(process.cwd(), String(process.env.GLOBAL_HELP_FEEDBACK_FILE || 'data/cache/global-ai-feedback.json'));
|
|
68
|
+
const GLOBAL_HELP_FEEDBACK_SESSION_TTL_MS = parseEnvInt(process.env.GLOBAL_HELP_FEEDBACK_SESSION_TTL_MS, DEFAULT_GLOBAL_HELP_FEEDBACK_SESSION_TTL_MS, 60_000, 12 * 60 * 60 * 1000);
|
|
69
|
+
const GLOBAL_HELP_OFFLINE_FAQ_ENABLED = parseEnvBool(process.env.GLOBAL_HELP_OFFLINE_FAQ_ENABLED, true);
|
|
70
|
+
const GLOBAL_HELP_OFFLINE_FAQ_INTERVAL_MS = parseEnvInt(process.env.GLOBAL_HELP_OFFLINE_FAQ_INTERVAL_MS, DEFAULT_GLOBAL_HELP_OFFLINE_FAQ_INTERVAL_MS, 30 * 60 * 1000, 72 * 60 * 60 * 1000);
|
|
71
|
+
|
|
72
|
+
const normalizeText = (value) =>
|
|
73
|
+
String(value || '')
|
|
74
|
+
.trim()
|
|
75
|
+
.toLowerCase()
|
|
76
|
+
.normalize('NFD')
|
|
77
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
78
|
+
.replace(/[^a-z0-9\s/_.-]/g, ' ')
|
|
79
|
+
.replace(/\s+/g, ' ')
|
|
80
|
+
.trim();
|
|
81
|
+
|
|
82
|
+
const tokenizeText = (value) => normalizeText(value).split(/\s+/).filter(Boolean);
|
|
83
|
+
|
|
84
|
+
const toFiniteNumber = (value, fallback = 0) => {
|
|
85
|
+
const parsed = Number(value);
|
|
86
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
87
|
+
return parsed;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const limitArray = (value, size = 6) => (Array.isArray(value) ? value : []).filter(Boolean).slice(0, Math.max(0, size));
|
|
91
|
+
|
|
92
|
+
const ensureArray = (value) => (Array.isArray(value) ? value.filter(Boolean) : []);
|
|
93
|
+
const ensureObject = (value) => (value && typeof value === 'object' ? value : {});
|
|
94
|
+
const pickFirstText = (...values) => {
|
|
95
|
+
for (const value of values) {
|
|
96
|
+
const text = String(value ?? '').trim();
|
|
97
|
+
if (text) return text;
|
|
98
|
+
}
|
|
99
|
+
return '';
|
|
100
|
+
};
|
|
101
|
+
const pickFirstBoolean = (...values) => {
|
|
102
|
+
for (const value of values) {
|
|
103
|
+
if (typeof value === 'boolean') return value;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const readEntryDescription = (entry = {}) => pickFirstText(entry?.description, entry?.docs?.summary, entry?.descricao);
|
|
109
|
+
|
|
110
|
+
const readEntryUsage = (entry = {}) => {
|
|
111
|
+
const usageV2 = ensureArray(entry?.usage);
|
|
112
|
+
if (usageV2.length) return usageV2;
|
|
113
|
+
const docsUsage = ensureArray(entry?.docs?.usage_examples);
|
|
114
|
+
if (docsUsage.length) return docsUsage;
|
|
115
|
+
return ensureArray(entry?.metodos_de_uso);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const readEntryPermission = (entry = {}) => pickFirstText(entry?.permission, entry?.permissao_necessaria);
|
|
119
|
+
|
|
120
|
+
const readEntryContexts = (entry = {}) => {
|
|
121
|
+
const contextsV2 = ensureArray(entry?.contexts);
|
|
122
|
+
if (contextsV2.length) return contextsV2;
|
|
123
|
+
return ensureArray(entry?.local_de_uso);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const readEntryUsageLimit = (entry = {}) => pickFirstText(entry?.limits?.usage_description, entry?.limite_de_uso);
|
|
127
|
+
|
|
128
|
+
const readEntryCategory = (entry = {}) => pickFirstText(entry?.category, entry?.categoria);
|
|
129
|
+
|
|
130
|
+
const readEntryDiscovery = (entry = {}) => ensureObject(entry?.discovery);
|
|
131
|
+
|
|
132
|
+
const readEntryKeywords = (entry = {}) => {
|
|
133
|
+
const discovery = readEntryDiscovery(entry);
|
|
134
|
+
const source = discovery.keywords?.length ? discovery.keywords : entry?.capability_keywords;
|
|
135
|
+
return ensureArray(source);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const readEntryFaqQueries = (entry = {}) => {
|
|
139
|
+
const discovery = readEntryDiscovery(entry);
|
|
140
|
+
const source = discovery.faq_queries?.length ? discovery.faq_queries : entry?.faq_patterns;
|
|
141
|
+
return ensureArray(source);
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const readEntryUserPhrasings = (entry = {}) => {
|
|
145
|
+
const discovery = readEntryDiscovery(entry);
|
|
146
|
+
const source = discovery.user_phrasings?.length ? discovery.user_phrasings : entry?.user_phrasings;
|
|
147
|
+
return ensureArray(source);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const readEntrySuggestionPriority = (entry = {}) => {
|
|
151
|
+
const discovery = readEntryDiscovery(entry);
|
|
152
|
+
const raw = discovery.suggestion_priority !== undefined ? discovery.suggestion_priority : entry?.suggestion_priority;
|
|
153
|
+
return toFiniteNumber(raw, 100);
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const readEntryRequirements = (entry = {}) => {
|
|
157
|
+
const requirements = ensureObject(entry?.requirements);
|
|
158
|
+
const requirementsLegacy = ensureObject(requirements?.legacy);
|
|
159
|
+
const preConditions = ensureObject(entry?.pre_condicoes);
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
require_group: pickFirstBoolean(requirements.require_group, requirements.requer_grupo, requirementsLegacy.require_group, requirementsLegacy.requer_grupo, preConditions.requer_grupo),
|
|
163
|
+
require_group_admin: pickFirstBoolean(requirements.require_group_admin, requirements.requer_admin, requirementsLegacy.require_group_admin, requirementsLegacy.requer_admin, preConditions.requer_admin),
|
|
164
|
+
require_bot_owner: pickFirstBoolean(requirements.require_bot_owner, requirements.requer_admin_principal, requirementsLegacy.require_bot_owner, requirementsLegacy.requer_admin_principal, preConditions.requer_admin_principal),
|
|
165
|
+
require_google_login: pickFirstBoolean(requirements.require_google_login, requirements.requer_google_login, requirementsLegacy.require_google_login, requirementsLegacy.requer_google_login, preConditions.requer_google_login),
|
|
166
|
+
require_nsfw_enabled: pickFirstBoolean(requirements.require_nsfw_enabled, requirements.requer_nsfw, requirementsLegacy.require_nsfw_enabled, requirementsLegacy.requer_nsfw, preConditions.requer_nsfw),
|
|
167
|
+
require_media: pickFirstBoolean(requirements.require_media, requirements.requer_midia, requirementsLegacy.require_media, requirementsLegacy.requer_midia, preConditions.requer_midia),
|
|
168
|
+
require_reply_message: pickFirstBoolean(requirements.require_reply_message, requirements.requer_mensagem_respondida, requirementsLegacy.require_reply_message, requirementsLegacy.requer_mensagem_respondida, preConditions.requer_mensagem_respondida),
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const formatPermissionLabel = (permission) => String(permission || 'nao definido').trim();
|
|
173
|
+
|
|
174
|
+
const formatWhereLabel = (contexts = []) => {
|
|
175
|
+
if (!Array.isArray(contexts) || contexts.length === 0) return 'nao definido';
|
|
176
|
+
return contexts.join(', ');
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const renderUsage = (method, commandPrefix = '/') => String(method || '').replaceAll('<prefix>', String(commandPrefix || '/'));
|
|
180
|
+
|
|
181
|
+
const levenshteinDistance = (left, right) => {
|
|
182
|
+
const a = normalizeText(left);
|
|
183
|
+
const b = normalizeText(right);
|
|
184
|
+
if (!a) return b.length;
|
|
185
|
+
if (!b) return a.length;
|
|
186
|
+
|
|
187
|
+
const matrix = Array.from({ length: a.length + 1 }, () => new Array(b.length + 1).fill(0));
|
|
188
|
+
for (let i = 0; i <= a.length; i += 1) matrix[i][0] = i;
|
|
189
|
+
for (let j = 0; j <= b.length; j += 1) matrix[0][j] = j;
|
|
190
|
+
|
|
191
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
192
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
193
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
194
|
+
matrix[i][j] = Math.min(matrix[i - 1][j] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j - 1] + cost);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return matrix[a.length][b.length];
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const clamp01 = (value) => Math.max(0, Math.min(1, Number(value) || 0));
|
|
202
|
+
|
|
203
|
+
const uniqueTokens = (tokens = []) => Array.from(new Set((Array.isArray(tokens) ? tokens : []).filter(Boolean)));
|
|
204
|
+
|
|
205
|
+
const computeStringSimilarity = (left, right) => {
|
|
206
|
+
const a = normalizeText(left);
|
|
207
|
+
const b = normalizeText(right);
|
|
208
|
+
if (!a || !b) return 0;
|
|
209
|
+
if (a === b) return 1;
|
|
210
|
+
const distance = levenshteinDistance(a, b);
|
|
211
|
+
const maxLength = Math.max(a.length, b.length, 1);
|
|
212
|
+
return clamp01(1 - distance / maxLength);
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
const computeTokenOverlapRatio = (leftTokens = [], rightTokens = []) => {
|
|
216
|
+
const left = uniqueTokens(leftTokens);
|
|
217
|
+
const rightSet = new Set(uniqueTokens(rightTokens));
|
|
218
|
+
if (!left.length || !rightSet.size) return 0;
|
|
219
|
+
let hits = 0;
|
|
220
|
+
for (const token of left) {
|
|
221
|
+
if (rightSet.has(token)) hits += 1;
|
|
222
|
+
}
|
|
223
|
+
return clamp01(hits / left.length);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const stringifyFeedbackCommand = (value) => normalizeText(value).replace(/^\/+/, '');
|
|
227
|
+
|
|
228
|
+
const createEmptyFeedbackStore = () => ({
|
|
229
|
+
version: GLOBAL_HELP_FEEDBACK_CACHE_VERSION,
|
|
230
|
+
updatedAt: null,
|
|
231
|
+
byCommand: {},
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let feedbackStoreCache = null;
|
|
235
|
+
let feedbackLoadPromise = null;
|
|
236
|
+
let feedbackWriteChain = Promise.resolve();
|
|
237
|
+
let offlineFaqSchedulerStarted = false;
|
|
238
|
+
let offlineFaqSchedulerHandle = null;
|
|
239
|
+
|
|
240
|
+
const withFeedbackWrite = async (writer) => {
|
|
241
|
+
feedbackWriteChain = feedbackWriteChain
|
|
242
|
+
.then(async () => {
|
|
243
|
+
await fs.mkdir(path.dirname(GLOBAL_HELP_FEEDBACK_FILE_PATH), { recursive: true });
|
|
244
|
+
return writer();
|
|
245
|
+
})
|
|
246
|
+
.catch((error) => {
|
|
247
|
+
logger.warn('Falha ao persistir feedback global de ajuda.', {
|
|
248
|
+
action: 'global_help_feedback_write_failed',
|
|
249
|
+
error: error?.message,
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
return feedbackWriteChain;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const ensureFeedbackStoreLoaded = async () => {
|
|
256
|
+
if (feedbackStoreCache) return feedbackStoreCache;
|
|
257
|
+
if (feedbackLoadPromise) return feedbackLoadPromise;
|
|
258
|
+
|
|
259
|
+
feedbackLoadPromise = (async () => {
|
|
260
|
+
try {
|
|
261
|
+
const raw = await fs.readFile(GLOBAL_HELP_FEEDBACK_FILE_PATH, 'utf8');
|
|
262
|
+
const parsed = JSON.parse(raw);
|
|
263
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
264
|
+
feedbackStoreCache = createEmptyFeedbackStore();
|
|
265
|
+
return feedbackStoreCache;
|
|
266
|
+
}
|
|
267
|
+
feedbackStoreCache = {
|
|
268
|
+
...createEmptyFeedbackStore(),
|
|
269
|
+
...parsed,
|
|
270
|
+
byCommand: parsed.byCommand && typeof parsed.byCommand === 'object' ? parsed.byCommand : {},
|
|
271
|
+
};
|
|
272
|
+
return feedbackStoreCache;
|
|
273
|
+
} catch {
|
|
274
|
+
feedbackStoreCache = createEmptyFeedbackStore();
|
|
275
|
+
return feedbackStoreCache;
|
|
276
|
+
} finally {
|
|
277
|
+
feedbackLoadPromise = null;
|
|
278
|
+
}
|
|
279
|
+
})();
|
|
280
|
+
|
|
281
|
+
return feedbackLoadPromise;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const persistFeedbackStore = async () => {
|
|
285
|
+
const store = await ensureFeedbackStoreLoaded();
|
|
286
|
+
store.updatedAt = new Date().toISOString();
|
|
287
|
+
await withFeedbackWrite(async () => {
|
|
288
|
+
await fs.writeFile(GLOBAL_HELP_FEEDBACK_FILE_PATH, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const ensureFeedbackCommandEntry = (store, commandName) => {
|
|
293
|
+
const safeName = stringifyFeedbackCommand(commandName);
|
|
294
|
+
if (!safeName) return null;
|
|
295
|
+
const current = store.byCommand[safeName] && typeof store.byCommand[safeName] === 'object' ? store.byCommand[safeName] : null;
|
|
296
|
+
|
|
297
|
+
if (current) {
|
|
298
|
+
current.success_count = Number(current.success_count || 0);
|
|
299
|
+
current.miss_count = Number(current.miss_count || 0);
|
|
300
|
+
current.last_updated_at = current.last_updated_at || null;
|
|
301
|
+
return current;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const created = {
|
|
305
|
+
success_count: 0,
|
|
306
|
+
miss_count: 0,
|
|
307
|
+
last_updated_at: null,
|
|
308
|
+
};
|
|
309
|
+
store.byCommand[safeName] = created;
|
|
310
|
+
return created;
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
const computeFeedbackScore = (feedbackStore, commandName) => {
|
|
314
|
+
const safeName = stringifyFeedbackCommand(commandName);
|
|
315
|
+
if (!safeName || !feedbackStore?.byCommand || typeof feedbackStore.byCommand !== 'object') return 0;
|
|
316
|
+
|
|
317
|
+
const stats = feedbackStore.byCommand[safeName];
|
|
318
|
+
if (!stats || typeof stats !== 'object') return 0;
|
|
319
|
+
|
|
320
|
+
const success = Number(stats.success_count || 0);
|
|
321
|
+
const miss = Number(stats.miss_count || 0);
|
|
322
|
+
const total = Math.max(0, success + miss);
|
|
323
|
+
if (!total) return 0;
|
|
324
|
+
|
|
325
|
+
const precision = (success + 1) / (total + 2);
|
|
326
|
+
const evidenceWeight = Math.min(1, total / 10);
|
|
327
|
+
return clamp01(precision * evidenceWeight);
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const formatPreConditions = (requirements = {}) => {
|
|
331
|
+
const lines = [];
|
|
332
|
+
if (requirements.require_group) lines.push('- Requer ser executado em grupo.');
|
|
333
|
+
if (requirements.require_group_admin) lines.push('- Requer permissao de admin do grupo.');
|
|
334
|
+
if (requirements.require_bot_owner) lines.push('- Requer admin principal do bot.');
|
|
335
|
+
if (requirements.require_google_login) lines.push('- Pode requerer login vinculado ao site.');
|
|
336
|
+
if (requirements.require_nsfw_enabled) lines.push('- Requer NSFW ativo quando aplicavel.');
|
|
337
|
+
if (requirements.require_media) lines.push('- Requer midia anexada/citada quando aplicavel.');
|
|
338
|
+
if (requirements.require_reply_message) lines.push('- Requer resposta/citacao de mensagem quando aplicavel.');
|
|
339
|
+
return lines;
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const buildGlobalCommandCacheKey = (commandName) => `explicar comando ${stringifyFeedbackCommand(commandName)}`;
|
|
343
|
+
|
|
344
|
+
const computeCachedQuestionMatchScore = (questionNormalized, candidateQuestion) => {
|
|
345
|
+
const left = normalizeText(questionNormalized);
|
|
346
|
+
const right = normalizeText(candidateQuestion);
|
|
347
|
+
if (!left || !right) return 0;
|
|
348
|
+
if (left === right) return 1;
|
|
349
|
+
|
|
350
|
+
const leftTokens = tokenizeText(left);
|
|
351
|
+
const rightTokens = tokenizeText(right);
|
|
352
|
+
const overlap = computeTokenOverlapRatio(leftTokens, rightTokens);
|
|
353
|
+
const contains = left.includes(right) || right.includes(left) ? 1 : 0;
|
|
354
|
+
const similarity = computeStringSimilarity(left, right);
|
|
355
|
+
return clamp01(contains * 0.45 + overlap * 0.35 + similarity * 0.2);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const isLikelyGenericBotQuestion = (normalizedQuestion) => /\b(o que (voce|vc|o bot) faz|como funciona|me ajuda|quais comandos|menu)\b/.test(String(normalizedQuestion || ''));
|
|
359
|
+
|
|
360
|
+
const resolveGlobalCommandTarget = (command) => {
|
|
361
|
+
const normalized = String(command || '')
|
|
362
|
+
.trim()
|
|
363
|
+
.toLowerCase();
|
|
364
|
+
if (!normalized) return null;
|
|
365
|
+
|
|
366
|
+
for (const wrapper of GLOBAL_HELP_WRAPPERS) {
|
|
367
|
+
const canonical = wrapper.resolveCommandName(normalized);
|
|
368
|
+
if (canonical) {
|
|
369
|
+
return {
|
|
370
|
+
moduleKey: wrapper.moduleKey,
|
|
371
|
+
wrapper,
|
|
372
|
+
commandName: canonical,
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return null;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const detectCommandTargetFromText = (text, commandPrefix = '/') => {
|
|
381
|
+
const prefix = String(commandPrefix || '/').trim() || '/';
|
|
382
|
+
const tokens = tokenizeText(text);
|
|
383
|
+
for (const token of tokens) {
|
|
384
|
+
const cleaned = token.startsWith(prefix) ? token.slice(prefix.length) : token;
|
|
385
|
+
if (!cleaned) continue;
|
|
386
|
+
const target = resolveGlobalCommandTarget(cleaned);
|
|
387
|
+
if (target) return target;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return null;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const collectKnownTokens = () => {
|
|
394
|
+
const tokens = [];
|
|
395
|
+
for (const wrapper of GLOBAL_HELP_WRAPPERS) {
|
|
396
|
+
const entries = wrapper.listEnabledCommands();
|
|
397
|
+
for (const entry of entries) {
|
|
398
|
+
const canonical = normalizeText(entry?.name).replace(/^\/+/, '');
|
|
399
|
+
if (!canonical) continue;
|
|
400
|
+
tokens.push({
|
|
401
|
+
token: canonical,
|
|
402
|
+
canonical,
|
|
403
|
+
moduleKey: wrapper.moduleKey,
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const aliases = Array.isArray(entry?.aliases) ? entry.aliases : [];
|
|
407
|
+
for (const alias of aliases) {
|
|
408
|
+
const normalizedAlias = normalizeText(alias);
|
|
409
|
+
if (!normalizedAlias) continue;
|
|
410
|
+
tokens.push({
|
|
411
|
+
token: normalizedAlias,
|
|
412
|
+
canonical,
|
|
413
|
+
moduleKey: wrapper.moduleKey,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return tokens;
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const collectCommandSearchRecords = () => {
|
|
422
|
+
const records = [];
|
|
423
|
+
|
|
424
|
+
for (const wrapper of GLOBAL_HELP_WRAPPERS) {
|
|
425
|
+
const entries = wrapper.listEnabledCommands();
|
|
426
|
+
for (const entry of entries) {
|
|
427
|
+
const commandName = normalizeText(entry?.name);
|
|
428
|
+
if (!commandName) continue;
|
|
429
|
+
|
|
430
|
+
const aliases = ensureArray(entry.aliases)
|
|
431
|
+
.map((value) => normalizeText(value))
|
|
432
|
+
.filter(Boolean);
|
|
433
|
+
const capabilityKeywords = readEntryKeywords(entry)
|
|
434
|
+
.map((value) => normalizeText(value))
|
|
435
|
+
.filter(Boolean);
|
|
436
|
+
const faqPatterns = readEntryFaqQueries(entry)
|
|
437
|
+
.map((value) => normalizeText(value))
|
|
438
|
+
.filter(Boolean);
|
|
439
|
+
const userPhrasings = readEntryUserPhrasings(entry)
|
|
440
|
+
.map((value) => normalizeText(value))
|
|
441
|
+
.filter(Boolean);
|
|
442
|
+
const description = readEntryDescription(entry);
|
|
443
|
+
|
|
444
|
+
records.push({
|
|
445
|
+
moduleKey: wrapper.moduleKey,
|
|
446
|
+
wrapper,
|
|
447
|
+
commandName,
|
|
448
|
+
entry,
|
|
449
|
+
aliases,
|
|
450
|
+
capabilityKeywords,
|
|
451
|
+
faqPatterns,
|
|
452
|
+
userPhrasings,
|
|
453
|
+
description: normalizeText(description),
|
|
454
|
+
descriptionTokens: tokenizeText(description),
|
|
455
|
+
category: normalizeText(readEntryCategory(entry)),
|
|
456
|
+
suggestionPriority: readEntrySuggestionPriority(entry),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return records;
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
const repeatPush = (target, values, factor = 1) => {
|
|
465
|
+
const safeFactor = Math.max(1, Number.parseInt(String(factor ?? 1), 10) || 1);
|
|
466
|
+
const safeValues = Array.isArray(values) ? values : [];
|
|
467
|
+
for (let i = 0; i < safeFactor; i += 1) {
|
|
468
|
+
target.push(...safeValues.filter(Boolean));
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const buildRecordDocumentTokens = (record) => {
|
|
473
|
+
const tokens = [];
|
|
474
|
+
const commandTokens = tokenizeText(record.commandName);
|
|
475
|
+
const aliasesTokens = record.aliases.flatMap((value) => tokenizeText(value));
|
|
476
|
+
const capabilityTokens = record.capabilityKeywords.flatMap((value) => tokenizeText(value));
|
|
477
|
+
const faqTokens = record.faqPatterns.flatMap((value) => tokenizeText(value));
|
|
478
|
+
const phrasingTokens = record.userPhrasings.flatMap((value) => tokenizeText(value));
|
|
479
|
+
const descriptionTokens = Array.isArray(record.descriptionTokens) ? record.descriptionTokens : [];
|
|
480
|
+
const categoryTokens = tokenizeText(record.category);
|
|
481
|
+
|
|
482
|
+
repeatPush(tokens, commandTokens, 6);
|
|
483
|
+
repeatPush(tokens, aliasesTokens, 4);
|
|
484
|
+
repeatPush(tokens, capabilityTokens, 3);
|
|
485
|
+
repeatPush(tokens, faqTokens, 2);
|
|
486
|
+
repeatPush(tokens, phrasingTokens, 2);
|
|
487
|
+
repeatPush(tokens, descriptionTokens, 1);
|
|
488
|
+
repeatPush(tokens, categoryTokens, 1);
|
|
489
|
+
|
|
490
|
+
return tokens.filter(Boolean);
|
|
491
|
+
};
|
|
492
|
+
|
|
493
|
+
const buildSearchIndex = (records) => {
|
|
494
|
+
const docs = [];
|
|
495
|
+
const docFrequency = new Map();
|
|
496
|
+
let totalDocLength = 0;
|
|
497
|
+
|
|
498
|
+
for (const record of records) {
|
|
499
|
+
const documentTokens = buildRecordDocumentTokens(record);
|
|
500
|
+
const tf = new Map();
|
|
501
|
+
for (const token of documentTokens) {
|
|
502
|
+
tf.set(token, Number(tf.get(token) || 0) + 1);
|
|
503
|
+
}
|
|
504
|
+
const unique = new Set(documentTokens);
|
|
505
|
+
for (const token of unique) {
|
|
506
|
+
docFrequency.set(token, Number(docFrequency.get(token) || 0) + 1);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const docLength = documentTokens.length;
|
|
510
|
+
totalDocLength += docLength;
|
|
511
|
+
docs.push({
|
|
512
|
+
...record,
|
|
513
|
+
documentTokens,
|
|
514
|
+
tokenFrequency: tf,
|
|
515
|
+
tokenSet: unique,
|
|
516
|
+
docLength,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const avgDocLength = docs.length ? totalDocLength / docs.length : 1;
|
|
521
|
+
return {
|
|
522
|
+
docs,
|
|
523
|
+
docFrequency,
|
|
524
|
+
docCount: docs.length,
|
|
525
|
+
avgDocLength: avgDocLength || 1,
|
|
526
|
+
};
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const computeBm25Score = (doc, queryTokens, searchIndex) => {
|
|
530
|
+
const tokens = uniqueTokens(queryTokens);
|
|
531
|
+
if (!tokens.length || !searchIndex.docCount) return 0;
|
|
532
|
+
let score = 0;
|
|
533
|
+
|
|
534
|
+
for (const token of tokens) {
|
|
535
|
+
const tf = Number(doc.tokenFrequency.get(token) || 0);
|
|
536
|
+
if (!tf) continue;
|
|
537
|
+
const df = Number(searchIndex.docFrequency.get(token) || 0);
|
|
538
|
+
const idf = Math.log(1 + (searchIndex.docCount - df + 0.5) / (df + 0.5));
|
|
539
|
+
const denominator = tf + BM25_K1 * (1 - BM25_B + BM25_B * (doc.docLength / searchIndex.avgDocLength));
|
|
540
|
+
score += idf * ((tf * (BM25_K1 + 1)) / Math.max(1e-9, denominator));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return Math.max(0, score);
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const computeOverlapScore = (doc, questionNormalized, queryTokens, commandPrefix = '/') => {
|
|
547
|
+
const overlapByTokens = computeTokenOverlapRatio(queryTokens, doc.documentTokens);
|
|
548
|
+
const prefix = String(commandPrefix || '/').trim() || '/';
|
|
549
|
+
const explicitCommand = questionNormalized.includes(`${prefix}${doc.commandName}`) ? 1 : 0;
|
|
550
|
+
const exactCommand = queryTokens.includes(doc.commandName) ? 1 : 0;
|
|
551
|
+
return clamp01(Math.max(overlapByTokens, explicitCommand, exactCommand * 0.9));
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const computeFuzzyScore = (doc, questionNormalized, queryTokens) => {
|
|
555
|
+
const commandSimilarity = computeStringSimilarity(questionNormalized, doc.commandName);
|
|
556
|
+
const aliasSimilarity = doc.aliases.reduce((best, alias) => Math.max(best, computeStringSimilarity(questionNormalized, alias)), 0);
|
|
557
|
+
|
|
558
|
+
const commandTokens = uniqueTokens([...tokenizeText(doc.commandName), ...doc.aliases.flatMap((alias) => tokenizeText(alias))]);
|
|
559
|
+
const tokenSimilarities = [];
|
|
560
|
+
for (const qToken of uniqueTokens(queryTokens)) {
|
|
561
|
+
let best = 0;
|
|
562
|
+
for (const cToken of commandTokens) {
|
|
563
|
+
best = Math.max(best, computeStringSimilarity(qToken, cToken));
|
|
564
|
+
if (best >= 1) break;
|
|
565
|
+
}
|
|
566
|
+
tokenSimilarities.push(best);
|
|
567
|
+
}
|
|
568
|
+
const tokenAverage = tokenSimilarities.length ? tokenSimilarities.reduce((acc, value) => acc + value, 0) / tokenSimilarities.length : 0;
|
|
569
|
+
|
|
570
|
+
return clamp01(Math.max(commandSimilarity, aliasSimilarity, tokenAverage));
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
const rankCommandRecords = async ({ questionNormalized, commandPrefix = '/' } = {}) => {
|
|
574
|
+
const records = collectCommandSearchRecords();
|
|
575
|
+
const queryTokens = tokenizeText(questionNormalized);
|
|
576
|
+
if (!records.length || !queryTokens.length) return [];
|
|
577
|
+
|
|
578
|
+
const feedbackStore = await ensureFeedbackStoreLoaded();
|
|
579
|
+
const searchIndex = buildSearchIndex(records);
|
|
580
|
+
const rankedRaw = searchIndex.docs.map((doc) => ({
|
|
581
|
+
...doc,
|
|
582
|
+
bm25Raw: computeBm25Score(doc, queryTokens, searchIndex),
|
|
583
|
+
overlapScore: computeOverlapScore(doc, questionNormalized, queryTokens, commandPrefix),
|
|
584
|
+
fuzzyScore: computeFuzzyScore(doc, questionNormalized, queryTokens),
|
|
585
|
+
feedbackScore: computeFeedbackScore(feedbackStore, doc.commandName),
|
|
586
|
+
legacyScore: scoreCommandRecord(doc, questionNormalized, queryTokens, commandPrefix),
|
|
587
|
+
}));
|
|
588
|
+
|
|
589
|
+
const maxBm25 = Math.max(1e-9, ...rankedRaw.map((item) => item.bm25Raw));
|
|
590
|
+
|
|
591
|
+
return rankedRaw
|
|
592
|
+
.map((item) => {
|
|
593
|
+
const bm25Score = clamp01(item.bm25Raw / maxBm25);
|
|
594
|
+
const finalScore = BM25_WEIGHT * bm25Score + OVERLAP_WEIGHT * item.overlapScore + FUZZY_WEIGHT * item.fuzzyScore + FEEDBACK_WEIGHT * item.feedbackScore;
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
...item,
|
|
598
|
+
bm25Score,
|
|
599
|
+
finalScore: clamp01(finalScore),
|
|
600
|
+
};
|
|
601
|
+
})
|
|
602
|
+
.sort((a, b) => {
|
|
603
|
+
if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
|
|
604
|
+
if (b.legacyScore !== a.legacyScore) return b.legacyScore - a.legacyScore;
|
|
605
|
+
if (b.suggestionPriority !== a.suggestionPriority) return b.suggestionPriority - a.suggestionPriority;
|
|
606
|
+
return a.commandName.localeCompare(b.commandName);
|
|
607
|
+
});
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
const scoreCommandRecord = (record, questionNormalized, questionTokens, commandPrefix = '/') => {
|
|
611
|
+
let score = 0;
|
|
612
|
+
const prefix = String(commandPrefix || '/').trim() || '/';
|
|
613
|
+
|
|
614
|
+
if (questionNormalized.includes(`${prefix}${record.commandName}`)) {
|
|
615
|
+
score += 90;
|
|
616
|
+
}
|
|
617
|
+
if (questionTokens.includes(record.commandName)) {
|
|
618
|
+
score += 70;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
for (const alias of record.aliases) {
|
|
622
|
+
if (questionTokens.includes(alias)) {
|
|
623
|
+
score += 55;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let capabilityHits = 0;
|
|
629
|
+
for (const keyword of record.capabilityKeywords) {
|
|
630
|
+
if (questionNormalized.includes(keyword)) capabilityHits += 1;
|
|
631
|
+
}
|
|
632
|
+
score += Math.min(72, capabilityHits * 24);
|
|
633
|
+
|
|
634
|
+
let faqHits = 0;
|
|
635
|
+
for (const pattern of record.faqPatterns) {
|
|
636
|
+
if (questionNormalized.includes(pattern)) faqHits += 1;
|
|
637
|
+
}
|
|
638
|
+
score += Math.min(60, faqHits * 30);
|
|
639
|
+
|
|
640
|
+
let phrasingHits = 0;
|
|
641
|
+
for (const phrasing of record.userPhrasings) {
|
|
642
|
+
if (questionNormalized.includes(phrasing)) phrasingHits += 1;
|
|
643
|
+
}
|
|
644
|
+
score += Math.min(48, phrasingHits * 16);
|
|
645
|
+
|
|
646
|
+
const descriptionOverlap = record.descriptionTokens.filter((token) => questionTokens.includes(token));
|
|
647
|
+
score += Math.min(12, descriptionOverlap.length * 2);
|
|
648
|
+
|
|
649
|
+
if (record.category && questionTokens.includes(record.category)) {
|
|
650
|
+
score += 18;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const priorityBoost = Math.max(0, Math.min(20, Math.floor(record.suggestionPriority / 10)));
|
|
654
|
+
score += priorityBoost;
|
|
655
|
+
|
|
656
|
+
return score;
|
|
657
|
+
};
|
|
658
|
+
|
|
659
|
+
const buildDeterministicCommandAnswer = ({ entry, commandName, commandPrefix = '/', suggestions = [], intro }) => {
|
|
660
|
+
const usage = readEntryUsage(entry).map((method) => renderUsage(method, commandPrefix));
|
|
661
|
+
const description = readEntryDescription(entry) || 'Sem descricao cadastrada.';
|
|
662
|
+
const permissionLabel = formatPermissionLabel(readEntryPermission(entry));
|
|
663
|
+
const whereLabel = formatWhereLabel(readEntryContexts(entry));
|
|
664
|
+
const limitLabel = readEntryUsageLimit(entry) || 'nao informado';
|
|
665
|
+
const preconditions = formatPreConditions(readEntryRequirements(entry));
|
|
666
|
+
|
|
667
|
+
const lines = [intro || `🤖 Posso te orientar sobre *${commandPrefix}${commandName}*.`, `📝 ${description}`, '', `👤 *Quem pode usar:* ${permissionLabel}`, `📍 *Onde pode usar:* ${whereLabel}`, `⏱️ *Limite:* ${limitLabel}`, '', '*Como usar:*', ...(usage.length ? usage.map((line) => `- ${line}`) : [`- ${commandPrefix}${commandName}`]), ...(preconditions.length ? ['', '*Pre-condicoes:*', ...preconditions] : []), '', '🔒 Esta resposta e apenas orientacao; nenhum comando foi executado.'];
|
|
668
|
+
|
|
669
|
+
const normalizedSuggestions = limitArray(
|
|
670
|
+
suggestions.map((value) => String(value || '').trim()).filter((value) => value && value !== `${commandPrefix}${commandName}`),
|
|
671
|
+
3,
|
|
672
|
+
);
|
|
673
|
+
if (normalizedSuggestions.length) {
|
|
674
|
+
lines.push('');
|
|
675
|
+
lines.push(`Sugestoes relacionadas: ${normalizedSuggestions.join(', ')}`);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return lines.join('\n');
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
const explainTargetWithFallback = async ({ target, context = {}, intro, suggestions = [] }) => {
|
|
682
|
+
const commandPrefix = context.commandPrefix || '/';
|
|
683
|
+
const entry = typeof target?.wrapper?.getCommandEntry === 'function' ? target.wrapper.getCommandEntry(target.commandName) : null;
|
|
684
|
+
|
|
685
|
+
if (entry) {
|
|
686
|
+
return {
|
|
687
|
+
ok: true,
|
|
688
|
+
source: 'deterministic',
|
|
689
|
+
moduleKey: target.moduleKey,
|
|
690
|
+
commandName: target.commandName,
|
|
691
|
+
text: buildDeterministicCommandAnswer({
|
|
692
|
+
entry,
|
|
693
|
+
commandName: target.commandName,
|
|
694
|
+
commandPrefix,
|
|
695
|
+
intro,
|
|
696
|
+
suggestions,
|
|
697
|
+
}),
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (typeof target?.wrapper?.explicarComando === 'function') {
|
|
702
|
+
try {
|
|
703
|
+
const explanation = await target.wrapper.explicarComando(target.commandName, context);
|
|
704
|
+
return {
|
|
705
|
+
...explanation,
|
|
706
|
+
ok: Boolean(explanation?.ok),
|
|
707
|
+
source: explanation?.source || 'module',
|
|
708
|
+
moduleKey: target.moduleKey,
|
|
709
|
+
commandName: explanation?.commandName || target.commandName,
|
|
710
|
+
text: String(explanation?.text || '').trim() || `Nao consegui montar a explicacao para ${commandPrefix}${target.commandName}.`,
|
|
711
|
+
};
|
|
712
|
+
} catch (error) {
|
|
713
|
+
logger.warn('Falha ao explicar comando pelo wrapper global.', {
|
|
714
|
+
action: 'global_command_explain_wrapper_failed',
|
|
715
|
+
module: target.moduleKey,
|
|
716
|
+
command: target.commandName,
|
|
717
|
+
error: error?.message,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
ok: false,
|
|
724
|
+
source: 'none',
|
|
725
|
+
moduleKey: target.moduleKey,
|
|
726
|
+
commandName: target.commandName,
|
|
727
|
+
text: `Nao consegui gerar a ajuda para ${commandPrefix}${target.commandName} agora.`,
|
|
728
|
+
};
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
const buildConversationFallbackText = ({ commandPrefix = '/', suggestions = [] }) => {
|
|
732
|
+
const normalizedSuggestions = limitArray(
|
|
733
|
+
suggestions
|
|
734
|
+
.map((value) => String(value || '').trim())
|
|
735
|
+
.filter(Boolean)
|
|
736
|
+
.map((value) => (value.startsWith(commandPrefix) ? value : `${commandPrefix}${value}`)),
|
|
737
|
+
5,
|
|
738
|
+
);
|
|
739
|
+
|
|
740
|
+
return ['Ainda nao identifiquei exatamente o comando que voce precisa.', `Voce pode perguntar direto, por exemplo: "${commandPrefix}help <comando>" ou "como usar sticker".`, normalizedSuggestions.length ? `Sugestoes rapidas: ${normalizedSuggestions.join(', ')}` : `Use ${commandPrefix}menu para ver os comandos gerais e ${commandPrefix}menuadm para comandos administrativos.`].join('\n');
|
|
741
|
+
};
|
|
742
|
+
|
|
743
|
+
const buildCachedGlobalResponse = ({ row, intentType = 'cached', sourceOverride = null, suggestions = [] } = {}) => {
|
|
744
|
+
const safeText = String(row?.answer_text || '').trim();
|
|
745
|
+
if (!safeText) return null;
|
|
746
|
+
|
|
747
|
+
return {
|
|
748
|
+
ok: true,
|
|
749
|
+
source: sourceOverride || row?.source || 'db_cache',
|
|
750
|
+
moduleKey: row?.module_key || null,
|
|
751
|
+
commandName: row?.command_name || null,
|
|
752
|
+
intentType,
|
|
753
|
+
suggestions: limitArray(suggestions, 5),
|
|
754
|
+
text: safeText,
|
|
755
|
+
};
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
const lookupGlobalCacheByQuestion = async ({ rawQuestion, normalizedQuestion, scope }) => {
|
|
759
|
+
const exact = await getAiHelpCachedResponse({
|
|
760
|
+
moduleKey: GLOBAL_HELP_CACHE_MODULE_KEY,
|
|
761
|
+
scope,
|
|
762
|
+
question: rawQuestion,
|
|
763
|
+
normalizedQuestion,
|
|
764
|
+
updateUsage: true,
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
if (exact?.answer_text) {
|
|
768
|
+
return {
|
|
769
|
+
row: exact,
|
|
770
|
+
cacheKind: 'exact',
|
|
771
|
+
score: 1,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const candidates = await listAiHelpCachedResponses({
|
|
776
|
+
moduleKey: GLOBAL_HELP_CACHE_MODULE_KEY,
|
|
777
|
+
scope,
|
|
778
|
+
limit: GLOBAL_HELP_DB_CACHE_FUZZY_LIMIT,
|
|
779
|
+
});
|
|
780
|
+
if (!Array.isArray(candidates) || !candidates.length) return null;
|
|
781
|
+
|
|
782
|
+
const ranked = candidates
|
|
783
|
+
.map((row) => ({
|
|
784
|
+
row,
|
|
785
|
+
score: computeCachedQuestionMatchScore(normalizedQuestion, row?.normalized_question),
|
|
786
|
+
}))
|
|
787
|
+
.filter((item) => item.score >= GLOBAL_HELP_DB_CACHE_FUZZY_THRESHOLD)
|
|
788
|
+
.sort((a, b) => b.score - a.score);
|
|
789
|
+
|
|
790
|
+
if (!ranked.length) return null;
|
|
791
|
+
return {
|
|
792
|
+
row: ranked[0].row,
|
|
793
|
+
cacheKind: 'fuzzy',
|
|
794
|
+
score: ranked[0].score,
|
|
795
|
+
};
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const saveGlobalCacheAnswer = async ({ scope = GLOBAL_HELP_CACHE_SCOPE_QUESTION, question, answer, commandName = null, source = 'deterministic', intentType = null, confidence = null, suggestions = [], metadata = null } = {}) => {
|
|
799
|
+
const rawQuestion = String(question || '').trim();
|
|
800
|
+
const rawAnswer = String(answer || '').trim();
|
|
801
|
+
if (!rawQuestion || !rawAnswer) return false;
|
|
802
|
+
|
|
803
|
+
const safeSource =
|
|
804
|
+
String(source || 'deterministic')
|
|
805
|
+
.trim()
|
|
806
|
+
.slice(0, 32) || 'deterministic';
|
|
807
|
+
return upsertAiHelpCachedResponse({
|
|
808
|
+
moduleKey: GLOBAL_HELP_CACHE_MODULE_KEY,
|
|
809
|
+
scope,
|
|
810
|
+
question: rawQuestion,
|
|
811
|
+
normalizedQuestion: normalizeText(rawQuestion),
|
|
812
|
+
answer: rawAnswer,
|
|
813
|
+
source: safeSource,
|
|
814
|
+
commandName: commandName || null,
|
|
815
|
+
metadata: {
|
|
816
|
+
intentType: intentType || null,
|
|
817
|
+
confidence: typeof confidence === 'number' ? confidence : null,
|
|
818
|
+
suggestions: limitArray(suggestions, 5),
|
|
819
|
+
...(metadata && typeof metadata === 'object' ? metadata : {}),
|
|
820
|
+
},
|
|
821
|
+
});
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
const maybeUseWrapperQuestionFallback = async ({ target, rawQuestion, context = {} } = {}) => {
|
|
825
|
+
if (!GLOBAL_HELP_ENABLE_WRAPPER_LLM_FALLBACK) return null;
|
|
826
|
+
if (typeof target?.wrapper?.responderPergunta !== 'function') return null;
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
const response = await target.wrapper.responderPergunta(rawQuestion, context);
|
|
830
|
+
const safeText = String(response?.text || '').trim();
|
|
831
|
+
if (!safeText) return null;
|
|
832
|
+
return {
|
|
833
|
+
ok: Boolean(response?.ok ?? true),
|
|
834
|
+
source: response?.source || 'module_question_fallback',
|
|
835
|
+
moduleKey: target.moduleKey,
|
|
836
|
+
commandName: response?.commandName || target.commandName || null,
|
|
837
|
+
text: safeText,
|
|
838
|
+
};
|
|
839
|
+
} catch (error) {
|
|
840
|
+
logger.warn('Falha no fallback de pergunta via wrapper do modulo.', {
|
|
841
|
+
action: 'global_wrapper_question_fallback_failed',
|
|
842
|
+
module: target?.moduleKey || null,
|
|
843
|
+
command: target?.commandName || null,
|
|
844
|
+
error: error?.message,
|
|
845
|
+
});
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
};
|
|
849
|
+
|
|
850
|
+
const runGlobalOfflineFaqGeneration = async ({ reason = 'scheduler', force = true } = {}) => {
|
|
851
|
+
const outputs = [];
|
|
852
|
+
for (const wrapper of GLOBAL_HELP_WRAPPERS) {
|
|
853
|
+
if (typeof wrapper?.gerarFaqAutomatica !== 'function') continue;
|
|
854
|
+
try {
|
|
855
|
+
const result = await wrapper.gerarFaqAutomatica({
|
|
856
|
+
commandPrefix: '/',
|
|
857
|
+
force,
|
|
858
|
+
reason: `global_${reason}`,
|
|
859
|
+
});
|
|
860
|
+
outputs.push({
|
|
861
|
+
moduleKey: wrapper.moduleKey,
|
|
862
|
+
ok: Boolean(result?.ok),
|
|
863
|
+
commandCount: Number(result?.commandCount || 0),
|
|
864
|
+
faqCount: Number(result?.faqCount || 0),
|
|
865
|
+
});
|
|
866
|
+
} catch (error) {
|
|
867
|
+
logger.warn('Falha ao gerar FAQ offline global por modulo.', {
|
|
868
|
+
action: 'global_offline_faq_module_failed',
|
|
869
|
+
module: wrapper?.moduleKey || null,
|
|
870
|
+
error: error?.message,
|
|
871
|
+
});
|
|
872
|
+
outputs.push({
|
|
873
|
+
moduleKey: wrapper?.moduleKey || null,
|
|
874
|
+
ok: false,
|
|
875
|
+
commandCount: 0,
|
|
876
|
+
faqCount: 0,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return outputs;
|
|
881
|
+
};
|
|
882
|
+
|
|
883
|
+
const ensureGlobalOfflineFaqScheduler = () => {
|
|
884
|
+
if (!GLOBAL_HELP_OFFLINE_FAQ_ENABLED || offlineFaqSchedulerStarted) return;
|
|
885
|
+
offlineFaqSchedulerStarted = true;
|
|
886
|
+
|
|
887
|
+
const run = async () => {
|
|
888
|
+
try {
|
|
889
|
+
await runGlobalOfflineFaqGeneration({ reason: 'scheduler', force: true });
|
|
890
|
+
} catch (error) {
|
|
891
|
+
logger.warn('Falha no scheduler de FAQ offline global.', {
|
|
892
|
+
action: 'global_offline_faq_scheduler_failed',
|
|
893
|
+
error: error?.message,
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
run();
|
|
899
|
+
offlineFaqSchedulerHandle = setInterval(run, GLOBAL_HELP_OFFLINE_FAQ_INTERVAL_MS);
|
|
900
|
+
if (typeof offlineFaqSchedulerHandle?.unref === 'function') {
|
|
901
|
+
offlineFaqSchedulerHandle.unref();
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
|
|
905
|
+
const getTopDiscoverySuggestions = async ({ questionNormalized = '', commandPrefix = '/', limit = 5 } = {}) => {
|
|
906
|
+
const ranked = await rankCommandRecords({
|
|
907
|
+
questionNormalized,
|
|
908
|
+
commandPrefix,
|
|
909
|
+
});
|
|
910
|
+
|
|
911
|
+
const suggestions = [];
|
|
912
|
+
for (const item of ranked) {
|
|
913
|
+
const token = `${commandPrefix}${item.commandName}`;
|
|
914
|
+
if (!suggestions.includes(token)) suggestions.push(token);
|
|
915
|
+
if (suggestions.length >= limit) break;
|
|
916
|
+
}
|
|
917
|
+
return suggestions;
|
|
918
|
+
};
|
|
919
|
+
|
|
920
|
+
const buildFallbackUnknownSuggestion = (rawCommand, { commandPrefix = '/' } = {}) => {
|
|
921
|
+
for (const wrapper of GLOBAL_HELP_WRAPPERS) {
|
|
922
|
+
try {
|
|
923
|
+
const suggestion = wrapper.buildUnknownCommandSuggestion(rawCommand, { commandPrefix });
|
|
924
|
+
if (suggestion) return suggestion;
|
|
925
|
+
} catch (error) {
|
|
926
|
+
logger.warn('Falha ao gerar sugestao de comando desconhecido por modulo.', {
|
|
927
|
+
action: 'global_unknown_suggestion_wrapper_failed',
|
|
928
|
+
module: wrapper.moduleKey,
|
|
929
|
+
rawCommand,
|
|
930
|
+
error: error?.message,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
return null;
|
|
935
|
+
};
|
|
936
|
+
|
|
937
|
+
export const buildGlobalUnknownCommandSuggestion = (rawCommand, { commandPrefix = '/' } = {}) => {
|
|
938
|
+
const normalized = normalizeText(rawCommand).replace(/^\/+/, '');
|
|
939
|
+
if (!normalized) return null;
|
|
940
|
+
|
|
941
|
+
const tokens = collectKnownTokens();
|
|
942
|
+
const ranked = tokens
|
|
943
|
+
.map((item) => ({
|
|
944
|
+
...item,
|
|
945
|
+
distance: levenshteinDistance(normalized, item.token),
|
|
946
|
+
}))
|
|
947
|
+
.sort((a, b) => a.distance - b.distance)
|
|
948
|
+
.slice(0, 8);
|
|
949
|
+
|
|
950
|
+
const bestDistance = ranked[0]?.distance;
|
|
951
|
+
const tolerance = Math.max(2, Math.floor(normalized.length * 0.45));
|
|
952
|
+
if (!Number.isFinite(bestDistance) || bestDistance > tolerance) {
|
|
953
|
+
return buildFallbackUnknownSuggestion(rawCommand, { commandPrefix });
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const suggestions = [];
|
|
957
|
+
for (const item of ranked) {
|
|
958
|
+
const suggestion = `${commandPrefix}${item.canonical}`;
|
|
959
|
+
if (!suggestions.includes(suggestion)) suggestions.push(suggestion);
|
|
960
|
+
if (suggestions.length >= 4) break;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
if (!suggestions.length) {
|
|
964
|
+
return buildFallbackUnknownSuggestion(rawCommand, { commandPrefix });
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
return [`❓ O comando *${rawCommand}* nao foi encontrado.`, `Talvez voce quis usar: ${suggestions.join(', ')}.`, `Use ${commandPrefix}menu para comandos gerais e ${commandPrefix}menuadm para comandos administrativos.`].join('\n');
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
export const explicarComandoGlobal = async (command, context = {}) => {
|
|
971
|
+
ensureGlobalOfflineFaqScheduler();
|
|
972
|
+
const commandPrefix = context.commandPrefix || '/';
|
|
973
|
+
const target = resolveGlobalCommandTarget(command);
|
|
974
|
+
|
|
975
|
+
if (!target) {
|
|
976
|
+
return {
|
|
977
|
+
ok: false,
|
|
978
|
+
moduleKey: null,
|
|
979
|
+
commandName: null,
|
|
980
|
+
source: 'none',
|
|
981
|
+
text: buildGlobalUnknownCommandSuggestion(command, { commandPrefix }) || `Nao encontrei esse comando. Use ${commandPrefix}menu para listar comandos.`,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const cacheCommandQuestion = buildGlobalCommandCacheKey(target.commandName);
|
|
986
|
+
const normalizedCacheCommandQuestion = normalizeText(cacheCommandQuestion);
|
|
987
|
+
const cachedCommandAnswer = await lookupGlobalCacheByQuestion({
|
|
988
|
+
rawQuestion: cacheCommandQuestion,
|
|
989
|
+
normalizedQuestion: normalizedCacheCommandQuestion,
|
|
990
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_COMMAND,
|
|
991
|
+
});
|
|
992
|
+
if (cachedCommandAnswer?.row?.answer_text) {
|
|
993
|
+
const fromCache = buildCachedGlobalResponse({
|
|
994
|
+
row: cachedCommandAnswer.row,
|
|
995
|
+
intentType: 'cached_command_explain',
|
|
996
|
+
sourceOverride: cachedCommandAnswer.cacheKind === 'exact' ? 'db_global_command_exact' : 'db_global_command_fuzzy',
|
|
997
|
+
});
|
|
998
|
+
if (fromCache) return fromCache;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const answer = await explainTargetWithFallback({
|
|
1002
|
+
target,
|
|
1003
|
+
context,
|
|
1004
|
+
intro: `📘 Aqui esta a explicacao do comando *${commandPrefix}${target.commandName}*.`,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
if (answer?.text) {
|
|
1008
|
+
await saveGlobalCacheAnswer({
|
|
1009
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_COMMAND,
|
|
1010
|
+
question: cacheCommandQuestion,
|
|
1011
|
+
answer: answer.text,
|
|
1012
|
+
commandName: target.commandName,
|
|
1013
|
+
source: answer.source || 'deterministic',
|
|
1014
|
+
intentType: 'command_explain',
|
|
1015
|
+
confidence: 1,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return answer;
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
export const responderPerguntaGlobal = async (question, context = {}) => {
|
|
1023
|
+
ensureGlobalOfflineFaqScheduler();
|
|
1024
|
+
const commandPrefix = context.commandPrefix || '/';
|
|
1025
|
+
const rawQuestion = String(question || '').trim();
|
|
1026
|
+
const normalizedQuestion = normalizeText(rawQuestion);
|
|
1027
|
+
|
|
1028
|
+
if (!normalizedQuestion) {
|
|
1029
|
+
return {
|
|
1030
|
+
ok: false,
|
|
1031
|
+
source: 'none',
|
|
1032
|
+
moduleKey: null,
|
|
1033
|
+
commandName: null,
|
|
1034
|
+
intentType: 'empty_question',
|
|
1035
|
+
suggestions: [],
|
|
1036
|
+
text: `Envie sua pergunta ou use ${commandPrefix}menu para ver os comandos.`,
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
const cachedQuestionAnswer = await lookupGlobalCacheByQuestion({
|
|
1041
|
+
rawQuestion,
|
|
1042
|
+
normalizedQuestion,
|
|
1043
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1044
|
+
});
|
|
1045
|
+
if (cachedQuestionAnswer?.row?.answer_text) {
|
|
1046
|
+
const fromCache = buildCachedGlobalResponse({
|
|
1047
|
+
row: cachedQuestionAnswer.row,
|
|
1048
|
+
intentType: 'cached_question',
|
|
1049
|
+
sourceOverride: cachedQuestionAnswer.cacheKind === 'exact' ? 'db_global_question_exact' : 'db_global_question_fuzzy',
|
|
1050
|
+
});
|
|
1051
|
+
if (fromCache) return fromCache;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const forcedTarget = resolveGlobalCommandTarget(context.forceCommandName);
|
|
1055
|
+
if (forcedTarget) {
|
|
1056
|
+
const forcedAnswer = await explainTargetWithFallback({
|
|
1057
|
+
target: forcedTarget,
|
|
1058
|
+
context,
|
|
1059
|
+
intro: `📎 Continuando sua duvida anterior sobre *${commandPrefix}${forcedTarget.commandName}*:`,
|
|
1060
|
+
});
|
|
1061
|
+
const forcedSuggestions = await getTopDiscoverySuggestions({
|
|
1062
|
+
questionNormalized: normalizedQuestion,
|
|
1063
|
+
commandPrefix,
|
|
1064
|
+
limit: 3,
|
|
1065
|
+
});
|
|
1066
|
+
const result = {
|
|
1067
|
+
...forcedAnswer,
|
|
1068
|
+
intentType: 'followup_forced',
|
|
1069
|
+
suggestions: forcedSuggestions,
|
|
1070
|
+
};
|
|
1071
|
+
await saveGlobalCacheAnswer({
|
|
1072
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1073
|
+
question: rawQuestion,
|
|
1074
|
+
answer: result.text,
|
|
1075
|
+
commandName: result.commandName,
|
|
1076
|
+
source: result.source || 'deterministic',
|
|
1077
|
+
intentType: result.intentType,
|
|
1078
|
+
confidence: 1,
|
|
1079
|
+
suggestions: result.suggestions,
|
|
1080
|
+
});
|
|
1081
|
+
return result;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const explicitTarget = detectCommandTargetFromText(rawQuestion, commandPrefix);
|
|
1085
|
+
if (explicitTarget) {
|
|
1086
|
+
const explicitAnswer = await explainTargetWithFallback({
|
|
1087
|
+
target: explicitTarget,
|
|
1088
|
+
context,
|
|
1089
|
+
intro: `✅ Identifiquei que voce falou do comando *${commandPrefix}${explicitTarget.commandName}*:`,
|
|
1090
|
+
});
|
|
1091
|
+
const explicitSuggestions = await getTopDiscoverySuggestions({
|
|
1092
|
+
questionNormalized: normalizedQuestion,
|
|
1093
|
+
commandPrefix,
|
|
1094
|
+
limit: 3,
|
|
1095
|
+
});
|
|
1096
|
+
const result = {
|
|
1097
|
+
...explicitAnswer,
|
|
1098
|
+
intentType: 'explicit_command',
|
|
1099
|
+
suggestions: explicitSuggestions,
|
|
1100
|
+
};
|
|
1101
|
+
await saveGlobalCacheAnswer({
|
|
1102
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1103
|
+
question: rawQuestion,
|
|
1104
|
+
answer: result.text,
|
|
1105
|
+
commandName: result.commandName,
|
|
1106
|
+
source: result.source || 'deterministic',
|
|
1107
|
+
intentType: result.intentType,
|
|
1108
|
+
confidence: 1,
|
|
1109
|
+
suggestions: result.suggestions,
|
|
1110
|
+
});
|
|
1111
|
+
return result;
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
const ranked = await rankCommandRecords({
|
|
1115
|
+
questionNormalized: normalizedQuestion,
|
|
1116
|
+
commandPrefix,
|
|
1117
|
+
});
|
|
1118
|
+
const topMatch = ranked[0] || null;
|
|
1119
|
+
const followupHintPattern = /^(e|tambem|tambem\?|tamb[eé]m|isso|como|explica|detalha|detalhe|funciona)\b/i;
|
|
1120
|
+
if ((!topMatch || topMatch.finalScore < GLOBAL_HELP_LLM_FALLBACK_THRESHOLD) && context.previousCommandName && followupHintPattern.test(rawQuestion)) {
|
|
1121
|
+
const previousTarget = resolveGlobalCommandTarget(context.previousCommandName);
|
|
1122
|
+
if (previousTarget) {
|
|
1123
|
+
const previousAnswer = await explainTargetWithFallback({
|
|
1124
|
+
target: previousTarget,
|
|
1125
|
+
context,
|
|
1126
|
+
intro: `📎 Pelo contexto anterior, voce pode estar falando de *${commandPrefix}${previousTarget.commandName}*:`,
|
|
1127
|
+
});
|
|
1128
|
+
const previousSuggestions = await getTopDiscoverySuggestions({
|
|
1129
|
+
questionNormalized: normalizedQuestion,
|
|
1130
|
+
commandPrefix,
|
|
1131
|
+
limit: 3,
|
|
1132
|
+
});
|
|
1133
|
+
const result = {
|
|
1134
|
+
...previousAnswer,
|
|
1135
|
+
intentType: 'followup_previous_command',
|
|
1136
|
+
suggestions: previousSuggestions,
|
|
1137
|
+
};
|
|
1138
|
+
await saveGlobalCacheAnswer({
|
|
1139
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1140
|
+
question: rawQuestion,
|
|
1141
|
+
answer: result.text,
|
|
1142
|
+
commandName: result.commandName,
|
|
1143
|
+
source: result.source || 'deterministic',
|
|
1144
|
+
intentType: result.intentType,
|
|
1145
|
+
confidence: 0.9,
|
|
1146
|
+
suggestions: result.suggestions,
|
|
1147
|
+
});
|
|
1148
|
+
return result;
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
if (topMatch && topMatch.finalScore >= GLOBAL_HELP_CONFIDENCE_THRESHOLD) {
|
|
1153
|
+
const target = {
|
|
1154
|
+
moduleKey: topMatch.moduleKey,
|
|
1155
|
+
wrapper: topMatch.wrapper,
|
|
1156
|
+
commandName: topMatch.commandName,
|
|
1157
|
+
};
|
|
1158
|
+
const relatedSuggestions = ranked
|
|
1159
|
+
.slice(1)
|
|
1160
|
+
.map((item) => `${commandPrefix}${item.commandName}`)
|
|
1161
|
+
.filter((value, index, array) => array.indexOf(value) === index)
|
|
1162
|
+
.slice(0, 3);
|
|
1163
|
+
const keywordAnswer = await explainTargetWithFallback({
|
|
1164
|
+
target,
|
|
1165
|
+
context,
|
|
1166
|
+
intro: `🔎 Pela sua pergunta, o melhor comando parece ser *${commandPrefix}${topMatch.commandName}*:`,
|
|
1167
|
+
suggestions: relatedSuggestions,
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
const result = {
|
|
1171
|
+
...keywordAnswer,
|
|
1172
|
+
intentType: 'keyword_match',
|
|
1173
|
+
confidence: topMatch.finalScore,
|
|
1174
|
+
suggestions: relatedSuggestions,
|
|
1175
|
+
};
|
|
1176
|
+
await saveGlobalCacheAnswer({
|
|
1177
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1178
|
+
question: rawQuestion,
|
|
1179
|
+
answer: result.text,
|
|
1180
|
+
commandName: result.commandName,
|
|
1181
|
+
source: result.source || 'deterministic',
|
|
1182
|
+
intentType: result.intentType,
|
|
1183
|
+
confidence: result.confidence,
|
|
1184
|
+
suggestions: result.suggestions,
|
|
1185
|
+
metadata: {
|
|
1186
|
+
ranking: {
|
|
1187
|
+
bm25: topMatch.bm25Score,
|
|
1188
|
+
overlap: topMatch.overlapScore,
|
|
1189
|
+
fuzzy: topMatch.fuzzyScore,
|
|
1190
|
+
feedback: topMatch.feedbackScore,
|
|
1191
|
+
},
|
|
1192
|
+
},
|
|
1193
|
+
});
|
|
1194
|
+
return result;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const toolCallOutcome = await maybeResolveAndExecuteToolCall({
|
|
1198
|
+
question: rawQuestion,
|
|
1199
|
+
context: {
|
|
1200
|
+
...context,
|
|
1201
|
+
commandPrefix,
|
|
1202
|
+
suggestions: ranked
|
|
1203
|
+
.slice(0, 5)
|
|
1204
|
+
.map((item) => `${commandPrefix}${item.commandName}`)
|
|
1205
|
+
.filter((value, index, array) => array.indexOf(value) === index),
|
|
1206
|
+
topMatchScore: topMatch?.finalScore || 0,
|
|
1207
|
+
},
|
|
1208
|
+
});
|
|
1209
|
+
if (toolCallOutcome?.handled) {
|
|
1210
|
+
return {
|
|
1211
|
+
ok: Boolean(toolCallOutcome.ok),
|
|
1212
|
+
source: toolCallOutcome.source || 'tool_call',
|
|
1213
|
+
moduleKey: toolCallOutcome.moduleKey || null,
|
|
1214
|
+
commandName: toolCallOutcome.commandName || null,
|
|
1215
|
+
intentType: toolCallOutcome.intentType || 'tool_call',
|
|
1216
|
+
suggestions: [],
|
|
1217
|
+
suppressReply: Boolean(toolCallOutcome.suppressReply),
|
|
1218
|
+
text: String(toolCallOutcome.text || '').trim(),
|
|
1219
|
+
metadata: toolCallOutcome.metadata && typeof toolCallOutcome.metadata === 'object' ? toolCallOutcome.metadata : {},
|
|
1220
|
+
};
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (topMatch && topMatch.finalScore >= GLOBAL_HELP_LLM_FALLBACK_THRESHOLD) {
|
|
1224
|
+
const target = {
|
|
1225
|
+
moduleKey: topMatch.moduleKey,
|
|
1226
|
+
wrapper: topMatch.wrapper,
|
|
1227
|
+
commandName: topMatch.commandName,
|
|
1228
|
+
};
|
|
1229
|
+
const wrapperFallback = await maybeUseWrapperQuestionFallback({
|
|
1230
|
+
target,
|
|
1231
|
+
rawQuestion,
|
|
1232
|
+
context,
|
|
1233
|
+
});
|
|
1234
|
+
const lowConfidenceSuggestions = ranked
|
|
1235
|
+
.slice(0, 4)
|
|
1236
|
+
.map((item) => `${commandPrefix}${item.commandName}`)
|
|
1237
|
+
.filter((value, index, array) => array.indexOf(value) === index);
|
|
1238
|
+
|
|
1239
|
+
if (wrapperFallback?.text) {
|
|
1240
|
+
const result = {
|
|
1241
|
+
...wrapperFallback,
|
|
1242
|
+
intentType: 'low_confidence_wrapper_fallback',
|
|
1243
|
+
confidence: topMatch.finalScore,
|
|
1244
|
+
suggestions: lowConfidenceSuggestions,
|
|
1245
|
+
};
|
|
1246
|
+
await saveGlobalCacheAnswer({
|
|
1247
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1248
|
+
question: rawQuestion,
|
|
1249
|
+
answer: result.text,
|
|
1250
|
+
commandName: result.commandName,
|
|
1251
|
+
source: result.source || 'module_question_fallback',
|
|
1252
|
+
intentType: result.intentType,
|
|
1253
|
+
confidence: result.confidence,
|
|
1254
|
+
suggestions: result.suggestions,
|
|
1255
|
+
});
|
|
1256
|
+
return result;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
const deterministicLowConfidence = await explainTargetWithFallback({
|
|
1260
|
+
target,
|
|
1261
|
+
context,
|
|
1262
|
+
intro: `🔎 Tenho uma sugestao provavel para voce: *${commandPrefix}${topMatch.commandName}*.`,
|
|
1263
|
+
suggestions: lowConfidenceSuggestions.slice(1),
|
|
1264
|
+
});
|
|
1265
|
+
const result = {
|
|
1266
|
+
...deterministicLowConfidence,
|
|
1267
|
+
intentType: 'low_confidence_deterministic',
|
|
1268
|
+
confidence: topMatch.finalScore,
|
|
1269
|
+
suggestions: lowConfidenceSuggestions,
|
|
1270
|
+
};
|
|
1271
|
+
await saveGlobalCacheAnswer({
|
|
1272
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1273
|
+
question: rawQuestion,
|
|
1274
|
+
answer: result.text,
|
|
1275
|
+
commandName: result.commandName,
|
|
1276
|
+
source: result.source || 'deterministic',
|
|
1277
|
+
intentType: result.intentType,
|
|
1278
|
+
confidence: result.confidence,
|
|
1279
|
+
suggestions: result.suggestions,
|
|
1280
|
+
});
|
|
1281
|
+
return result;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
const fallbackSuggestions = await getTopDiscoverySuggestions({
|
|
1285
|
+
questionNormalized: normalizedQuestion,
|
|
1286
|
+
commandPrefix,
|
|
1287
|
+
limit: 5,
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const fallbackResult = {
|
|
1291
|
+
ok: true,
|
|
1292
|
+
source: 'deterministic_fallback',
|
|
1293
|
+
moduleKey: null,
|
|
1294
|
+
commandName: null,
|
|
1295
|
+
intentType: 'fallback',
|
|
1296
|
+
suggestions: fallbackSuggestions,
|
|
1297
|
+
text: buildConversationFallbackText({
|
|
1298
|
+
commandPrefix,
|
|
1299
|
+
suggestions: fallbackSuggestions,
|
|
1300
|
+
}),
|
|
1301
|
+
};
|
|
1302
|
+
await saveGlobalCacheAnswer({
|
|
1303
|
+
scope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1304
|
+
question: rawQuestion,
|
|
1305
|
+
answer: fallbackResult.text,
|
|
1306
|
+
commandName: null,
|
|
1307
|
+
source: fallbackResult.source,
|
|
1308
|
+
intentType: fallbackResult.intentType,
|
|
1309
|
+
confidence: topMatch?.finalScore || 0,
|
|
1310
|
+
suggestions: fallbackResult.suggestions,
|
|
1311
|
+
metadata: {
|
|
1312
|
+
genericQuestion: isLikelyGenericBotQuestion(normalizedQuestion),
|
|
1313
|
+
},
|
|
1314
|
+
});
|
|
1315
|
+
return fallbackResult;
|
|
1316
|
+
};
|
|
1317
|
+
|
|
1318
|
+
export const registerGlobalHelpCommandExecution = async ({ chatId, userId, isGroupMessage = false, executedCommand = '' } = {}) => {
|
|
1319
|
+
const safeExecutedCommand = stringifyFeedbackCommand(executedCommand);
|
|
1320
|
+
if (!safeExecutedCommand) return { ok: false, reason: 'invalid_command' };
|
|
1321
|
+
|
|
1322
|
+
const scope = isGroupMessage ? 'group' : 'private';
|
|
1323
|
+
const session = getConversationSession({
|
|
1324
|
+
chatId,
|
|
1325
|
+
userId,
|
|
1326
|
+
scope,
|
|
1327
|
+
ttlMs: GLOBAL_HELP_FEEDBACK_SESSION_TTL_MS,
|
|
1328
|
+
});
|
|
1329
|
+
const suggestedCommand = stringifyFeedbackCommand(session?.lastIntent?.commandName);
|
|
1330
|
+
if (!suggestedCommand) {
|
|
1331
|
+
return { ok: false, reason: 'no_previous_suggestion' };
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
const feedbackStore = await ensureFeedbackStoreLoaded();
|
|
1335
|
+
const entry = ensureFeedbackCommandEntry(feedbackStore, suggestedCommand);
|
|
1336
|
+
if (!entry) return { ok: false, reason: 'feedback_entry_failed' };
|
|
1337
|
+
|
|
1338
|
+
if (safeExecutedCommand === suggestedCommand) {
|
|
1339
|
+
entry.success_count = Number(entry.success_count || 0) + 1;
|
|
1340
|
+
} else {
|
|
1341
|
+
entry.miss_count = Number(entry.miss_count || 0) + 1;
|
|
1342
|
+
}
|
|
1343
|
+
entry.last_updated_at = new Date().toISOString();
|
|
1344
|
+
await persistFeedbackStore();
|
|
1345
|
+
|
|
1346
|
+
setConversationSessionIntent({
|
|
1347
|
+
chatId,
|
|
1348
|
+
userId,
|
|
1349
|
+
scope,
|
|
1350
|
+
intent: null,
|
|
1351
|
+
ttlMs: GLOBAL_HELP_FEEDBACK_SESSION_TTL_MS,
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
return {
|
|
1355
|
+
ok: true,
|
|
1356
|
+
matched: safeExecutedCommand === suggestedCommand,
|
|
1357
|
+
suggestedCommand,
|
|
1358
|
+
executedCommand: safeExecutedCommand,
|
|
1359
|
+
};
|
|
1360
|
+
};
|
|
1361
|
+
|
|
1362
|
+
export const gerarFaqGlobalOffline = async ({ reason = 'manual', force = true } = {}) => {
|
|
1363
|
+
ensureGlobalOfflineFaqScheduler();
|
|
1364
|
+
const modules = await runGlobalOfflineFaqGeneration({ reason, force });
|
|
1365
|
+
return {
|
|
1366
|
+
ok: true,
|
|
1367
|
+
modules,
|
|
1368
|
+
};
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
export const getGlobalHelpDeterministicConfig = () => ({
|
|
1372
|
+
cacheModuleKey: GLOBAL_HELP_CACHE_MODULE_KEY,
|
|
1373
|
+
cacheQuestionScope: GLOBAL_HELP_CACHE_SCOPE_QUESTION,
|
|
1374
|
+
cacheCommandScope: GLOBAL_HELP_CACHE_SCOPE_COMMAND,
|
|
1375
|
+
dbCacheFuzzyLimit: GLOBAL_HELP_DB_CACHE_FUZZY_LIMIT,
|
|
1376
|
+
dbCacheFuzzyThreshold: GLOBAL_HELP_DB_CACHE_FUZZY_THRESHOLD,
|
|
1377
|
+
confidenceThreshold: GLOBAL_HELP_CONFIDENCE_THRESHOLD,
|
|
1378
|
+
llmFallbackThreshold: GLOBAL_HELP_LLM_FALLBACK_THRESHOLD,
|
|
1379
|
+
enableWrapperLlmFallback: GLOBAL_HELP_ENABLE_WRAPPER_LLM_FALLBACK,
|
|
1380
|
+
offlineFaqEnabled: GLOBAL_HELP_OFFLINE_FAQ_ENABLED,
|
|
1381
|
+
offlineFaqIntervalMs: GLOBAL_HELP_OFFLINE_FAQ_INTERVAL_MS,
|
|
1382
|
+
weights: {
|
|
1383
|
+
bm25: BM25_WEIGHT,
|
|
1384
|
+
overlap: OVERLAP_WEIGHT,
|
|
1385
|
+
fuzzy: FUZZY_WEIGHT,
|
|
1386
|
+
feedback: FEEDBACK_WEIGHT,
|
|
1387
|
+
},
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
export const resolveGlobalCommandModule = (command) => {
|
|
1391
|
+
const target = resolveGlobalCommandTarget(command);
|
|
1392
|
+
if (!target) {
|
|
1393
|
+
return {
|
|
1394
|
+
moduleKey: null,
|
|
1395
|
+
commandName: null,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
return {
|
|
1399
|
+
moduleKey: target.moduleKey,
|
|
1400
|
+
commandName: target.commandName,
|
|
1401
|
+
};
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
export const resolveGlobalCommandAiHelpWrapper = (command) => {
|
|
1405
|
+
const target = resolveGlobalCommandTarget(command);
|
|
1406
|
+
if (!target) return null;
|
|
1407
|
+
return {
|
|
1408
|
+
moduleKey: target.moduleKey,
|
|
1409
|
+
commandName: target.commandName,
|
|
1410
|
+
wrapper: target.wrapper,
|
|
1411
|
+
};
|
|
1412
|
+
};
|