Bug: Captain dispatchava pra /webhooks/captain-inbox-<inbox.id>, mas o script hermes-provision criava subscription com nome captain-inbox-<slug>. Mismatch → daemon retornava 404, Sidekiq retentava, AutoReact firava N reactions sem nunca dispatchar pro LLM. Fix: - subscription_name_for(inbox): se o assistant tem hermes_profile_name, usa "captain-inbox-<slug>" (estável por agente). Fallback pra "captain-inbox-<inbox.id>" só se não tiver slug. - subscription_signing_secret(inbox): lê de assistant.hermes_subscription_secret primeiro (DB-driven, gravado pelo script). Fallback pra env var legacy CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id>. Resultado: admin pode apontar Angelina (inbox 1) pra qualquer agente Hermes (Valentina · Hermes / Nina · Hermes / Lara.H / Juliana · Hermes) e o roteamento funciona — não depende mais de inbox.id no path. Renomeei manualmente as subscriptions de Valentina e Nina nos profiles da VPS (eram captain-inbox-1 e captain-inbox-5 legado) pra captain-inbox-valentina e captain-inbox-nina. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
116 lines
4.4 KiB
Ruby
116 lines
4.4 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 preferencial é DATA-DRIVEN: cada Captain::Assistant tem coluna
|
|
# `engine` ('captain_interno' | 'hermes'). Inboxes apontam pra um assistant
|
|
# via CaptainInbox; o engine do assistant determina o roteamento. Trocar de
|
|
# engine = trocar a association no painel, sem deploy.
|
|
#
|
|
# Por compatibilidade durante a migração (gradual), também respeitamos as
|
|
# env vars antigas: se uma inbox está em CAPTAIN_HERMES_INBOX_IDS mas o
|
|
# assistant ainda é 'captain_interno', tratamos como Hermes — assim Valentina
|
|
# continua funcionando antes do admin re-apontar a inbox no painel.
|
|
# Esse fallback deve ser removido depois que todos as inboxes migrarem.
|
|
#
|
|
# Env vars (apenas credenciais — config funcional vive no DB):
|
|
# CAPTAIN_HERMES_CALLBACK_SECRET HMAC-SHA256 secret pra validar
|
|
# callback do Hermes
|
|
# (X-Hermes-Callback-Signature).
|
|
# CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_<id>
|
|
# Per-inbox secret retornado pelo
|
|
# `hermes webhook subscribe`. Usado
|
|
# pra assinar o POST OUTGOING.
|
|
#
|
|
# Env vars LEGACY (em descontinuação — preferir DB):
|
|
# CAPTAIN_HERMES_INBOX_IDS CSV de inbox.id. Usado só como
|
|
# fallback até as inboxes terem
|
|
# assistant com engine='hermes'.
|
|
# CAPTAIN_HERMES_WEBHOOK_BASE_URL Base URL default. Idem.
|
|
# CAPTAIN_HERMES_BASE_URL_INBOX_<id> Per-inbox base URL. Idem.
|
|
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)
|
|
|
|
return true if assistant_for(inbox)&.hermes?
|
|
|
|
legacy_inbox_ids.include?(inbox.id)
|
|
end
|
|
|
|
def assistant_for(inbox)
|
|
return nil if inbox.blank?
|
|
return nil unless inbox.respond_to?(:captain_inbox)
|
|
|
|
inbox.captain_inbox&.captain_assistant
|
|
end
|
|
|
|
def webhook_base_url(inbox = nil)
|
|
assistant = assistant_for(inbox)
|
|
return assistant.hermes_webhook_base_url.chomp('/') if assistant&.hermes? && assistant.hermes_webhook_base_url.present?
|
|
|
|
legacy_webhook_base_url(inbox)
|
|
end
|
|
|
|
def webhook_url_for(inbox)
|
|
"#{webhook_base_url(inbox)}/webhooks/#{subscription_name_for(inbox)}"
|
|
end
|
|
|
|
# Convenção de nome de subscription no Hermes:
|
|
# - Pra Hermes assistant criado pelo Construtor (tem hermes_profile_name):
|
|
# usa "captain-inbox-<slug>" (única por agente, independente de qual
|
|
# inbox o admin atrelou).
|
|
# - Pra agentes legados (Valentina, Nina) criados antes do Construtor:
|
|
# fallback pro padrão velho "captain-inbox-<inbox.id>".
|
|
def subscription_name_for(inbox)
|
|
assistant = assistant_for(inbox)
|
|
if assistant&.hermes_profile_name.present?
|
|
"captain-inbox-#{assistant.hermes_profile_name}"
|
|
else
|
|
"captain-inbox-#{inbox.id}"
|
|
end
|
|
end
|
|
|
|
def subscription_signing_secret(inbox)
|
|
assistant = assistant_for(inbox)
|
|
return assistant.hermes_subscription_secret if assistant&.hermes_subscription_secret.present?
|
|
|
|
ENV.fetch("CAPTAIN_HERMES_SUBSCRIPTION_SECRET_INBOX_#{inbox.id}", nil)
|
|
end
|
|
|
|
def callback_signing_secret
|
|
ENV.fetch('CAPTAIN_HERMES_CALLBACK_SECRET', nil)
|
|
end
|
|
|
|
def reset_cache!
|
|
@legacy_inbox_ids = nil
|
|
end
|
|
|
|
# === Legacy (env var) fallbacks ===
|
|
|
|
def legacy_inbox_ids
|
|
@legacy_inbox_ids ||= ENV.fetch('CAPTAIN_HERMES_INBOX_IDS', '')
|
|
.split(',')
|
|
.map { |s| s.strip.to_i }
|
|
.reject(&:zero?)
|
|
.freeze
|
|
end
|
|
|
|
def legacy_webhook_base_url(inbox = nil)
|
|
if inbox && (per_inbox = ENV.fetch("CAPTAIN_HERMES_BASE_URL_INBOX_#{inbox.id}", nil)).present?
|
|
return per_inbox.chomp('/')
|
|
end
|
|
|
|
(ENV['CAPTAIN_HERMES_WEBHOOK_BASE_URL'].presence || DEFAULT_BASE_URL).chomp('/')
|
|
end
|
|
end
|