iachat/enterprise/app/services/captain/hermes/client.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

77 lines
2.4 KiB
Ruby

# Cliente HTTP que dispara mensagens do Captain pro webhook do Hermes Agent.
#
# Uso:
# Captain::Hermes::Client.new(inbox).dispatch(message: msg, conversation: conv)
#
# Resultado: POST autenticado via HMAC-SHA256 (X-Hub-Signature-256) no endpoint
# /webhooks/<subscription_name> do Hermes. O Hermes responde 202 imediato e
# processa em background. Quando terminar, invoca o plugin captain-http-callback
# que POSTa de volta no Captain (HermesCallbackController).
class Captain::Hermes::Client
TIMEOUT_SECONDS = 10
class DispatchError < StandardError; end
def initialize(inbox)
@inbox = inbox
end
def dispatch(message:, conversation:)
payload = build_payload(message: message, conversation: conversation)
body = payload.to_json
headers = signed_headers(body)
Rails.logger.info "[Captain::Hermes::Client] dispatching msg #{message.id} (conv #{conversation.display_id}) → #{webhook_url}"
response = HTTParty.post(
webhook_url,
body: body,
headers: headers,
timeout: TIMEOUT_SECONDS
)
return response if response.success? || response.code == 202
raise DispatchError, "Hermes webhook returned HTTP #{response.code}: #{response.body.to_s.truncate(300)}"
rescue HTTParty::Error, Net::ReadTimeout, Net::OpenTimeout, Errno::ECONNREFUSED => e
raise DispatchError, "Network error contacting Hermes (#{e.class}): #{e.message}"
end
private
attr_reader :inbox
def webhook_url
Captain::Hermes.webhook_url_for(inbox)
end
def build_payload(message:, conversation:)
{
message: message.content.to_s,
contact_name: conversation.contact&.name,
contact_id: conversation.contact_id,
conversation_id: conversation.display_id,
conversation_internal_id: conversation.id,
inbox_id: inbox.id,
inbox_name: inbox.name,
account_id: inbox.account_id,
message_id: message.id,
timestamp: Time.current.to_i
}
end
def signed_headers(body)
headers = { 'Content-Type' => 'application/json; charset=utf-8' }
secret = Captain::Hermes.subscription_signing_secret(inbox)
if secret.present?
sig = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
headers['X-Hub-Signature-256'] = "sha256=#{sig}"
else
Rails.logger.warn "[Captain::Hermes::Client] no signing secret for inbox #{inbox.id} — Hermes will reject"
end
headers
end
end