UI nova dentro do Construtor (Hermes) — TabBar com Chat e Verificação. Verificação roda HermesBuilder::Validator (DB+runtime) e exibe resultado agrupado por categoria, com botão Refazer inline em FAIL/WARN reparáveis. Backend (porta dos checks DB do CLI bin/hermes-validate): - HermesBuilder::Validator com 22+ checks: engine, profile, port, secret, parent, unit, Brand, CaptainInbox sync (o bug que travou Juliana), pricing dry-run, Inter creds, typing/response_delay, registry MCP completo. - HermesBuilder::Repairer com 4 handlers automáticos: set_engine_hermes, sync_captain_inbox_unit, set_default_typing_delay, set_default_response_delay. - Endpoints novos: GET assistants, GET validate?slug=, POST repair. Frontend: - builder/Index.vue: wrapper com TabBar. - builder/BuilderChat.vue: extraído do Index original. - builder/BuilderVerification.vue: dropdown + Conferir agora + lista agrupada por categoria com badges + botão Refazer inline. i18n: keys em pt_BR e en sob CAPTAIN_HERMES_BUILDER.VERIFY.*. Filesystem/systemd checks ficam pro CLI hermes-validate (Rails container não enxerga /root/.hermes/profiles do host). Validado HTTP: GET /validate?slug=juliana_qnn1 → 28 PASS / 0 FAIL / 1 WARN. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
200 lines
7.3 KiB
Ruby
200 lines
7.3 KiB
Ruby
# Validação de saúde de um agente Hermes — porta dos checks DB/runtime do
|
|
# script CLI `bin/hermes-validate` pro contexto Rails (consumido pela aba
|
|
# "Verificação" no Construtor UI).
|
|
#
|
|
# Cobre: configuração do Captain::Assistant, mapeamento CaptainInbox/Unit,
|
|
# pricing dry-run, credenciais Inter, registry MCP, humanização. NÃO cobre
|
|
# filesystem (SOUL.md/SKILL.md/config.yaml) nem systemd — esses ficam pro
|
|
# CLI (rodar no shell da VPS) porque o container Rails não tem visibilidade
|
|
# do `/root/.hermes/profiles/` do host.
|
|
#
|
|
# Cada check tem `repair_id` opcional — quando setado, indica que existe
|
|
# handler em HermesBuilder::Repairer pra ressincronizar/corrigir o estado
|
|
# (ex: sync_captain_inbox_unit, set_engine_hermes, set_default_typing).
|
|
class HermesBuilder::Validator
|
|
EXPECTED_MCP_TOOLS = %w[
|
|
generate_pix faq_lookup add_label send_suite_images react_to_message
|
|
update_contact get_contact_history check_pix_payment reschedule_reservation
|
|
].freeze
|
|
|
|
Result = Struct.new(:label, :status, :detail, :repair_id, :category, keyword_init: true) do
|
|
def to_h
|
|
{ label: label, status: status, detail: detail.to_s, repair_id: repair_id, category: category }
|
|
end
|
|
end
|
|
|
|
def self.run(slug)
|
|
new(slug).call
|
|
end
|
|
|
|
def initialize(slug)
|
|
@slug = slug.to_s.strip
|
|
@results = []
|
|
end
|
|
|
|
def call
|
|
asst = ::Captain::Assistant.find_by(hermes_profile_name: @slug, engine: 'hermes')
|
|
if asst.nil?
|
|
add('Captain::Assistant existe', 'FAIL', "Nenhum assistant com hermes_profile_name='#{@slug}'", category: 'db')
|
|
return summary
|
|
end
|
|
|
|
@asst = asst
|
|
@ci = ::CaptainInbox.where(captain_assistant_id: asst.id).first
|
|
@inbox = @ci&.inbox
|
|
@unit = asst.captain_unit
|
|
|
|
check_db
|
|
check_pricing
|
|
check_routing
|
|
check_humanization
|
|
check_mcp_tools
|
|
summary
|
|
end
|
|
|
|
private
|
|
|
|
def check_db
|
|
check_db_engine
|
|
check_db_endpoint
|
|
check_db_unit_consistency
|
|
end
|
|
|
|
def check_db_engine
|
|
add('engine=hermes', pf(@asst.engine == 'hermes'), @asst.engine,
|
|
repair_id: 'set_engine_hermes', category: 'db')
|
|
add('hermes_profile_name setado', pf(@asst.hermes_profile_name.present?),
|
|
@asst.hermes_profile_name, category: 'db')
|
|
add('parent_assistant_id setado', pw(@asst.parent_assistant_id.present?),
|
|
"parent=#{@asst.parent_assistant_id}", category: 'db')
|
|
end
|
|
|
|
def check_db_endpoint
|
|
add('hermes_port setado', pf(@asst.hermes_port.present?),
|
|
"port=#{@asst.hermes_port}", category: 'db')
|
|
add('hermes_subscription_secret setado', pf(@asst.hermes_subscription_secret.present?),
|
|
@asst.hermes_subscription_secret.present? ? 'presente' : 'vazio', category: 'db')
|
|
add('hermes_webhook_base_url', pf(@asst.hermes_webhook_base_url.to_s.start_with?('http')),
|
|
@asst.hermes_webhook_base_url, category: 'db')
|
|
end
|
|
|
|
def pf(bool) = bool ? 'PASS' : 'FAIL'
|
|
def pw(bool) = bool ? 'PASS' : 'WARN'
|
|
|
|
def check_db_unit_consistency
|
|
add('captain_unit_id setado', @asst.captain_unit_id.present? ? 'PASS' : 'FAIL',
|
|
@unit&.name, category: 'db')
|
|
|
|
ci_unit = @ci&.captain_unit_id
|
|
sync_ok = @asst.captain_unit_id.present? && ci_unit == @asst.captain_unit_id
|
|
add('CaptainInbox.unit == Assistant.unit', sync_ok ? 'PASS' : 'FAIL',
|
|
"asst=#{@asst.captain_unit_id} ci=#{ci_unit}",
|
|
repair_id: sync_ok ? nil : 'sync_captain_inbox_unit', category: 'db')
|
|
|
|
add('Brand resolvida', @unit&.brand.present? ? 'PASS' : 'FAIL', @unit&.brand&.name, category: 'db')
|
|
add('CaptainInbox mapeada', @inbox.present? ? 'PASS' : 'WARN', "inbox=#{@inbox&.id}", category: 'db')
|
|
end
|
|
|
|
def check_pricing
|
|
cats = (@unit&.pricing_categories&.includes(:amounts)&.to_a) || []
|
|
add('Pricing categorias > 0', cats.any? ? 'PASS' : 'FAIL',
|
|
"#{cats.size} cats: #{cats.map(&:key).join(',')}", category: 'pricing')
|
|
add('Pricing amounts > 0', cats.flat_map { |c| c.amounts.to_a }.size.positive? ? 'PASS' : 'FAIL',
|
|
"#{cats.flat_map { |c| c.amounts.to_a }.size} amounts", category: 'pricing')
|
|
|
|
check_pricing_dry_run(cats)
|
|
check_inter_creds
|
|
end
|
|
|
|
def check_pricing_dry_run(cats)
|
|
return if cats.empty?
|
|
|
|
first_cat = cats.first.key
|
|
res = ::Captain::Mcp::PricingTables.calculate(
|
|
unit_id: @unit.id, suite_category: first_cat,
|
|
period: 'pernoite_promo', total_guests: 2
|
|
)
|
|
if res[:error]
|
|
add('Pricing dry-run', 'FAIL', "ERR: #{res[:error]}", category: 'pricing')
|
|
else
|
|
add('Pricing dry-run', 'PASS', "OK R$ #{res[:amount]} (#{first_cat}/pernoite)", category: 'pricing')
|
|
end
|
|
end
|
|
|
|
def check_inter_creds
|
|
inter_ok = @unit.present? && @unit.respond_to?(:inter_credentials_present?) && @unit.inter_credentials_present?
|
|
add('Credenciais Inter completas', pw(inter_ok),
|
|
inter_ok ? 'cert+key+client_id presentes' : 'faltam — generate_pix cai no fallback',
|
|
category: 'pricing')
|
|
end
|
|
|
|
def check_routing
|
|
if @inbox.blank?
|
|
add('Captain::Hermes.enabled_for?', 'WARN', 'sem inbox mapeada', category: 'routing')
|
|
return
|
|
end
|
|
|
|
enabled = ::Captain::Hermes.enabled_for?(@inbox)
|
|
add('Captain::Hermes.enabled_for?', enabled ? 'PASS' : 'FAIL', enabled.to_s, category: 'routing')
|
|
|
|
url = ::Captain::Hermes.webhook_url_for(@inbox).to_s
|
|
expected_suffix = "/webhooks/captain-inbox-#{@slug}"
|
|
add('webhook_url aponta pra slug', url.end_with?(expected_suffix) ? 'PASS' : 'FAIL',
|
|
url, category: 'routing')
|
|
|
|
secret = ::Captain::Hermes.subscription_signing_secret(@inbox).to_s
|
|
add('subscription_signing_secret presente', secret.present? ? 'PASS' : 'FAIL',
|
|
secret.present? ? "#{secret.first(8)}..." : 'vazio', category: 'routing')
|
|
end
|
|
|
|
def check_humanization
|
|
typing_delay = @inbox.respond_to?(:typing_delay) ? @inbox.typing_delay.to_i : 0
|
|
add('Inbox.typing_delay > 0 (debounce)', typing_delay.positive? ? 'PASS' : 'WARN',
|
|
"#{typing_delay}s",
|
|
repair_id: typing_delay.positive? ? nil : 'set_default_typing_delay',
|
|
category: 'humanization')
|
|
|
|
mode = @asst.config.to_h.dig('response_delay', 'mode')
|
|
add('config.response_delay (typing simulation)', mode == 'typing_simulation' ? 'PASS' : 'WARN',
|
|
mode || 'none',
|
|
repair_id: mode == 'typing_simulation' ? nil : 'set_default_response_delay',
|
|
category: 'humanization')
|
|
|
|
gallery_count = @unit&.gallery_items&.count || 0
|
|
add('GalleryItem para fotos', gallery_count.positive? ? 'PASS' : 'WARN',
|
|
"#{gallery_count} items (send_suite_images precisa)", category: 'humanization')
|
|
end
|
|
|
|
def check_mcp_tools
|
|
registered = mcp_tool_names
|
|
EXPECTED_MCP_TOOLS.each do |t|
|
|
add("MCP tool '#{t}' registrado", registered.include?(t) ? 'PASS' : 'FAIL', nil, category: 'mcp')
|
|
end
|
|
end
|
|
|
|
def mcp_tool_names
|
|
::Captain::Mcp::ToolRegistry::TOOLS.map(&:name)
|
|
rescue StandardError
|
|
[]
|
|
end
|
|
|
|
def add(label, status, detail = nil, repair_id: nil, category: nil)
|
|
@results << Result.new(label: label, status: status, detail: detail, repair_id: repair_id, category: category)
|
|
end
|
|
|
|
def summary
|
|
pass = @results.count { |r| r.status == 'PASS' }
|
|
fail_n = @results.count { |r| r.status == 'FAIL' }
|
|
warn = @results.count { |r| r.status == 'WARN' }
|
|
{
|
|
slug: @slug,
|
|
ok: fail_n.zero?,
|
|
total: @results.size,
|
|
pass: pass,
|
|
fail: fail_n,
|
|
warn: warn,
|
|
results: @results.map(&:to_h)
|
|
}
|
|
end
|
|
end
|