@omnizap-system/omnizap 2.6.1 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +54 -9
- package/.github/workflows/ci.yml +3 -3
- package/.github/workflows/security-runner-hardening.yml +1 -1
- package/.github/workflows/security-zap-full-scan.yml +1 -0
- package/app/config/index.js +2 -0
- package/app/configParts/adminIdentity.js +5 -5
- package/app/configParts/baileysConfig.js +226 -55
- package/app/configParts/groupUtils.js +5 -0
- package/app/configParts/messagePersistenceService.js +143 -3
- 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 +625 -124
- 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 +88 -9
- 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 +856 -14
- package/app/modules/adminModule/groupCommandHandlers.test.js +375 -9
- 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 +132 -25
- 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 +74 -16
- package/app/modules/playModule/playCommandConstants.js +13 -7
- package/app/modules/playModule/playCommandCore.js +4 -6
- package/app/modules/playModule/{playCommandYtDlpClient.js → playCommandMediaClient.js} +684 -332
- package/app/modules/playModule/playConfigRuntime.js +5 -6
- package/app/modules/playModule/playModuleCriticalFlows.test.js +44 -59
- package/app/modules/quoteModule/AGENT.md +1 -1
- package/app/modules/quoteModule/commandConfig.json +29 -0
- package/app/modules/rpgPokemonModule/AGENT.md +1 -1
- package/app/modules/rpgPokemonModule/commandConfig.json +29 -0
- package/app/modules/statsModule/AGENT.md +1 -1
- package/app/modules/statsModule/commandConfig.json +58 -0
- package/app/modules/stickerModule/AGENT.md +1 -1
- package/app/modules/stickerModule/commandConfig.json +145 -0
- 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/stickerAutoPackByTagsRuntime.js +1 -1
- package/app/modules/stickerPackModule/stickerPackCommandHandlers.js +78 -57
- package/app/modules/stickerPackModule/stickerPackService.js +13 -6
- package/app/modules/systemMetricsModule/AGENT.md +1 -1
- package/app/modules/systemMetricsModule/commandConfig.json +29 -0
- package/app/modules/tiktokModule/AGENT.md +1 -1
- package/app/modules/tiktokModule/commandConfig.json +29 -0
- package/app/modules/userModule/AGENT.md +1 -1
- package/app/modules/userModule/commandConfig.json +29 -0
- package/app/modules/waifuPicsModule/AGENT.md +57 -27
- package/app/modules/waifuPicsModule/commandConfig.json +87 -0
- package/app/observability/metrics.js +136 -0
- package/app/services/ai/commandConfigEnrichmentService.js +229 -47
- package/app/services/ai/geminiService.js +131 -7
- package/app/services/ai/geminiService.test.js +59 -2
- package/app/services/ai/moduleAiHelpCoreService.js +33 -4
- package/app/services/group/groupMetadataService.js +24 -1
- package/app/services/infra/dbWriteQueue.js +51 -21
- package/app/services/messaging/newsBroadcastService.js +843 -27
- 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/store/aiPromptStore.js +36 -19
- 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/database/index.js +6 -0
- 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/privacy-policy-2026-03-07.md +2 -2
- package/docs/security/dsar-lgpd-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/ecosystem.prod.config.cjs +31 -11
- package/index.js +52 -18
- 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 +12 -5
- package/public/comandos/commands-catalog.json +2253 -78
- package/public/js/apps/commandsReactApp.js +267 -87
- package/public/js/apps/createPackApp.js +3 -3
- package/public/js/apps/stickersApp.js +255 -103
- package/public/js/apps/termsReactApp.js +57 -8
- package/public/js/apps/userPasswordResetReactApp.js +406 -0
- package/public/js/apps/userReactApp.js +96 -47
- package/public/js/apps/userSystemAdmReactApp.js +1506 -0
- package/public/pages/politica-de-privacidade.html +1 -1
- package/public/pages/stickers.html +5 -5
- package/public/pages/termos-de-uso-texto-integral.html +1 -1
- package/public/pages/termos-de-uso.html +1 -1
- package/public/pages/user-password-reset.html +3 -4
- package/public/pages/user-systemadm.html +8 -462
- package/public/pages/user.html +1 -1
- package/scripts/clear-whatsapp-session.sh +123 -0
- package/scripts/core-ai-mode.mjs +163 -0
- package/scripts/deploy.sh +10 -0
- package/scripts/enrich-command-config-ux-openai.mjs +492 -0
- package/scripts/generate-commands-catalog.mjs +155 -0
- package/scripts/new-whatsapp-session.sh +317 -0
- package/scripts/security-web-surface-check.mjs +218 -0
- package/server/controllers/admin/adminPanelHandlers.js +253 -3
- package/server/controllers/admin/systemAdminController.js +267 -0
- package/server/controllers/sticker/stickerCatalogController.js +9 -23
- package/server/controllers/system/contactController.js +9 -17
- package/server/controllers/system/stickerCatalogSystemContext.js +27 -6
- package/server/controllers/system/systemController.js +254 -1
- package/server/controllers/userController.js +6 -0
- package/server/email/emailTemplateService.js +3 -2
- package/server/http/httpServer.js +8 -4
- 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/whatsapp/contactEnv.js +39 -0
- package/vite.config.mjs +2 -1
- package/app/modules/playModule/local/installYtDlp.js +0 -25
- package/app/modules/playModule/local/ytDlpInstaller.js +0 -28
|
@@ -32,6 +32,8 @@ const METRICS_SERVICE = process.env.METRICS_SERVICE_NAME || process.env.ECOSYSTE
|
|
|
32
32
|
const HTTP_SLO_TARGET_MS = Math.max(50, parseEnvNumber(process.env.HTTP_SLO_TARGET_MS, 750));
|
|
33
33
|
|
|
34
34
|
const QUERY_THRESHOLDS_MS = parseThresholds(process.env.DB_QUERY_ALERT_THRESHOLDS, [500, 1000]);
|
|
35
|
+
const ADMIN_ALERT_SEVERITIES = Object.freeze(['critical', 'high', 'medium', 'low', 'unknown']);
|
|
36
|
+
const ADMIN_FEATURE_FLAG_STATES = Object.freeze(['enabled', 'disabled', 'total']);
|
|
35
37
|
|
|
36
38
|
const registry = new client.Registry();
|
|
37
39
|
let metrics = null;
|
|
@@ -375,6 +377,53 @@ const ensureMetrics = () => {
|
|
|
375
377
|
labelNames: ['outcome'],
|
|
376
378
|
registers: [registry],
|
|
377
379
|
}),
|
|
380
|
+
adminOverviewUpdatedAtSeconds: new client.Gauge({
|
|
381
|
+
name: 'omnizap_admin_overview_updated_at_seconds',
|
|
382
|
+
help: 'Timestamp Unix (s) da ultima atualizacao de snapshot do painel admin',
|
|
383
|
+
registers: [registry],
|
|
384
|
+
}),
|
|
385
|
+
adminOverviewRequestsTotal: new client.Counter({
|
|
386
|
+
name: 'omnizap_admin_overview_requests_total',
|
|
387
|
+
help: 'Total de snapshots do painel admin publicados em metricas',
|
|
388
|
+
labelNames: ['source'],
|
|
389
|
+
registers: [registry],
|
|
390
|
+
}),
|
|
391
|
+
adminCounters: new client.Gauge({
|
|
392
|
+
name: 'omnizap_admin_counters',
|
|
393
|
+
help: 'Contadores agregados do painel admin',
|
|
394
|
+
labelNames: ['counter'],
|
|
395
|
+
registers: [registry],
|
|
396
|
+
}),
|
|
397
|
+
adminDashboardQuick: new client.Gauge({
|
|
398
|
+
name: 'omnizap_admin_dashboard_quick',
|
|
399
|
+
help: 'Metricas rapidas do dashboard admin',
|
|
400
|
+
labelNames: ['metric'],
|
|
401
|
+
registers: [registry],
|
|
402
|
+
}),
|
|
403
|
+
adminSystemHealth: new client.Gauge({
|
|
404
|
+
name: 'omnizap_admin_system_health',
|
|
405
|
+
help: 'Indicadores de saude expostos no painel admin',
|
|
406
|
+
labelNames: ['metric'],
|
|
407
|
+
registers: [registry],
|
|
408
|
+
}),
|
|
409
|
+
adminAlertsTotal: new client.Gauge({
|
|
410
|
+
name: 'omnizap_admin_alerts_total',
|
|
411
|
+
help: 'Total de alertas ativos por severidade no painel admin',
|
|
412
|
+
labelNames: ['severity'],
|
|
413
|
+
registers: [registry],
|
|
414
|
+
}),
|
|
415
|
+
adminFeatureFlagsTotal: new client.Gauge({
|
|
416
|
+
name: 'omnizap_admin_feature_flags_total',
|
|
417
|
+
help: 'Distribuicao de feature flags no painel admin',
|
|
418
|
+
labelNames: ['state'],
|
|
419
|
+
registers: [registry],
|
|
420
|
+
}),
|
|
421
|
+
adminSnapshotItemsTotal: new client.Gauge({
|
|
422
|
+
name: 'omnizap_admin_snapshot_items_total',
|
|
423
|
+
help: 'Total de itens por secao no snapshot do painel admin',
|
|
424
|
+
labelNames: ['section'],
|
|
425
|
+
registers: [registry],
|
|
426
|
+
}),
|
|
378
427
|
};
|
|
379
428
|
|
|
380
429
|
return metrics;
|
|
@@ -502,6 +551,93 @@ export const recordMessagesUpsert = ({ durationMs, type, messagesCount, ok }) =>
|
|
|
502
551
|
}
|
|
503
552
|
};
|
|
504
553
|
|
|
554
|
+
export const setAdminOverviewSnapshot = ({ overview = null, source = 'admin_overview' } = {}) => {
|
|
555
|
+
const m = ensureMetrics();
|
|
556
|
+
if (!m) return;
|
|
557
|
+
if (!overview || typeof overview !== 'object') return;
|
|
558
|
+
|
|
559
|
+
const setGaugeIfFinite = (gauge, labels, value, { clampToZero = false } = {}) => {
|
|
560
|
+
const numeric = Number(value);
|
|
561
|
+
if (!Number.isFinite(numeric)) return;
|
|
562
|
+
gauge.set(labels, clampToZero ? Math.max(0, numeric) : numeric);
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const sourceLabel = normalizeLabel(source, 'admin_overview').slice(0, 32);
|
|
566
|
+
m.adminOverviewRequestsTotal.inc({ source: sourceLabel });
|
|
567
|
+
|
|
568
|
+
const updatedAtMs = Date.parse(String(overview?.updated_at || ''));
|
|
569
|
+
const updatedAtSeconds = Number.isFinite(updatedAtMs) ? updatedAtMs / 1000 : Date.now() / 1000;
|
|
570
|
+
m.adminOverviewUpdatedAtSeconds.set(updatedAtSeconds);
|
|
571
|
+
|
|
572
|
+
const counters = overview?.counters && typeof overview.counters === 'object' ? overview.counters : {};
|
|
573
|
+
const counterKeys = ['total_packs_any_status', 'total_stickers_any_status', 'active_google_sessions', 'known_google_users', 'active_bans', 'visit_events_24h', 'visit_events_7d', 'unique_visitors_7d'];
|
|
574
|
+
for (const key of counterKeys) {
|
|
575
|
+
setGaugeIfFinite(m.adminCounters, { counter: key }, counters?.[key], { clampToZero: true });
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const dashboardQuick = overview?.dashboard_quick && typeof overview.dashboard_quick === 'object' ? overview.dashboard_quick : {};
|
|
579
|
+
const quickKeys = ['bots_online', 'messages_today', 'spam_blocked_today', 'errors_5xx'];
|
|
580
|
+
for (const key of quickKeys) {
|
|
581
|
+
setGaugeIfFinite(m.adminDashboardQuick, { metric: key }, dashboardQuick?.[key], { clampToZero: true });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const systemHealth = overview?.system_health && typeof overview.system_health === 'object' ? overview.system_health : {};
|
|
585
|
+
const healthKeys = ['cpu_percent', 'ram_percent', 'http_latency_p95_ms', 'queue_pending', 'db_total_queries', 'db_slow_queries'];
|
|
586
|
+
for (const key of healthKeys) {
|
|
587
|
+
setGaugeIfFinite(m.adminSystemHealth, { metric: key }, systemHealth?.[key], { clampToZero: true });
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const moderationQueue = Array.isArray(overview?.moderation_queue) ? overview.moderation_queue : [];
|
|
591
|
+
const auditLog = Array.isArray(overview?.audit_log) ? overview.audit_log : [];
|
|
592
|
+
const users = Array.isArray(overview?.users_sessions?.users) ? overview.users_sessions.users : [];
|
|
593
|
+
const activeSessions = Array.isArray(overview?.users_sessions?.active_sessions) ? overview.users_sessions.active_sessions : [];
|
|
594
|
+
const blockedAccounts = Array.isArray(overview?.users_sessions?.blocked_accounts) ? overview.users_sessions.blocked_accounts : [];
|
|
595
|
+
const alerts = Array.isArray(overview?.alerts) ? overview.alerts : [];
|
|
596
|
+
const featureFlags = Array.isArray(overview?.feature_flags) ? overview.feature_flags : [];
|
|
597
|
+
|
|
598
|
+
m.adminSnapshotItemsTotal.set({ section: 'moderation_queue' }, moderationQueue.length);
|
|
599
|
+
m.adminSnapshotItemsTotal.set({ section: 'audit_log' }, auditLog.length);
|
|
600
|
+
m.adminSnapshotItemsTotal.set({ section: 'users' }, users.length);
|
|
601
|
+
m.adminSnapshotItemsTotal.set({ section: 'active_sessions' }, activeSessions.length);
|
|
602
|
+
m.adminSnapshotItemsTotal.set({ section: 'blocked_accounts' }, blockedAccounts.length);
|
|
603
|
+
m.adminSnapshotItemsTotal.set({ section: 'alerts' }, alerts.length);
|
|
604
|
+
m.adminSnapshotItemsTotal.set({ section: 'feature_flags' }, featureFlags.length);
|
|
605
|
+
|
|
606
|
+
const severityCounts = Object.fromEntries(ADMIN_ALERT_SEVERITIES.map((severity) => [severity, 0]));
|
|
607
|
+
for (const alert of alerts) {
|
|
608
|
+
const severityRaw = String(alert?.severity || '')
|
|
609
|
+
.trim()
|
|
610
|
+
.toLowerCase();
|
|
611
|
+
const severity = ADMIN_ALERT_SEVERITIES.includes(severityRaw) ? severityRaw : 'unknown';
|
|
612
|
+
severityCounts[severity] = Number(severityCounts[severity] || 0) + 1;
|
|
613
|
+
}
|
|
614
|
+
for (const severity of ADMIN_ALERT_SEVERITIES) {
|
|
615
|
+
m.adminAlertsTotal.set({ severity }, Number(severityCounts[severity] || 0));
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
let enabledFlags = 0;
|
|
619
|
+
let disabledFlags = 0;
|
|
620
|
+
for (const flag of featureFlags) {
|
|
621
|
+
if (flag?.is_enabled) {
|
|
622
|
+
enabledFlags += 1;
|
|
623
|
+
} else {
|
|
624
|
+
disabledFlags += 1;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const totalFlags = enabledFlags + disabledFlags;
|
|
628
|
+
for (const state of ADMIN_FEATURE_FLAG_STATES) {
|
|
629
|
+
if (state === 'enabled') {
|
|
630
|
+
m.adminFeatureFlagsTotal.set({ state }, enabledFlags);
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (state === 'disabled') {
|
|
634
|
+
m.adminFeatureFlagsTotal.set({ state }, disabledFlags);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
m.adminFeatureFlagsTotal.set({ state }, totalFlags);
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
|
|
505
641
|
export const recordRpgPlayerCreated = (value = 1) => {
|
|
506
642
|
const m = ensureMetrics();
|
|
507
643
|
if (!m) return;
|
|
@@ -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,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
|
|
|
@@ -109,8 +195,46 @@ export const createGeminiTextService = ({ apiKey = process.env.GEMINI_API_KEY, d
|
|
|
109
195
|
}
|
|
110
196
|
};
|
|
111
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
|
+
|
|
112
235
|
return {
|
|
113
236
|
defaultModel: resolvedDefaultModel,
|
|
237
|
+
transport: selectedTransport,
|
|
114
238
|
generateText,
|
|
115
239
|
};
|
|
116
240
|
};
|