iachat/enterprise/app/services/captain/hermes.rb
Rodribm10 35de8b7fde feat(captain): cliente Captain ↔ Hermes (outgoing job + callback endpoint)
Implementa o lado Captain da integração Nível 2 (Hermes como cérebro).
Ativação por inbox via env var CAPTAIN_HERMES_INBOX_IDS — inboxes não
listadas seguem usando o orquestrador interno do Captain (Daniela_Reservas
etc) sem mudança alguma. Princípio "só adiciona, não retira".

Componentes:

- enterprise/app/services/captain/hermes.rb
  Módulo helper de config (env vars, URLs, secrets per-inbox).

- enterprise/app/services/captain/hermes/client.rb
  Service que monta payload (msg + contexto da conversa/inbox/contato) e
  faz POST autenticado via HMAC-SHA256 (X-Hub-Signature-256) no webhook
  do Hermes Agent (porta 8644). DispatchError em falha de rede/HTTP.

- enterprise/app/jobs/captain/hermes/outgoing_job.rb
  Wrapper Sidekiq do Client. Retry 3x em DispatchError.

- app/controllers/webhooks/captain/hermes_callback_controller.rb
  Recebe callback do plugin captain-http-callback do Hermes. Valida HMAC
  se CAPTAIN_HERMES_CALLBACK_SECRET setado, identifica conversation pela
  última pending da inbox (janela 5min) e cria mensagem outgoing.

- config/routes.rb
  Rota POST /webhooks/captain/hermes_callback (fora de /api/v1/accounts).

- enterprise/app/services/enterprise/message_templates/hook_execution_service.rb
  Branch novo no schedule_captain_response: se Hermes habilitado pra inbox,
  dispara HermesOutgoingJob; senão, fluxo Captain interno como antes.

Env vars (todas opcionais; sem set = Hermes desabilitado em todas inboxes):
- CAPTAIN_HERMES_INBOX_IDS (CSV de inbox.id)
- CAPTAIN_HERMES_WEBHOOK_BASE_URL (default http://172.17.0.1:8644)
- CAPTAIN_HERMES_CALLBACK_SECRET (HMAC validar callbacks de entrada)
- CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id> (HMAC assinar saídas)

Limitação: identificação da conversation no callback usa última pending
da inbox dentro de 5min. OK pra PoC com 1 conversa de teste por vez. Em
produção, melhorar mapeando delivery_id ↔ conversation_id em Redis.

Próximo passo manual (admin VPS): criar subscription no Hermes:
  hermes webhook subscribe captain-inbox-1 \\
    --prompt 'Cliente disse: {message}. Responda como Daniela ...' \\
    --deliver http_callback \\
    --deliver-chat-id 'http://CAPTAIN_HOST/webhooks/captain/hermes_callback?inbox_id=1'

Depois set CAPTAIN_HERMES_INBOX_IDS=1 + CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_1
no stack do Captain e testar pela inbox Angelina.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 13:22:22 -03:00

76 lines
2.9 KiB
Ruby

# Configuração compartilhada da integração Captain ↔ Hermes Agent.
#
# A integração usa o Hermes como cérebro do atendimento (Nível 2):
# - Captain recebe msg WhatsApp
# - Dispara webhook do Hermes (POST /webhooks/captain-inbox-<id>)
# - Hermes processa via subscription Codex/etc dele
# - Hermes invoca plugin captain-http-callback que POSTa de volta no Captain
# - Captain cria mensagem outgoing e envia pro WhatsApp
#
# A ativação é por inbox via env var. As 9 outras inboxes do Captain seguem
# usando o orquestrador interno (Daniela_Reservas, etc) sem mudança.
#
# Env vars:
# CAPTAIN_HERMES_INBOX_IDS CSV de inbox.id (ex: "1,5"). Se vazio,
# desativa em todas. Inboxes não listadas
# continuam no fluxo Captain interno.
# CAPTAIN_HERMES_WEBHOOK_BASE_URL Base URL do gateway Hermes
# (default http://172.17.0.1:8644).
# CAPTAIN_HERMES_CALLBACK_SECRET HMAC-SHA256 secret pra validar callback
# do Hermes (X-Hermes-Callback-Signature).
# Se vazio, validação é desabilitada (NÃO
# recomendado em prod).
# CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id>
# Per-inbox secret retornado pelo
# `hermes webhook subscribe`. Usado pra
# assinar o POST OUTGOING. Sem ele, o
# Hermes vai rejeitar o webhook.
module Captain::Hermes
DEFAULT_BASE_URL = 'http://172.17.0.1:8644'.freeze
module_function
def enabled_for?(inbox)
return false if inbox.blank?
return false unless inbox.respond_to?(:id)
inbox_ids.include?(inbox.id)
end
def inbox_ids
@inbox_ids ||= ENV.fetch('CAPTAIN_HERMES_INBOX_IDS', '')
.split(',')
.map { |s| s.strip.to_i }
.reject(&:zero?)
.freeze
end
def webhook_base_url
@webhook_base_url ||= (ENV['CAPTAIN_HERMES_WEBHOOK_BASE_URL'].presence || DEFAULT_BASE_URL).chomp('/')
end
def webhook_url_for(inbox)
"#{webhook_base_url}/webhooks/#{subscription_name_for(inbox)}"
end
# Convenção de nome de subscription no Hermes: precisa bater com o que o
# admin criou via `hermes webhook subscribe captain-inbox-<id> ...`.
def subscription_name_for(inbox)
"captain-inbox-#{inbox.id}"
end
def subscription_signing_secret(inbox)
ENV.fetch("CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_#{inbox.id}", nil)
end
def callback_signing_secret
ENV.fetch('CAPTAIN_HERMES_CALLBACK_SECRET', nil)
end
# Reseta caches. Útil em specs ou após reload de config.
def reset_cache!
@inbox_ids = nil
@webhook_base_url = nil
end
end