iachat/enterprise/app/services/hermes_builder/repairer.rb
Rodribm10 28e880d7b6 feat(captain/hermes-builder): aba Verificação com 22+ checks + reparo automático
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>
2026-05-02 15:27:40 -03:00

88 lines
3.3 KiB
Ruby

# Reparos automatizados pra checks FAIL/WARN identificados pelo Validator.
#
# Cada `repair_id` presente em Validator::Result mapeia pra um handler aqui.
# Reparos cobrem só estado em DB (Captain::Assistant, CaptainInbox, Inbox,
# config). Reparos de filesystem/systemd ficam pro CLI hermes-provision na
# VPS — UI mostra mensagem orientadora pro admin.
class HermesBuilder::Repairer
REPAIR_HANDLERS = %w[
set_engine_hermes
sync_captain_inbox_unit
set_default_typing_delay
set_default_response_delay
].freeze
def self.repair(slug:, repair_id:)
asst = ::Captain::Assistant.find_by(hermes_profile_name: slug, engine: 'hermes')
return failure("Assistant '#{slug}' não encontrado") if asst.nil?
return failure("Reparo '#{repair_id}' não suportado pela UI. Rode hermes-provision na VPS.") unless REPAIR_HANDLERS.include?(repair_id)
send("repair_#{repair_id}", asst)
rescue StandardError => e
Rails.logger.error("[HermesBuilder::Repairer] error: #{e.class}: #{e.message}")
failure("Erro: #{e.class}: #{e.message}")
end
class << self
private
# rubocop:disable Rails/SkipsModelValidations
def repair_set_engine_hermes(asst)
asst.update_columns(engine: 'hermes')
success("Engine setado pra 'hermes' no assistant #{asst.id}")
end
# rubocop:enable Rails/SkipsModelValidations
# Resincroniza CaptainInbox.captain_unit_id com Assistant.captain_unit_id.
# Esse foi o bug raiz que travou a Juliana — CaptainInbox apontava pra
# unit antiga (Dolce Amore) enquanto Assistant.captain_unit_id era a
# nova (Qnn01). Resolve_unit usa CaptainInbox como segundo nível e
# vazava unit errada quando o context['assistant_id'] não chegava.
# rubocop:disable Rails/SkipsModelValidations
def repair_sync_captain_inbox_unit(asst)
return failure('Assistant sem captain_unit_id setado — corrija primeiro pelo cadastro') if asst.captain_unit_id.blank?
ci = ::CaptainInbox.where(captain_assistant_id: asst.id).first
return failure('Sem CaptainInbox pra esse assistant — vincule no painel de inboxes primeiro') if ci.nil?
old = ci.captain_unit_id
ci.update_columns(captain_unit_id: asst.captain_unit_id)
::Captain::Unit.where(inbox_id: ci.inbox_id).where.not(id: asst.captain_unit_id).update_all(inbox_id: nil)
::Captain::Unit.where(id: asst.captain_unit_id).update_all(inbox_id: ci.inbox_id)
success("CaptainInbox.unit_id ressincronizado: #{old}#{asst.captain_unit_id}")
end
# rubocop:enable Rails/SkipsModelValidations
def repair_set_default_typing_delay(asst)
ci = ::CaptainInbox.where(captain_assistant_id: asst.id).first
return failure('Sem inbox vinculada') if ci&.inbox.nil?
ci.inbox.update!(typing_delay: 5)
success("Inbox #{ci.inbox.id} typing_delay setado pra 5s")
end
def repair_set_default_response_delay(asst)
cfg = asst.config.to_h.merge(
'response_delay' => {
'mode' => 'typing_simulation',
'chars_per_second' => 25,
'min_seconds' => 1.5,
'max_seconds' => 6.0
}
)
asst.update!(config: cfg)
success('Humanização typing_simulation ativada (default: 25 cps, 1.5s..6s)')
end
def success(msg)
{ ok: true, message: msg }
end
def failure(msg)
{ ok: false, error: msg }
end
end
end