@omnizap-system/omnizap 2.6.0 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +58 -13
- package/.github/workflows/ci.yml +5 -5
- package/.github/workflows/codeql.yml +1 -1
- package/.github/workflows/db-migration-check.yml +2 -2
- package/.github/workflows/dependency-review.yml +1 -1
- package/.github/workflows/deploy.yml +2 -2
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/security-attest-provenance.yml +2 -2
- package/.github/workflows/security-gitleaks.yml +13 -4
- package/.github/workflows/security-runner-hardening.yml +2 -2
- package/.github/workflows/security-scorecard.yml +1 -1
- package/.github/workflows/security-zap-baseline.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +2 -1
- package/.github/workflows/security-zizmor.yml +1 -1
- package/.github/workflows/wiki-sync.yml +1 -1
- package/.gitleaksignore +9 -0
- package/CODE_OF_CONDUCT.md +2 -2
- package/GEMINI.md +64 -0
- package/README.md +52 -82
- package/SECURITY.md +1 -1
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +230 -58
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +145 -4
- package/app/configParts/sessionConfig.js +157 -0
- package/app/connection/baileysCompatibility.test.js +1 -1
- package/app/connection/groupOwnerWriteStateResolver.js +109 -0
- package/app/connection/socketController.js +660 -158
- package/app/connection/socketController.multiSession.test.js +108 -0
- package/app/controllers/messageController.js +1 -1
- package/app/controllers/messagePipeline/commandMiddleware.js +12 -10
- package/app/controllers/messagePipeline/conversationMiddleware.js +2 -1
- package/app/controllers/messagePipeline/messagePipelineMiddlewares.test.js +104 -0
- package/app/controllers/messagePipeline/preProcessingMiddlewares.js +80 -2
- package/app/controllers/messageProcessingPipeline.js +93 -13
- package/app/controllers/messageProcessingPipeline.test.js +200 -0
- package/app/modules/adminModule/AGENT.md +1 -1
- package/app/modules/adminModule/commandConfig.json +3318 -1347
- package/app/modules/adminModule/groupCommandHandlers.js +858 -15
- package/app/modules/adminModule/groupCommandHandlers.test.js +378 -11
- package/app/modules/adminModule/groupWarningRepository.js +152 -0
- package/app/modules/aiModule/AGENT.md +47 -30
- package/app/modules/aiModule/aiConfigRuntime.js +1 -0
- package/app/modules/aiModule/catCommand.js +135 -27
- package/app/modules/aiModule/commandConfig.json +114 -28
- package/app/modules/analyticsModule/messageAnalysisEventRepository.js +54 -6
- package/app/modules/gameModule/AGENT.md +1 -1
- package/app/modules/gameModule/commandConfig.json +29 -0
- package/app/modules/menuModule/AGENT.md +1 -1
- package/app/modules/menuModule/commandConfig.json +45 -10
- package/app/modules/menuModule/menuCatalogService.js +190 -0
- package/app/modules/menuModule/menuCommandUsageRepository.js +109 -0
- package/app/modules/menuModule/menuDynamicService.js +511 -0
- package/app/modules/menuModule/menuDynamicService.test.js +141 -0
- package/app/modules/menuModule/menus.js +36 -5
- package/app/modules/playModule/AGENT.md +10 -5
- package/app/modules/playModule/commandConfig.json +140 -12
- package/app/modules/playModule/playCommand.js +1 -1417
- package/app/modules/playModule/playCommandConstants.js +80 -0
- package/app/modules/playModule/playCommandCore.js +361 -0
- package/app/modules/playModule/playCommandHandlers.js +41 -0
- package/app/modules/playModule/playCommandMediaClient.js +1872 -0
- package/app/modules/playModule/playConfigRuntime.js +245 -4
- package/app/modules/playModule/playModuleCriticalFlows.test.js +152 -0
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/quoteModule/quoteCommand.js +3 -2
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/rpgBattleCanvasRenderer.js +5 -4
- package/app/modules/rpgPokemonModule/rpgBattleService.test.js +2 -1
- package/app/modules/rpgPokemonModule/rpgPokemonDomain.js +2 -1
- package/app/modules/rpgPokemonModule/rpgPokemonService.js +38 -37
- package/app/modules/rpgPokemonModule/rpgProfileCanvasRenderer.js +4 -3
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/statsModule/rankingCommon.js +5 -4
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/addStickerMetadata.js +4 -3
- package/app/modules/stickerModule/commandConfig.json +145 -0
- package/app/modules/stickerModule/stickerCommand.js +1 -1
- package/app/modules/stickerPackModule/AGENT.md +1 -1
- package/app/modules/stickerPackModule/autoPackCollectorService.js +5 -1
- package/app/modules/stickerPackModule/commandConfig.json +29 -0
- package/app/modules/stickerPackModule/semanticThemeClusterService.js +7 -6
- package/app/modules/stickerPackModule/stickerAutoPackByTagsRuntime.js +10 -9
- package/app/modules/stickerPackModule/stickerClassificationBackgroundRuntime.js +9 -8
- package/app/modules/stickerPackModule/stickerDomainEventConsumerRuntime.js +3 -2
- package/app/modules/stickerPackModule/stickerMarketplaceDriftService.js +2 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +80 -58
- package/app/modules/stickerPackModule/stickerPackMarketplaceService.js +2 -1
- package/app/modules/stickerPackModule/stickerPackRepository.js +2 -1
- package/app/modules/stickerPackModule/stickerPackScoreSnapshotRuntime.js +5 -4
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/stickerPackModule/stickerStorageService.js +3 -2
- package/app/modules/stickerPackModule/stickerWorkerPipelineRuntime.js +2 -1
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/systemMetricsModule/pingCommand.js +6 -5
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/tiktokCommand.js +2 -1
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/userModule/userCommand.js +72 -23
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/modules/waifuPicsModule/waifuPicsCommand.js +3 -2
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/conversationRouterService.js +4 -3
- package/app/services/ai/geminiService.js +132 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/globalModuleAiHelpService.js +3 -2
- package/app/services/ai/messageCommandExecutionService.js +2 -1
- package/app/services/ai/moduleAiHelpCoreService.js +45 -14
- package/app/services/ai/moduleToolExecutorService.js +3 -2
- package/app/services/ai/moduleToolRegistryService.js +2 -1
- package/app/services/ai/toolCandidateSelectorService.js +6 -5
- package/app/services/auth/googleWebLinkService.js +3 -2
- package/app/services/auth/whatsappLoginLinkService.js +3 -2
- package/app/services/external/pokeApiService.js +4 -3
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +57 -26
- package/app/services/infra/featureFlagService.js +2 -1
- package/app/services/messaging/captchaService.js +3 -2
- package/app/services/messaging/newsBroadcastService.js +846 -29
- package/app/services/multiSession/assignmentBalancerService.js +457 -0
- package/app/services/multiSession/groupOwnershipRepository.js +381 -0
- package/app/services/multiSession/groupOwnershipService.js +890 -0
- package/app/services/multiSession/groupOwnershipService.test.js +309 -0
- package/app/services/multiSession/sessionRegistryService.js +293 -0
- package/app/services/sticker/stickerFocusService.js +11 -10
- package/app/store/aiPromptStore.js +36 -19
- package/app/store/conversationSessionStore.js +7 -6
- package/app/store/groupConfigStore.js +41 -5
- package/app/store/premiumUserStore.js +21 -7
- package/app/utils/antiLink/antiLinkModule.js +352 -16
- package/app/workers/aiHelperContinuousLearningWorker.js +512 -0
- package/app/workers/aiLearningWorker.js +6 -5
- package/app/workers/commandConfigEnrichmentWorker.js +4 -3
- package/database/index.js +14 -8
- package/database/migrations/20260307_d0_hardening_down.sql +1 -1
- package/database/migrations/20260314_d7_canonical_sender_down.sql +1 -1
- package/database/migrations/20260406_d30_security_analytics_down.sql +1 -1
- package/database/migrations/20260411_d35_group_community_metadata_down.sql +59 -0
- package/database/migrations/20260411_d35_group_community_metadata_up.sql +62 -0
- package/database/migrations/20260412_d36_system_config_tables_down.sql +32 -0
- package/database/migrations/20260412_d36_system_config_tables_up.sql +66 -0
- package/database/migrations/20260413_d37_group_user_warnings_down.sql +11 -0
- package/database/migrations/20260413_d37_group_user_warnings_up.sql +24 -0
- package/database/migrations/20260414_d38_multi_session_foundation_down.sql +72 -0
- package/database/migrations/20260414_d38_multi_session_foundation_up.sql +125 -0
- package/database/migrations/20260414_d39_multi_session_cutover_down.sql +103 -0
- package/database/migrations/20260414_d39_multi_session_cutover_up.sql +83 -0
- package/database/schema.sql +102 -1
- package/docker-compose.yml +4 -1
- package/docs/compliance/acceptable-use-policy-2026-03-07.md +1 -1
- package/docs/compliance/dpa-b2b-standard-2026-03-07.md +1 -1
- package/docs/compliance/privacy-policy-2026-03-07.md +4 -4
- package/docs/security/dsar-lgpd-runbook-2026-03-07.md +1 -1
- package/docs/security/incident-response-lgpd-anpd-runbook-2026-03-07.md +1 -1
- package/docs/security/network-hardening-runbook-2026-03-07.md +53 -0
- package/docs/security/omnizap-static-security-headers.conf +25 -0
- package/docs/wiki/Home.md +1 -1
- package/ecosystem.prod.config.cjs +32 -12
- package/index.js +57 -23
- package/observability/alert-rules.yml +20 -0
- package/observability/grafana/dashboards/omnizap-system-admin.json +229 -0
- package/observability/mysql-setup.sql +4 -4
- package/observability/system-admin-observability.md +26 -0
- package/package.json +20 -6
- package/public/apple-touch-icon.png +0 -0
- package/public/comandos/commands-catalog.json +2853 -3326
- 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 +3 -2
- package/public/js/apps/commandsReactApp.js +280 -99
- package/public/js/apps/createPackApp.js +11 -10
- package/public/js/apps/homeReactApp.js +181 -130
- package/public/js/apps/loginReactApp.js +1 -1
- package/public/js/apps/stickersApp.js +263 -110
- package/public/js/apps/termsReactApp.js +73 -24
- package/public/js/apps/userApp.js +4 -3
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +355 -280
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/api-docs.html +1 -1
- package/public/pages/aup.html +2 -2
- package/public/pages/dpa.html +3 -3
- package/public/pages/licenca.html +4 -4
- package/public/pages/login.html +1 -1
- package/public/pages/notice-and-takedown.html +2 -2
- package/public/pages/politica-de-privacidade.html +6 -6
- package/public/pages/seo-bot-whatsapp-para-grupo.html +3 -3
- package/public/pages/seo-bot-whatsapp-sem-programar.html +3 -3
- package/public/pages/seo-como-automatizar-avisos-no-whatsapp.html +3 -3
- package/public/pages/seo-como-criar-comandos-whatsapp.html +3 -3
- package/public/pages/seo-como-evitar-spam-no-whatsapp.html +3 -3
- package/public/pages/seo-como-moderar-grupo-whatsapp.html +3 -3
- package/public/pages/seo-como-organizar-comunidade-whatsapp.html +3 -3
- package/public/pages/seo-melhor-bot-whatsapp-para-grupos.html +3 -3
- package/public/pages/stickers-admin.html +1 -1
- package/public/pages/stickers-create.html +1 -1
- package/public/pages/stickers.html +6 -6
- package/public/pages/suboperadores.html +2 -2
- package/public/pages/termos-de-uso-texto-integral.html +6 -6
- package/public/pages/termos-de-uso.html +3 -3
- package/public/pages/user-password-reset.html +4 -5
- package/public/pages/user-systemadm.html +9 -463
- package/public/pages/user.html +2 -2
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +11 -1
- package/scripts/email-broadcast-terms-update.mjs +2 -1
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +166 -2
- package/scripts/generate-module-agents.mjs +2 -1
- package/scripts/generate-seo-satellite-pages.mjs +5 -4
- package/scripts/github-deploy-notify.mjs +2 -1
- package/scripts/github-release-notify.mjs +25 -10
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/release.sh +2 -19
- package/scripts/security-smoketest.mjs +6 -5
- package/scripts/security-web-surface-check.mjs +218 -0
- package/scripts/sticker-catalog-loadtest.mjs +5 -4
- package/server/auth/googleWebAuth/googleWebAuthService.js +8 -7
- package/server/auth/jwt/webJwtService.js +1 -1
- package/server/auth/stickerCatalogAuthContext.js +2 -1
- package/server/auth/termsAcceptance/termsAcceptanceHandler.js +2 -1
- package/server/auth/userPassword/userPasswordAuthService.js +2 -1
- package/server/auth/userPassword/userPasswordRecoveryService.js +4 -3
- package/server/auth/webAccount/webAccountHandlers.js +9 -10
- package/server/controllers/admin/adminPanelHandlers.js +267 -16
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/seo/stickerCatalogSeoContext.js +10 -9
- package/server/controllers/sticker/nonCatalogHandlers.js +2 -1
- package/server/controllers/sticker/stickerCatalogController.js +23 -36
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/githubController.js +3 -2
- package/server/controllers/system/stickerCatalogSystemContext.js +41 -19
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/system/systemMetricsController.js +2 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +5 -3
- package/server/http/httpServer.js +11 -6
- package/server/middleware/rateLimit.js +2 -1
- package/server/middleware/securityHeaders.js +20 -1
- package/server/routes/admin/systemAdminRouter.js +6 -0
- package/server/routes/indexRouter.js +30 -6
- package/server/routes/observability/grafanaProxyRouter.js +254 -0
- package/server/routes/static/staticPageRouter.js +27 -1
- package/server/utils/publicContact.js +31 -0
- package/utils/time/timeModule.js +135 -0
- package/utils/time/timeModule.test.js +65 -0
- package/utils/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +7 -1
- package/public/assets/images/brand-icon-192.png +0 -0
- package/scripts/sync-readme-snapshot.mjs +0 -133
|
@@ -4,9 +4,13 @@ import path from 'node:path';
|
|
|
4
4
|
import OpenAI from 'openai';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
|
|
7
|
+
import { toUnixMs as __timeNowMs } from '#time';
|
|
7
8
|
import logger from '#logger';
|
|
9
|
+
import { createGeminiTextService, DEFAULT_GEMINI_MODEL, isGeminiAuthReady } from './geminiService.js';
|
|
8
10
|
|
|
9
|
-
const DEFAULT_MODEL =
|
|
11
|
+
const DEFAULT_MODEL = DEFAULT_GEMINI_MODEL;
|
|
12
|
+
const DEFAULT_PROVIDER = 'gemini';
|
|
13
|
+
const DEFAULT_GEMINI_AUTH_MODE = 'cli';
|
|
10
14
|
const DEFAULT_TIMEOUT_MS = 25_000;
|
|
11
15
|
const DEFAULT_CONTEXT_MAX_CHARS = 5_200;
|
|
12
16
|
const DEFAULT_AGENT_MAX_CHARS = 2_400;
|
|
@@ -28,13 +32,52 @@ const parseEnvFloat = (value, fallback, min, max) => {
|
|
|
28
32
|
return Math.max(min, Math.min(max, parsed));
|
|
29
33
|
};
|
|
30
34
|
|
|
35
|
+
const normalizeProvider = (value, fallback = DEFAULT_PROVIDER) => {
|
|
36
|
+
const normalized = String(value || '')
|
|
37
|
+
.trim()
|
|
38
|
+
.toLowerCase();
|
|
39
|
+
if (normalized === 'gemini') return 'gemini';
|
|
40
|
+
if (normalized === 'openai') return 'openai';
|
|
41
|
+
return fallback;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const normalizeGeminiAuthMode = (value, fallback = DEFAULT_GEMINI_AUTH_MODE) => {
|
|
45
|
+
const normalized = String(value || '')
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
if (normalized === 'api_key') return 'api_key';
|
|
49
|
+
if (normalized === 'cli') return 'cli';
|
|
50
|
+
if (normalized === 'auto') return 'auto';
|
|
51
|
+
return fallback;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const looksLikeGeminiModel = (value) =>
|
|
55
|
+
String(value || '')
|
|
56
|
+
.trim()
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.includes('gemini');
|
|
59
|
+
|
|
60
|
+
const looksLikeOpenAiModel = (value) => {
|
|
61
|
+
const normalized = String(value || '')
|
|
62
|
+
.trim()
|
|
63
|
+
.toLowerCase();
|
|
64
|
+
if (!normalized) return false;
|
|
65
|
+
return normalized.startsWith('gpt-') || normalized.startsWith('o1') || normalized.startsWith('o3') || normalized.startsWith('o4') || normalized.startsWith('text-');
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const COMMAND_CONFIG_ENRICHMENT_PROVIDER = normalizeProvider(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_PROVIDER || process.env.AI_HELP_LLM_PROVIDER || DEFAULT_PROVIDER, DEFAULT_PROVIDER);
|
|
31
69
|
const COMMAND_CONFIG_ENRICHMENT_MODEL = String(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_MODEL || DEFAULT_MODEL).trim() || DEFAULT_MODEL;
|
|
70
|
+
const COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL = looksLikeGeminiModel(COMMAND_CONFIG_ENRICHMENT_MODEL) ? COMMAND_CONFIG_ENRICHMENT_MODEL : String(process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL).trim() || DEFAULT_GEMINI_MODEL;
|
|
71
|
+
const COMMAND_CONFIG_ENRICHMENT_OPENAI_MODEL = looksLikeOpenAiModel(COMMAND_CONFIG_ENRICHMENT_MODEL) ? COMMAND_CONFIG_ENRICHMENT_MODEL : String(process.env.OPENAI_MODEL || 'gpt-5-nano').trim() || 'gpt-5-nano';
|
|
32
72
|
const COMMAND_CONFIG_ENRICHMENT_TIMEOUT_MS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, 5_000, 90_000);
|
|
33
73
|
const COMMAND_CONFIG_ENRICHMENT_CONTEXT_MAX_CHARS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_CONTEXT_MAX_CHARS, DEFAULT_CONTEXT_MAX_CHARS, 1_500, 16_000);
|
|
34
74
|
const COMMAND_CONFIG_ENRICHMENT_AGENT_MAX_CHARS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_AGENT_MAX_CHARS, DEFAULT_AGENT_MAX_CHARS, 600, 8_000);
|
|
35
75
|
const COMMAND_CONFIG_ENRICHMENT_SOURCE_FILE_MAX_CHARS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_SOURCE_FILE_MAX_CHARS, DEFAULT_SOURCE_FILE_MAX_CHARS, 400, 5_000);
|
|
36
76
|
const COMMAND_CONFIG_ENRICHMENT_MAX_SOURCE_FILES = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_MAX_SOURCE_FILES, DEFAULT_MAX_SOURCE_FILES, 1, 8);
|
|
37
77
|
const COMMAND_CONFIG_ENRICHMENT_BASE_CONFIDENCE = parseEnvFloat(process.env.COMMAND_CONFIG_ENRICHMENT_BASE_CONFIDENCE, 0.55, 0.1, 1);
|
|
78
|
+
const COMMAND_CONFIG_ENRICHMENT_PROVIDER_FAILURE_COOLDOWN_MS = parseEnvInt(process.env.COMMAND_CONFIG_ENRICHMENT_PROVIDER_FAILURE_COOLDOWN_MS, 15 * 60 * 1000, 30_000, 24 * 60 * 60 * 1000);
|
|
79
|
+
const COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE = normalizeGeminiAuthMode(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_GEMINI_AUTH_MODE || process.env.GEMINI_AUTH_MODE || DEFAULT_GEMINI_AUTH_MODE, DEFAULT_GEMINI_AUTH_MODE);
|
|
80
|
+
const COMMAND_CONFIG_ENRICHMENT_GEMINI_CLI_COMMAND = String(process.env.COMMAND_CONFIG_ENRICHMENT_WORKER_GEMINI_CLI_COMMAND || process.env.GEMINI_CLI_COMMAND || 'gemini').trim() || 'gemini';
|
|
38
81
|
|
|
39
82
|
const AI_ENRICHMENT_OUTPUT_SCHEMA = z
|
|
40
83
|
.object({
|
|
@@ -48,6 +91,12 @@ const AI_ENRICHMENT_OUTPUT_SCHEMA = z
|
|
|
48
91
|
.strict();
|
|
49
92
|
|
|
50
93
|
let cachedClient = null;
|
|
94
|
+
let cachedGeminiService = null;
|
|
95
|
+
let cachedGeminiServiceKey = '';
|
|
96
|
+
const providerCooldownUntil = {
|
|
97
|
+
gemini: 0,
|
|
98
|
+
openai: 0,
|
|
99
|
+
};
|
|
51
100
|
|
|
52
101
|
const moduleConfigCache = new Map();
|
|
53
102
|
const fileTextCache = new Map();
|
|
@@ -105,6 +154,51 @@ const parseJsonSafe = (value) => {
|
|
|
105
154
|
}
|
|
106
155
|
};
|
|
107
156
|
|
|
157
|
+
const parseJsonFromModelOutput = (rawOutput) => {
|
|
158
|
+
const direct = parseJsonSafe(rawOutput);
|
|
159
|
+
if (direct && typeof direct === 'object') return direct;
|
|
160
|
+
|
|
161
|
+
const text = String(rawOutput || '').trim();
|
|
162
|
+
if (!text) return null;
|
|
163
|
+
|
|
164
|
+
const fenceMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
165
|
+
if (fenceMatch?.[1]) {
|
|
166
|
+
const fenced = parseJsonSafe(fenceMatch[1]);
|
|
167
|
+
if (fenced && typeof fenced === 'object') return fenced;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const start = text.indexOf('{');
|
|
171
|
+
const end = text.lastIndexOf('}');
|
|
172
|
+
if (start >= 0 && end > start) {
|
|
173
|
+
const sliced = parseJsonSafe(text.slice(start, end + 1));
|
|
174
|
+
if (sliced && typeof sliced === 'object') return sliced;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const isProviderInCooldown = (provider) => {
|
|
181
|
+
const now = __timeNowMs();
|
|
182
|
+
const safeProvider = provider === 'openai' ? 'openai' : 'gemini';
|
|
183
|
+
return Number(providerCooldownUntil[safeProvider] || 0) > now;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const shouldApplyProviderCooldown = (provider, error) => {
|
|
187
|
+
const message = normalizeText(error?.message || error);
|
|
188
|
+
if (!message) return false;
|
|
189
|
+
|
|
190
|
+
const hardErrorPatterns = ['modelnotfound', 'does not exist', 'requested entity was not found', 'permission denied', 'unauthorized', 'invalid api key', 'quota', '429'];
|
|
191
|
+
if (hardErrorPatterns.some((token) => message.includes(token))) return true;
|
|
192
|
+
return false;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const markProviderCooldown = (provider, error) => {
|
|
196
|
+
if (!shouldApplyProviderCooldown(provider, error)) return false;
|
|
197
|
+
const safeProvider = provider === 'openai' ? 'openai' : 'gemini';
|
|
198
|
+
providerCooldownUntil[safeProvider] = __timeNowMs() + COMMAND_CONFIG_ENRICHMENT_PROVIDER_FAILURE_COOLDOWN_MS;
|
|
199
|
+
return true;
|
|
200
|
+
};
|
|
201
|
+
|
|
108
202
|
const uniqueList = (values = [], { maxItems = DEFAULT_MAX_LIST_ITEMS, maxLength = 200, normalizeMode = 'display' } = {}) => {
|
|
109
203
|
const source = Array.isArray(values) ? values : [];
|
|
110
204
|
const output = [];
|
|
@@ -175,6 +269,86 @@ const getOpenAIClient = () => {
|
|
|
175
269
|
return cachedClient;
|
|
176
270
|
};
|
|
177
271
|
|
|
272
|
+
const isGeminiReady = () =>
|
|
273
|
+
isGeminiAuthReady({
|
|
274
|
+
authMode: COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE,
|
|
275
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
276
|
+
cliCommand: COMMAND_CONFIG_ENRICHMENT_GEMINI_CLI_COMMAND,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const isOpenAiReady = () => Boolean(String(process.env.OPENAI_API_KEY || '').trim());
|
|
280
|
+
|
|
281
|
+
const getGeminiService = () => {
|
|
282
|
+
if (!isGeminiReady()) return null;
|
|
283
|
+
|
|
284
|
+
const serviceKey = `${COMMAND_CONFIG_ENRICHMENT_MODEL}|${COMMAND_CONFIG_ENRICHMENT_TIMEOUT_MS}|${COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE}|${COMMAND_CONFIG_ENRICHMENT_GEMINI_CLI_COMMAND}|${Boolean(String(process.env.GEMINI_API_KEY || '').trim())}`;
|
|
285
|
+
if (!cachedGeminiService || cachedGeminiServiceKey !== serviceKey) {
|
|
286
|
+
cachedGeminiService = createGeminiTextService({
|
|
287
|
+
apiKey: process.env.GEMINI_API_KEY,
|
|
288
|
+
defaultModel: COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL,
|
|
289
|
+
timeoutMs: COMMAND_CONFIG_ENRICHMENT_TIMEOUT_MS,
|
|
290
|
+
authMode: COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE,
|
|
291
|
+
cliCommand: COMMAND_CONFIG_ENRICHMENT_GEMINI_CLI_COMMAND,
|
|
292
|
+
});
|
|
293
|
+
cachedGeminiServiceKey = serviceKey;
|
|
294
|
+
}
|
|
295
|
+
return cachedGeminiService;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const callGeminiEnrichment = async ({ systemPrompt, contextPayload }) => {
|
|
299
|
+
const service = getGeminiService();
|
|
300
|
+
if (!service) return null;
|
|
301
|
+
|
|
302
|
+
const response = await service.generateText({
|
|
303
|
+
instructions: systemPrompt,
|
|
304
|
+
userPrompt: contextPayload,
|
|
305
|
+
model: COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL,
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const text = String(response?.text || '').trim();
|
|
309
|
+
if (!text) return null;
|
|
310
|
+
return {
|
|
311
|
+
provider: 'gemini',
|
|
312
|
+
model: String(response?.model || COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL).trim() || COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL,
|
|
313
|
+
outputText: text,
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const callOpenAiEnrichment = async ({ systemPrompt, contextPayload }) => {
|
|
318
|
+
if (!isOpenAiReady()) return null;
|
|
319
|
+
|
|
320
|
+
const client = getOpenAIClient();
|
|
321
|
+
const completion = await client.chat.completions.create({
|
|
322
|
+
model: COMMAND_CONFIG_ENRICHMENT_OPENAI_MODEL,
|
|
323
|
+
response_format: { type: 'json_object' },
|
|
324
|
+
messages: [
|
|
325
|
+
{
|
|
326
|
+
role: 'system',
|
|
327
|
+
content: systemPrompt,
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
role: 'user',
|
|
331
|
+
content: contextPayload,
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
const message = completion?.choices?.[0]?.message || {};
|
|
337
|
+
const outputText = extractTextFromAssistantMessage(message);
|
|
338
|
+
if (!outputText) return null;
|
|
339
|
+
return {
|
|
340
|
+
provider: 'openai',
|
|
341
|
+
model: COMMAND_CONFIG_ENRICHMENT_OPENAI_MODEL,
|
|
342
|
+
outputText,
|
|
343
|
+
};
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const resolveLlmCallOrder = () => {
|
|
347
|
+
const primary = normalizeProvider(COMMAND_CONFIG_ENRICHMENT_PROVIDER, DEFAULT_PROVIDER);
|
|
348
|
+
const fallback = primary === 'gemini' ? 'openai' : 'gemini';
|
|
349
|
+
return [primary, fallback];
|
|
350
|
+
};
|
|
351
|
+
|
|
178
352
|
const isFilePathInside = (baseDir, candidatePath) => {
|
|
179
353
|
const normalizedBase = path.resolve(baseDir);
|
|
180
354
|
const normalizedCandidate = path.resolve(candidatePath);
|
|
@@ -333,7 +507,7 @@ const buildCommandContextPayload = async ({ learningEvent, toolRecord }) => {
|
|
|
333
507
|
};
|
|
334
508
|
|
|
335
509
|
const parseAndSanitizeOutput = (rawJson) => {
|
|
336
|
-
const parsed =
|
|
510
|
+
const parsed = parseJsonFromModelOutput(rawJson);
|
|
337
511
|
if (!parsed || typeof parsed !== 'object') return null;
|
|
338
512
|
|
|
339
513
|
const validated = AI_ENRICHMENT_OUTPUT_SCHEMA.safeParse(parsed);
|
|
@@ -379,7 +553,7 @@ const buildHeuristicSuggestion = ({ learningEvent, toolRecord }) => {
|
|
|
379
553
|
};
|
|
380
554
|
};
|
|
381
555
|
|
|
382
|
-
const isLlmReady = () =>
|
|
556
|
+
const isLlmReady = () => isGeminiReady() || isOpenAiReady();
|
|
383
557
|
|
|
384
558
|
export const generateCommandConfigEnrichmentSuggestion = async ({ learningEvent, toolRecord } = {}) => {
|
|
385
559
|
if (!learningEvent || !toolRecord) return null;
|
|
@@ -390,63 +564,71 @@ export const generateCommandConfigEnrichmentSuggestion = async ({ learningEvent,
|
|
|
390
564
|
}
|
|
391
565
|
|
|
392
566
|
const contextPayload = await buildCommandContextPayload({ learningEvent, toolRecord });
|
|
393
|
-
const
|
|
567
|
+
const systemPrompt = buildSystemPrompt();
|
|
394
568
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
response_format: { type: 'json_object' },
|
|
401
|
-
messages: [
|
|
402
|
-
{
|
|
403
|
-
role: 'system',
|
|
404
|
-
content: buildSystemPrompt(),
|
|
405
|
-
},
|
|
406
|
-
{
|
|
407
|
-
role: 'user',
|
|
408
|
-
content: contextPayload,
|
|
409
|
-
},
|
|
410
|
-
],
|
|
411
|
-
});
|
|
412
|
-
} catch (error) {
|
|
413
|
-
logger.warn('Falha ao gerar enriquecimento de commandConfig com LLM.', {
|
|
414
|
-
action: 'command_config_enrichment_llm_failed',
|
|
415
|
-
module: toolRecord?.moduleKey || null,
|
|
416
|
-
command: toolRecord?.commandName || null,
|
|
417
|
-
event_id: learningEvent?.id || null,
|
|
418
|
-
error: error?.message,
|
|
419
|
-
});
|
|
420
|
-
return fallbackSuggestion;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
const message = completion?.choices?.[0]?.message || {};
|
|
424
|
-
const rawJson = extractTextFromAssistantMessage(message);
|
|
425
|
-
const parsed = parseAndSanitizeOutput(rawJson);
|
|
569
|
+
const providerOrder = resolveLlmCallOrder();
|
|
570
|
+
for (const provider of providerOrder) {
|
|
571
|
+
if (isProviderInCooldown(provider)) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
426
574
|
|
|
427
|
-
|
|
428
|
-
|
|
575
|
+
try {
|
|
576
|
+
const llmOutput =
|
|
577
|
+
provider === 'gemini'
|
|
578
|
+
? await callGeminiEnrichment({
|
|
579
|
+
systemPrompt,
|
|
580
|
+
contextPayload,
|
|
581
|
+
})
|
|
582
|
+
: await callOpenAiEnrichment({
|
|
583
|
+
systemPrompt,
|
|
584
|
+
contextPayload,
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
if (!llmOutput?.outputText) continue;
|
|
588
|
+
|
|
589
|
+
const parsed = parseAndSanitizeOutput(llmOutput.outputText);
|
|
590
|
+
if (!parsed || !isSuggestionMeaningful(parsed.suggestion)) continue;
|
|
591
|
+
|
|
592
|
+
const eventConfidence = clamp01(learningEvent?.confidence);
|
|
593
|
+
const successSignal = learningEvent?.success ? 0.12 : 0.03;
|
|
594
|
+
const finalConfidence = clamp01(parsed.modelConfidence * 0.72 + eventConfidence * 0.18 + successSignal);
|
|
595
|
+
|
|
596
|
+
return {
|
|
597
|
+
suggestion: parsed.suggestion,
|
|
598
|
+
confidence: finalConfidence,
|
|
599
|
+
source: `llm_${provider}`,
|
|
600
|
+
modelName: llmOutput.model || COMMAND_CONFIG_ENRICHMENT_MODEL,
|
|
601
|
+
};
|
|
602
|
+
} catch (error) {
|
|
603
|
+
logger.warn('Falha ao gerar enriquecimento de commandConfig com LLM.', {
|
|
604
|
+
action: 'command_config_enrichment_llm_failed',
|
|
605
|
+
module: toolRecord?.moduleKey || null,
|
|
606
|
+
command: toolRecord?.commandName || null,
|
|
607
|
+
event_id: learningEvent?.id || null,
|
|
608
|
+
provider,
|
|
609
|
+
provider_cooldown_applied: markProviderCooldown(provider, error),
|
|
610
|
+
error: error?.message,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
429
613
|
}
|
|
430
614
|
|
|
431
|
-
|
|
432
|
-
const successSignal = learningEvent?.success ? 0.12 : 0.03;
|
|
433
|
-
const finalConfidence = clamp01(parsed.modelConfidence * 0.72 + eventConfidence * 0.18 + successSignal);
|
|
434
|
-
|
|
435
|
-
return {
|
|
436
|
-
suggestion: parsed.suggestion,
|
|
437
|
-
confidence: finalConfidence,
|
|
438
|
-
source: 'llm',
|
|
439
|
-
modelName: COMMAND_CONFIG_ENRICHMENT_MODEL,
|
|
440
|
-
};
|
|
615
|
+
return fallbackSuggestion;
|
|
441
616
|
};
|
|
442
617
|
|
|
443
618
|
export const getCommandConfigEnrichmentServiceConfig = () => ({
|
|
619
|
+
provider: COMMAND_CONFIG_ENRICHMENT_PROVIDER,
|
|
444
620
|
model: COMMAND_CONFIG_ENRICHMENT_MODEL,
|
|
621
|
+
geminiModel: COMMAND_CONFIG_ENRICHMENT_GEMINI_MODEL,
|
|
622
|
+
openaiModel: COMMAND_CONFIG_ENRICHMENT_OPENAI_MODEL,
|
|
445
623
|
timeoutMs: COMMAND_CONFIG_ENRICHMENT_TIMEOUT_MS,
|
|
446
624
|
contextMaxChars: COMMAND_CONFIG_ENRICHMENT_CONTEXT_MAX_CHARS,
|
|
447
625
|
agentMaxChars: COMMAND_CONFIG_ENRICHMENT_AGENT_MAX_CHARS,
|
|
448
626
|
sourceFileMaxChars: COMMAND_CONFIG_ENRICHMENT_SOURCE_FILE_MAX_CHARS,
|
|
449
627
|
maxSourceFiles: COMMAND_CONFIG_ENRICHMENT_MAX_SOURCE_FILES,
|
|
450
628
|
baseConfidence: COMMAND_CONFIG_ENRICHMENT_BASE_CONFIDENCE,
|
|
629
|
+
geminiAuthMode: COMMAND_CONFIG_ENRICHMENT_GEMINI_AUTH_MODE,
|
|
630
|
+
hasGeminiAuth: isGeminiReady(),
|
|
631
|
+
hasOpenAiApiKey: isOpenAiReady(),
|
|
451
632
|
hasApiKey: isLlmReady(),
|
|
633
|
+
providerFailureCooldownMs: COMMAND_CONFIG_ENRICHMENT_PROVIDER_FAILURE_COOLDOWN_MS,
|
|
452
634
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
|
|
1
2
|
import logger from '#logger';
|
|
2
3
|
import { isSameJidUser } from '../../config/index.js';
|
|
3
4
|
import { responderPerguntaGlobal } from './globalModuleAiHelpService.js';
|
|
@@ -119,7 +120,7 @@ const hasBotKeywordTrigger = (text) => {
|
|
|
119
120
|
return tokens.length <= 3;
|
|
120
121
|
};
|
|
121
122
|
|
|
122
|
-
const pruneGroupCooldown = (nowMs =
|
|
123
|
+
const pruneGroupCooldown = (nowMs = __timeNowMs()) => {
|
|
123
124
|
for (const [key, expiresAt] of groupCooldownCache.entries()) {
|
|
124
125
|
if (!Number.isFinite(expiresAt) || expiresAt <= nowMs) {
|
|
125
126
|
groupCooldownCache.delete(key);
|
|
@@ -207,7 +208,7 @@ const shouldSkipForGroupCooldown = ({ chatId, senderJid }) => {
|
|
|
207
208
|
if (!key) return false;
|
|
208
209
|
|
|
209
210
|
pruneGroupCooldown();
|
|
210
|
-
const nowMs =
|
|
211
|
+
const nowMs = __timeNowMs();
|
|
211
212
|
const expiresAt = groupCooldownCache.get(key) || 0;
|
|
212
213
|
if (expiresAt > nowMs) return true;
|
|
213
214
|
return false;
|
|
@@ -217,7 +218,7 @@ const markGroupCooldown = ({ chatId, senderJid }) => {
|
|
|
217
218
|
const key = buildGroupCooldownKey({ chatId, senderJid });
|
|
218
219
|
if (!key) return;
|
|
219
220
|
pruneGroupCooldown();
|
|
220
|
-
groupCooldownCache.set(key,
|
|
221
|
+
groupCooldownCache.set(key, __timeNowMs() + GROUP_COOLDOWN_MS);
|
|
221
222
|
};
|
|
222
223
|
|
|
223
224
|
const buildIntentFromAnswer = (answer) => ({
|
|
@@ -1,5 +1,13 @@
|
|
|
1
|
+
import { execFile, spawnSync } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
|
|
1
4
|
const DEFAULT_GEMINI_API_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta';
|
|
2
|
-
export const DEFAULT_GEMINI_MODEL = 'gemini-
|
|
5
|
+
export const DEFAULT_GEMINI_MODEL = 'gemini-2.5-flash';
|
|
6
|
+
const DEFAULT_GEMINI_AUTH_MODE = 'auto';
|
|
7
|
+
const DEFAULT_GEMINI_CLI_COMMAND = 'gemini';
|
|
8
|
+
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const cliAvailabilityCache = new Map();
|
|
3
11
|
|
|
4
12
|
const normalizeModelName = (value, fallback = DEFAULT_GEMINI_MODEL) => {
|
|
5
13
|
const raw = String(value || '').trim();
|
|
@@ -13,6 +21,64 @@ const toPositiveInt = (value, fallback, min = 1) => {
|
|
|
13
21
|
return parsed;
|
|
14
22
|
};
|
|
15
23
|
|
|
24
|
+
const normalizeAuthMode = (value, fallback = DEFAULT_GEMINI_AUTH_MODE) => {
|
|
25
|
+
const normalized = String(value || '')
|
|
26
|
+
.trim()
|
|
27
|
+
.toLowerCase();
|
|
28
|
+
if (normalized === 'api_key') return 'api_key';
|
|
29
|
+
if (normalized === 'cli') return 'cli';
|
|
30
|
+
if (normalized === 'auto') return 'auto';
|
|
31
|
+
return fallback;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const normalizeCliCommand = (value) => {
|
|
35
|
+
const command = String(value || DEFAULT_GEMINI_CLI_COMMAND).trim();
|
|
36
|
+
return command || DEFAULT_GEMINI_CLI_COMMAND;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const parseBooleanEnv = (value, fallback = false) => {
|
|
40
|
+
if (value === undefined || value === null || value === '') return fallback;
|
|
41
|
+
const normalized = String(value).trim().toLowerCase();
|
|
42
|
+
if (['1', 'true', 'yes', 'sim', 'on'].includes(normalized)) return true;
|
|
43
|
+
if (['0', 'false', 'no', 'nao', 'não', 'off'].includes(normalized)) return false;
|
|
44
|
+
return fallback;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const isGeminiCliAvailable = (cliCommand = DEFAULT_GEMINI_CLI_COMMAND) => {
|
|
48
|
+
const safeCliCommand = normalizeCliCommand(cliCommand);
|
|
49
|
+
if (cliAvailabilityCache.has(safeCliCommand)) {
|
|
50
|
+
return cliAvailabilityCache.get(safeCliCommand);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let available = false;
|
|
54
|
+
try {
|
|
55
|
+
const result = spawnSync(safeCliCommand, ['--version'], {
|
|
56
|
+
stdio: 'ignore',
|
|
57
|
+
shell: false,
|
|
58
|
+
});
|
|
59
|
+
available = result?.status === 0;
|
|
60
|
+
} catch {
|
|
61
|
+
available = false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
cliAvailabilityCache.set(safeCliCommand, available);
|
|
65
|
+
return available;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const sanitizeCliOutput = (value) =>
|
|
69
|
+
String(value || '')
|
|
70
|
+
.replaceAll('\0', '')
|
|
71
|
+
.replace(/\r\n/g, '\n')
|
|
72
|
+
.trim();
|
|
73
|
+
|
|
74
|
+
const buildCliPrompt = ({ instructions = '', userPrompt = '' } = {}) => {
|
|
75
|
+
const safePrompt = normalizeOutboundText(userPrompt);
|
|
76
|
+
const safeInstructions = normalizeOutboundText(instructions);
|
|
77
|
+
if (!safeInstructions) return safePrompt;
|
|
78
|
+
if (!safePrompt) return safeInstructions;
|
|
79
|
+
return `${safeInstructions}\n\n---\n\n${safePrompt}`;
|
|
80
|
+
};
|
|
81
|
+
|
|
16
82
|
const parseErrorMessage = (payload, status) => {
|
|
17
83
|
const explicit = String(payload?.error?.message || '').trim();
|
|
18
84
|
if (explicit) return explicit;
|
|
@@ -35,14 +101,25 @@ const normalizeOutboundText = (value) =>
|
|
|
35
101
|
.join('')
|
|
36
102
|
.trim();
|
|
37
103
|
|
|
38
|
-
export const
|
|
104
|
+
export const isGeminiAuthReady = ({ authMode = process.env.GEMINI_AUTH_MODE || DEFAULT_GEMINI_AUTH_MODE, apiKey = process.env.GEMINI_API_KEY, cliCommand = process.env.GEMINI_CLI_COMMAND || DEFAULT_GEMINI_CLI_COMMAND } = {}) => {
|
|
105
|
+
const mode = normalizeAuthMode(authMode, DEFAULT_GEMINI_AUTH_MODE);
|
|
39
106
|
const safeApiKey = String(apiKey || '').trim();
|
|
40
|
-
|
|
107
|
+
const useCliByEnv = parseBooleanEnv(process.env.GEMINI_USE_CLI_AUTH, false);
|
|
108
|
+
const modeWithLegacy = mode === 'auto' && useCliByEnv ? 'cli' : mode;
|
|
41
109
|
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
110
|
+
if (modeWithLegacy === 'api_key') return Boolean(safeApiKey);
|
|
111
|
+
if (modeWithLegacy === 'cli') return isGeminiCliAvailable(cliCommand);
|
|
112
|
+
if (safeApiKey) return true;
|
|
113
|
+
return isGeminiCliAvailable(cliCommand);
|
|
114
|
+
};
|
|
45
115
|
|
|
116
|
+
export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, defaultModel = process.env.GEMINI_MODEL || DEFAULT_GEMINI_MODEL, timeoutMs = 25_000, apiBaseUrl = process.env.GEMINI_API_BASE_URL || DEFAULT_GEMINI_API_BASE_URL, authMode = process.env.GEMINI_AUTH_MODE || DEFAULT_GEMINI_AUTH_MODE, cliCommand = process.env.GEMINI_CLI_COMMAND || DEFAULT_GEMINI_CLI_COMMAND, execFileAsyncImpl = execFileAsync, isCliAvailableImpl = isGeminiCliAvailable } = {}) => {
|
|
117
|
+
const safeApiKey = String(apiKey || '').trim();
|
|
118
|
+
const safeAuthMode = normalizeAuthMode(authMode, DEFAULT_GEMINI_AUTH_MODE);
|
|
119
|
+
const useCliByEnv = parseBooleanEnv(process.env.GEMINI_USE_CLI_AUTH, false);
|
|
120
|
+
const normalizedAuthMode = safeAuthMode === 'auto' && useCliByEnv ? 'cli' : safeAuthMode;
|
|
121
|
+
const selectedTransport = normalizedAuthMode === 'api_key' ? 'api_key' : normalizedAuthMode === 'cli' ? 'cli' : safeApiKey ? 'api_key' : 'cli';
|
|
122
|
+
const safeCliCommand = normalizeCliCommand(cliCommand);
|
|
46
123
|
const safeBaseUrl =
|
|
47
124
|
String(apiBaseUrl || DEFAULT_GEMINI_API_BASE_URL)
|
|
48
125
|
.trim()
|
|
@@ -50,7 +127,16 @@ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, d
|
|
|
50
127
|
const safeTimeoutMs = Math.max(1_000, toPositiveInt(timeoutMs, 25_000, 1_000));
|
|
51
128
|
const resolvedDefaultModel = normalizeModelName(defaultModel, DEFAULT_GEMINI_MODEL);
|
|
52
129
|
|
|
53
|
-
|
|
130
|
+
if (selectedTransport === 'api_key') {
|
|
131
|
+
if (!safeApiKey) return null;
|
|
132
|
+
if (typeof globalThis.fetch !== 'function') {
|
|
133
|
+
throw new Error('createGeminiTextService: global fetch indisponivel no runtime atual.');
|
|
134
|
+
}
|
|
135
|
+
} else if (!isCliAvailableImpl(safeCliCommand)) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const generateTextViaApiKey = async ({ instructions = '', userPrompt = '', model = resolvedDefaultModel } = {}) => {
|
|
54
140
|
const safePrompt = normalizeOutboundText(userPrompt);
|
|
55
141
|
if (!safePrompt) return { text: '', model: normalizeModelName(model, resolvedDefaultModel) };
|
|
56
142
|
|
|
@@ -82,6 +168,7 @@ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, d
|
|
|
82
168
|
: null;
|
|
83
169
|
|
|
84
170
|
try {
|
|
171
|
+
// lgtm[js/file-access-to-http]
|
|
85
172
|
const response = await globalThis.fetch(endpoint, {
|
|
86
173
|
method: 'POST',
|
|
87
174
|
headers: {
|
|
@@ -108,8 +195,46 @@ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, d
|
|
|
108
195
|
}
|
|
109
196
|
};
|
|
110
197
|
|
|
198
|
+
const generateTextViaCli = async ({ instructions = '', userPrompt = '', model = resolvedDefaultModel } = {}) => {
|
|
199
|
+
const prompt = buildCliPrompt({ instructions, userPrompt });
|
|
200
|
+
const modelName = normalizeModelName(model, resolvedDefaultModel);
|
|
201
|
+
if (!prompt) return { text: '', model: modelName };
|
|
202
|
+
|
|
203
|
+
const args = ['-m', modelName, '-p', prompt, '--output-format', 'text'];
|
|
204
|
+
try {
|
|
205
|
+
const result = await execFileAsyncImpl(safeCliCommand, args, {
|
|
206
|
+
timeout: safeTimeoutMs,
|
|
207
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
208
|
+
env: process.env,
|
|
209
|
+
});
|
|
210
|
+
const text = sanitizeCliOutput(result?.stdout);
|
|
211
|
+
if (!text) {
|
|
212
|
+
const stderrText = sanitizeCliOutput(result?.stderr);
|
|
213
|
+
throw new Error(stderrText || 'Gemini CLI retornou resposta vazia.');
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
text,
|
|
217
|
+
model: modelName,
|
|
218
|
+
};
|
|
219
|
+
} catch (error) {
|
|
220
|
+
const stderrText = sanitizeCliOutput(error?.stderr);
|
|
221
|
+
const stdoutText = sanitizeCliOutput(error?.stdout);
|
|
222
|
+
const baseMessage = String(error?.message || '').trim();
|
|
223
|
+
const finalMessage = stderrText || stdoutText || baseMessage || 'Falha ao executar Gemini CLI.';
|
|
224
|
+
throw new Error(finalMessage);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const generateText = async ({ instructions = '', userPrompt = '', model = resolvedDefaultModel } = {}) => {
|
|
229
|
+
if (selectedTransport === 'api_key') {
|
|
230
|
+
return generateTextViaApiKey({ instructions, userPrompt, model });
|
|
231
|
+
}
|
|
232
|
+
return generateTextViaCli({ instructions, userPrompt, model });
|
|
233
|
+
};
|
|
234
|
+
|
|
111
235
|
return {
|
|
112
236
|
defaultModel: resolvedDefaultModel,
|
|
237
|
+
transport: selectedTransport,
|
|
113
238
|
generateText,
|
|
114
239
|
};
|
|
115
240
|
};
|
|
@@ -2,8 +2,8 @@ import test from 'node:test';
|
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import { createGeminiTextService } from './geminiService.js';
|
|
4
4
|
|
|
5
|
-
test('createGeminiTextService retorna null quando GEMINI_API_KEY nao existe', () => {
|
|
6
|
-
const service = createGeminiTextService({ apiKey: '' });
|
|
5
|
+
test('createGeminiTextService retorna null no modo api_key quando GEMINI_API_KEY nao existe', () => {
|
|
6
|
+
const service = createGeminiTextService({ authMode: 'api_key', apiKey: '' });
|
|
7
7
|
assert.equal(service, null);
|
|
8
8
|
});
|
|
9
9
|
|
|
@@ -85,3 +85,60 @@ test('createGeminiTextService propaga erro detalhado da API', async (t) => {
|
|
|
85
85
|
/Modelo invalido/,
|
|
86
86
|
);
|
|
87
87
|
});
|
|
88
|
+
|
|
89
|
+
test('createGeminiTextService usa Gemini CLI quando authMode=cli', async () => {
|
|
90
|
+
const calls = [];
|
|
91
|
+
const service = createGeminiTextService({
|
|
92
|
+
authMode: 'cli',
|
|
93
|
+
cliCommand: 'gemini',
|
|
94
|
+
defaultModel: 'gemini-2.5-flash',
|
|
95
|
+
isCliAvailableImpl: () => true,
|
|
96
|
+
execFileAsyncImpl: async (file, args, options) => {
|
|
97
|
+
calls.push({ file, args, options });
|
|
98
|
+
return {
|
|
99
|
+
stdout: 'OK_GEMINI_CLI\n',
|
|
100
|
+
stderr: 'Loaded cached credentials.',
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
assert.ok(service);
|
|
106
|
+
assert.equal(service.transport, 'cli');
|
|
107
|
+
|
|
108
|
+
const response = await service.generateText({
|
|
109
|
+
instructions: 'Responda curto.',
|
|
110
|
+
userPrompt: 'Diga OK.',
|
|
111
|
+
model: 'gemini-2.5-flash',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
assert.equal(response.model, 'gemini-2.5-flash');
|
|
115
|
+
assert.equal(response.text, 'OK_GEMINI_CLI');
|
|
116
|
+
assert.equal(calls.length, 1);
|
|
117
|
+
assert.equal(calls[0].file, 'gemini');
|
|
118
|
+
assert.ok(calls[0].args.includes('-m'));
|
|
119
|
+
assert.ok(calls[0].args.includes('gemini-2.5-flash'));
|
|
120
|
+
assert.ok(calls[0].args.includes('-p'));
|
|
121
|
+
assert.ok(calls[0].args.includes('--output-format'));
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('createGeminiTextService propaga erro do Gemini CLI', async () => {
|
|
125
|
+
const service = createGeminiTextService({
|
|
126
|
+
authMode: 'cli',
|
|
127
|
+
cliCommand: 'gemini',
|
|
128
|
+
defaultModel: 'gemini-2.5-flash',
|
|
129
|
+
isCliAvailableImpl: () => true,
|
|
130
|
+
execFileAsyncImpl: async () => {
|
|
131
|
+
const error = new Error('Command failed');
|
|
132
|
+
error.stderr = 'ModelNotFoundError: Requested entity was not found.';
|
|
133
|
+
throw error;
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
await assert.rejects(
|
|
138
|
+
() =>
|
|
139
|
+
service.generateText({
|
|
140
|
+
userPrompt: 'teste',
|
|
141
|
+
}),
|
|
142
|
+
/ModelNotFoundError/,
|
|
143
|
+
);
|
|
144
|
+
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { now as __timeNow, nowIso as __timeNowIso, toUnixMs as __timeNowMs } from '#time';
|
|
1
2
|
import fs from 'node:fs/promises';
|
|
2
3
|
import path from 'node:path';
|
|
3
4
|
|
|
@@ -283,7 +284,7 @@ const ensureFeedbackStoreLoaded = async () => {
|
|
|
283
284
|
|
|
284
285
|
const persistFeedbackStore = async () => {
|
|
285
286
|
const store = await ensureFeedbackStoreLoaded();
|
|
286
|
-
store.updatedAt =
|
|
287
|
+
store.updatedAt = __timeNowIso();
|
|
287
288
|
await withFeedbackWrite(async () => {
|
|
288
289
|
await fs.writeFile(GLOBAL_HELP_FEEDBACK_FILE_PATH, `${JSON.stringify(store, null, 2)}\n`, 'utf8');
|
|
289
290
|
});
|
|
@@ -1340,7 +1341,7 @@ export const registerGlobalHelpCommandExecution = async ({ chatId, userId, isGro
|
|
|
1340
1341
|
} else {
|
|
1341
1342
|
entry.miss_count = Number(entry.miss_count || 0) + 1;
|
|
1342
1343
|
}
|
|
1343
|
-
entry.last_updated_at =
|
|
1344
|
+
entry.last_updated_at = __timeNowIso();
|
|
1344
1345
|
await persistFeedbackStore();
|
|
1345
1346
|
|
|
1346
1347
|
setConversationSessionIntent({
|
|
@@ -329,6 +329,7 @@ export const executeMessageCommandRoute = async ({ command, args = [], text = ''
|
|
|
329
329
|
messageInfo,
|
|
330
330
|
expirationMessage,
|
|
331
331
|
senderJid,
|
|
332
|
+
senderIdentity,
|
|
332
333
|
args: safeArgs,
|
|
333
334
|
isGroupMessage,
|
|
334
335
|
commandPrefix,
|
|
@@ -377,7 +378,7 @@ export const executeMessageCommandRoute = async ({ command, args = [], text = ''
|
|
|
377
378
|
});
|
|
378
379
|
commandResult = await runCommand('unknown', () =>
|
|
379
380
|
sendReply(sock, remoteJid, messageInfo, expirationMessage, {
|
|
380
|
-
text: globalSuggestion ? `❌ *Comando não reconhecido*\n\nO comando *${normalizedCommand}* não está configurado ou ainda não existe.\n\n${globalSuggestion}\n\nℹ️ *Dica:* \nDigite *${commandPrefix}menu* para ver a lista geral de comandos.\n\n🚧 *Fase Beta* \nO omnizap
|
|
381
|
+
text: globalSuggestion ? `❌ *Comando não reconhecido*\n\nO comando *${normalizedCommand}* não está configurado ou ainda não existe.\n\n${globalSuggestion}\n\nℹ️ *Dica:* \nDigite *${commandPrefix}menu* para ver a lista geral de comandos.\n\n🚧 *Fase Beta* \nO omnizap ainda está em desenvolvimento e novos comandos estão sendo adicionados constantemente.` : `❌ *Comando não reconhecido*\n\nO comando *${normalizedCommand}* não está configurado ou ainda não existe.\n\nℹ️ *Dica:* \nDigite *${commandPrefix}menu* para ver a lista de comandos disponíveis.\n\n🚧 *Fase Beta* \nO omnizap ainda está em desenvolvimento e novos comandos estão sendo adicionados constantemente.`,
|
|
381
382
|
}),
|
|
382
383
|
);
|
|
383
384
|
break;
|