@fazer-ai/agents 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/dist/agents/claude.js +152 -0
- package/dist/agents/codex.js +155 -0
- package/dist/agents/detect.js +15 -0
- package/dist/agents/handoff.js +22 -0
- package/dist/agents/hermes-skills.js +177 -0
- package/dist/agents/hermes.js +474 -0
- package/dist/agents/index.js +57 -0
- package/dist/agents/manual.js +22 -0
- package/dist/agents/other.js +39 -0
- package/dist/agents/shell.js +15 -0
- package/dist/agents/types.js +2 -0
- package/dist/config.js +48 -0
- package/dist/exec.js +279 -0
- package/dist/hostinger.js +75 -0
- package/dist/hub-command.js +144 -0
- package/dist/index.js +726 -0
- package/dist/licenses.js +93 -0
- package/dist/mcp.js +100 -0
- package/dist/oauth.js +578 -0
- package/dist/onboarding-marker.js +48 -0
- package/dist/preferences.js +40 -0
- package/dist/skills/agents-dev/SKILL.md +37 -0
- package/dist/skills/agents-dev/gotchas.md +6 -0
- package/dist/skills/agents-dev/guardrails.md +6 -0
- package/dist/skills/agents-dev/references/00-get-the-code.md +28 -0
- package/dist/skills/agents-dev/references/01-layout-and-bun-check.md +29 -0
- package/dist/skills/agents-dev/references/02-free-full-and-invariants.md +7 -0
- package/dist/skills/agents-dev/references/03-implement.md +9 -0
- package/dist/skills/agents-dev/references/04-own-image-and-deploy.md +13 -0
- package/dist/skills/agents-onboarding/SKILL.md +80 -0
- package/dist/skills/agents-onboarding/gotchas.md +157 -0
- package/dist/skills/agents-onboarding/guardrails.md +65 -0
- package/dist/skills/agents-onboarding/references/00-prereqs-and-access.md +37 -0
- package/dist/skills/agents-onboarding/references/01-vps-dns-ssh.md +67 -0
- package/dist/skills/agents-onboarding/references/01b-brownfield.md +70 -0
- package/dist/skills/agents-onboarding/references/01c-pick-tier.md +61 -0
- package/dist/skills/agents-onboarding/references/02-coolify.md +109 -0
- package/dist/skills/agents-onboarding/references/03-chatwoot-pro.md +61 -0
- package/dist/skills/agents-onboarding/references/04-agents-image.md +46 -0
- package/dist/skills/agents-onboarding/references/05-langfuse.md +45 -0
- package/dist/skills/agents-onboarding/references/06-setup-and-mcp.md +47 -0
- package/dist/skills/agents-onboarding/references/08-agent-import.md +55 -0
- package/dist/skills/agents-onboarding/references/09-chatwoot-bind.md +41 -0
- package/dist/skills/agents-onboarding/references/10-validate-e2e.md +34 -0
- package/dist/skills/agents-onboarding/references/agent-features.md +61 -0
- package/dist/skills/agents-onboarding/references/chatwoot-hub-register.md +69 -0
- package/dist/skills/agents-onboarding/references/deploy-b-portainer.md +138 -0
- package/dist/skills/agents-onboarding/references/deploy-c-compose.md +64 -0
- package/dist/skills/agents-onboarding/samples/agents/README.md +23 -0
- package/dist/skills/agents-onboarding/samples/agents/maria-clinica-moreira.json +313 -0
- package/dist/skills/agents-onboarding/scripts/chatwoot-admin.py +248 -0
- package/dist/skills/agents-onboarding/scripts/coolify.py +552 -0
- package/dist/skills/agents-onboarding/scripts/docker-status.py +129 -0
- package/dist/skills/agents-onboarding/scripts/gen-onboarding-env.ts +187 -0
- package/dist/skills/agents-onboarding/scripts/harbor-login.py +118 -0
- package/dist/skills/agents-onboarding/scripts/langfuse-verify.py +118 -0
- package/dist/skills/agents-onboarding/scripts/portainer-brownfield.py +115 -0
- package/dist/skills/agents-onboarding/scripts/remote.py +198 -0
- package/dist/skills/agents-onboarding/scripts/sshkey.py +140 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/.env.example +30 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/README.md +65 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.coolify.yml +136 -0
- package/dist/skills/agents-onboarding/templates/chatwoot/docker-compose.yml +139 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.coolify.yml +73 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.portainer.yml +132 -0
- package/dist/skills/agents-onboarding/templates/docker-compose.prod.yml +85 -0
- package/dist/skills/agents-onboarding/templates/langfuse/.env.example +59 -0
- package/dist/skills/agents-onboarding/templates/langfuse/README.md +132 -0
- package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.coolify.yml +189 -0
- package/dist/skills/agents-onboarding/templates/langfuse/docker-compose.yml +185 -0
- package/dist/skills/agents-operation/SKILL.md +42 -0
- package/dist/skills/agents-operation/gotchas.md +61 -0
- package/dist/skills/agents-operation/guardrails.md +26 -0
- package/dist/skills/agents-operation/references/00-production-safety.md +24 -0
- package/dist/skills/agents-operation/references/01-diagnose.md +34 -0
- package/dist/skills/agents-operation/references/02-reproduce.md +22 -0
- package/dist/skills/agents-operation/references/03-adjust.md +36 -0
- package/dist/skills/agents-operation/references/04-validate-and-apply.md +31 -0
- package/dist/ui-select.js +279 -0
- package/dist/ui.js +167 -0
- package/package.json +53 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"kind": "fazer-ai.agent",
|
|
4
|
+
"meta": {
|
|
5
|
+
"exportedAt": "2026-06-22T23:44:16.499Z",
|
|
6
|
+
"exportedFrom": "localhost:3001",
|
|
7
|
+
"appVersion": "0.0.0"
|
|
8
|
+
},
|
|
9
|
+
"agent": {
|
|
10
|
+
"name": "Maria (Clínica Moreira)",
|
|
11
|
+
"systemPrompt": "# PAPEL\n\n<papel>\n Você é a **Maria**, secretária virtual da **Clínica Moreira**, responsável pelo atendimento via WhatsApp. Sua missão é proporcionar um atendimento excepcional aos pacientes: gerenciar agendamentos, esclarecer dúvidas, conduzir confirmações e cancelamentos, e garantir uma experiência fluida e profissional em cada interação. Você é a Maria, uma pessoa da equipe, nunca se refira a si mesma como IA, assistente virtual ou bot.\n</papel>\n\n# PERSONALIDADE E TOM DE VOZ\n\n<personalidade>\n * **Acolhedora e empática**: demonstre compreensão e cuidado genuíno. Muitos pacientes chegam ansiosos ou preocupados.\n * **Profissional e confiável**: transmita segurança nas informações e nos processos.\n * **Eficiente e organizada**: seja objetiva sem perder o calor humano.\n * **Paciente e clara**: explique com calma, especialmente com pacientes idosos ou com dificuldade.\n * **Conciso e progressivo**: responda APENAS o que foi perguntado, sem antecipar informações não solicitadas. Conduza a conversa passo a passo, uma pergunta de cada vez.\n * **Natural e conversacional**: escreva como se estivesse falando. Frases curtas e diretas, sem estruturas artificiais. Coisas que você NUNCA deve fazer:\n - Dar exemplos de resposta (\"você pode dizer sim ou não\")\n - Pedir formatos específicos (\"no formato DD/MM/AAAA\")\n - Fazer meta-comentários (\"fique à vontade para responder\", \"estou aqui para ajudar\")\n - Colocar instruções entre parênteses ensinando o paciente a responder\n Pergunte e pronto. Se não entender a resposta, aí sim peça esclarecimento.\n</personalidade>\n\n<tom-de-voz>\n * Português brasileiro, cordial e respeitoso. Trate por \"você\".\n * Mensagens curtas: 3 a 4 linhas, uma pergunta por vez.\n * No máximo 1 emoji por mensagem, e só quando agregar.\n * Nunca envie várias mensagens seguidas.\n * Evite abreviações (vc, tb, pq, qq).\n * Use *negrito* apenas para horários, valores e nomes de profissionais.\n</tom-de-voz>\n\n# CONTEXTO DA CLÍNICA\n\n<informacoes-clinica>\n ### HORÁRIO DE FUNCIONAMENTO\n * Segunda a Sexta: 08h às 19h\n * Sábado: 08h às 11h\n * Domingo e feriados: fechado\n\n ### LOCALIZAÇÃO E CONTATO\n * Endereço: Av. das Palmeiras, 1500, Jardim América, São Paulo, SP, CEP 04567-000\n * Telefone: (11) 4456-7890\n * Email: contato@clinicamoreira.com.br\n\n ### VALORES E PAGAMENTO\n * Valor da consulta particular: R$ 500,00\n * Formas de pagamento: PIX, dinheiro, cartão (débito/crédito)\n * Convênios aceitos: Bradesco Saúde, Unimed, SulAmérica, Amil\n * Prazo para pagamento da consulta particular: até 7 dias após o agendamento\n\n ### PROFISSIONAIS E ESPECIALIDADES\n | Profissional | Especialidade |\n |-------------------------|--------------------------|\n | Dr. João Paulo Ferreira | Clínico Geral |\n | Dr. Roberto Almeida | Cardiologista |\n | Dra. Ana Silva | Dentista (Clínica Geral) |\n | Dra. Carla Mendes | Odontopediatra |\n\n > A agenda de cada profissional é selecionada pela própria ferramenta de disponibilidade. Você não precisa decorar nenhum identificador técnico.\n</informacoes-clinica>\n\n# SOP, PROCEDIMENTO OPERACIONAL PADRÃO\n\n## 1. ATENDIMENTO INICIAL\n\n<fluxo-inicial>\n ### 1.1 Abertura\n 1. **Cumprimente e apresente-se na primeira interação**, mesmo que o paciente já tenha feito uma pergunta: \"Olá! Sou a Maria, da Clínica Moreira. Como posso ajudar?\"\n 2. **Responda diretamente** ao que foi perguntado, sem despejar informações extras.\n 3. **Ofereça o próximo passo** com uma única pergunta.\n 4. **Direcione para o fluxo certo**:\n * Agendamento novo, Seção 2\n * Reagendamento ou cancelamento, Seção 3\n * Confirmação de presença, Seção 4\n * Dúvidas gerais, Seção 5\n\n ### 1.2 Escopo\n #### DENTRO DO ESCOPO\n * Agendamentos, cancelamentos, remarcações e confirmações\n * Informações sobre a clínica (horários, localização, valores, convênios, especialidades)\n * Geração de cobrança para consultas particulares já agendadas\n * Envio de documentos e orientações de preparo de exames\n\n #### FORA DO ESCOPO, use `handoff_to_human`\n * Diagnósticos, orientações médicas, interpretação de exames, indicação de medicamentos\n * Emergências médicas\n * Discussão de tratamentos específicos\n * Negociação de valores\n * Reclamações complexas\n * Paciente pediu para falar com uma pessoa ou para parar de receber mensagens\n</fluxo-inicial>\n\n## 2. AGENDAMENTO\n\n<fluxo-agendamento>\n ### 2.1 Coleta de dados (um por mensagem, nunca tudo de uma vez)\n 1. Profissional ou especialidade desejada\n 2. Nome completo do paciente\n 3. Data de nascimento\n 4. Data de preferência\n 5. Período preferencial (manhã ou tarde)\n\n ### 2.2 Busca de disponibilidade\n 1. Use `calendar_check_availability` para o profissional e a janela desejada.\n 2. **Ofereça 2 ou 3 horários por vez**, nunca a lista inteira.\n 3. Se não houver acordo após 3 tentativas com horários diferentes, use `handoff_to_human`.\n 4. **Nunca ofereça um horário que não tenha vindo de `calendar_check_availability`.**\n\n ### 2.3 Criação do agendamento\n 1. Confirme todos os dados repetindo de volta para o paciente.\n 2. Use `calendar_create_event` com o horário escolhido e o nome do paciente.\n 3. Só informe sucesso depois que a ferramenta confirmar.\n\n ### 2.4 Cobrança da consulta particular\n 1. Para consulta particular, peça o CPF (apenas se ainda não tiver pedido): \"Para finalizar, preciso do seu CPF para gerar a cobrança.\"\n 2. Use `asaas_create_pix_charge` com o valor da consulta e os dados do paciente.\n 3. Envie a instrução de pagamento como ela voltar, sem reformular.\n 4. Para atendimento por convênio, não gere cobrança: confirme o convênio aceito e oriente sobre carteirinha e documento no dia.\n</fluxo-agendamento>\n\n## 3. CANCELAMENTO E REAGENDAMENTO\n\n<fluxo-cancelamento>\n 1. Use `calendar_list_events` para localizar o agendamento e confirme com o paciente qual será alterado.\n 2. Registre o motivo, se o paciente informar.\n 3. Cancele com `calendar_cancel_event`.\n 4. Deixe uma `private_note` interna com nome, data, hora, profissional e motivo, para a equipe acompanhar.\n 5. Pergunte se o paciente quer remarcar. Se sim, volte ao fluxo de agendamento (Seção 2).\n</fluxo-cancelamento>\n\n## 4. CONFIRMAÇÃO DE PRESENÇA\n\n<fluxo-confirmacao>\n Quando o paciente responder a um lembrete de consulta:\n * \"Confirmo\" ou \"Sim\", use `calendar_confirm_appointment` no evento correspondente.\n * \"Não posso\" ou \"Cancelar\", siga o fluxo de cancelamento (Seção 3).\n * Resposta ambígua, esclareça: \"Você confirma presença na consulta de [data] às [hora]?\"\n</fluxo-confirmacao>\n\n## 5. DÚVIDAS\n\n<fluxo-duvidas>\n **REGRA OBRIGATÓRIA:** antes de responder qualquer dúvida administrativa (horários, localização, valores, convênios, especialidades, **preparo de exames**, política de cancelamento, estacionamento, documentos, perguntas frequentes) OU de dizer que não tem uma informação, você **DEVE primeiro chamar `search_knowledge`**. Nunca escale uma dúvida administrativa, e nunca diga \"não tenho essa informação\" ou \"vou confirmar com a equipe\", sem ANTES consultar a base. Baseie a resposta no que ela retornar e não invente regras que não estejam lá.\n\n Se o paciente pedir um documento ou material (orientação de preparo, formulário), use `drive_find_file` para localizar e `drive_send_file` para enviar. Se não houver, ofereça encaminhar a um responsável.\n\n Escale com `handoff_to_human` **apenas para questões clínicas**: sintomas, diagnóstico, interpretação de resultados de exames, indicação ou ajuste de medicamentos. Preparo de exame e orientações administrativas **não** são questões clínicas: você mesma responde, consultando a base.\n</fluxo-duvidas>\n\n# FUNIL (KANBAN)\n\n<kanban>\n Mantenha o card do paciente sempre refletindo o estado real do atendimento. Atualize a cada avanço relevante, não a cada mensagem.\n\n | Etapa | Quando mover |\n |----------------------|---------------------------------------------------------|\n | Novo contato | Card criado no primeiro contato (automático) |\n | Agendamento solicitado | Paciente demonstrou intenção de marcar |\n | Agendado | `calendar_create_event` executado com sucesso |\n | Confirmado | Paciente confirmou presença |\n | Atendido | Consulta realizada |\n | Cancelado / Falta | Cancelamento ou não comparecimento |\n\n ## Regras\n * Use `kanban_move_card` ao mudar de etapa e `update_kanban_task` para enriquecer título e descrição.\n * **Ao atualizar a descrição, preserve sempre o conteúdo anterior**, apenas acrescente o dado novo.\n * Atualize o título com o nome real do paciente assim que souber.\n</kanban>\n\n# FERRAMENTAS\n\n<ferramentas>\n > As ferramentas recebem do sistema, automaticamente, os identificadores técnicos (conta, conversa, contato, card). Você só fornece os dados semânticos (nome, CPF, data, valor, motivo). Instruções de uso específicas de cada ferramenta chegam junto com ela.\n\n ## Agendamento\n * `calendar_check_availability`: consultar horários livres de um profissional.\n * `calendar_create_event`: criar um agendamento em horário disponível.\n * `calendar_list_events`: listar os agendamentos do paciente.\n * `calendar_update_event`: remarcar um agendamento.\n * `calendar_cancel_event`: cancelar um agendamento.\n * `calendar_confirm_appointment`: marcar presença confirmada.\n\n ## Conhecimento\n * `search_knowledge`: consultar a base da clínica (preparo de exames, convênios, valores, política de cancelamento, perguntas frequentes). Use antes de responder dúvidas administrativas e baseie a resposta no que ela retornar.\n\n ## Comunicação e gestão\n * `handoff_to_human`: escalar para a equipe humana (ver regras de escopo).\n * `private_note`: anotação interna, o paciente não vê.\n * `set_custom_attribute`: guardar dados do paciente (CPF, data de nascimento, convênio, preferências).\n * `assign_label`: marcar etapas e situações da conversa.\n * `react_to_message`: reagir com emoji a uma mensagem (use com parcimônia, no máximo 3 por conversa).\n * `resolve_conversation`: encerrar a conversa quando o atendimento estiver concluído.\n * `set_voice_preference`: ajustar resposta em áudio ou texto, quando o paciente pedir.\n * `kanban_move_card` e `update_kanban_task`: manter o funil vivo (ver seção Kanban).\n * `get_current_time`: resolver datas relativas (\"semana que vem\", \"amanhã\").\n * `calculator`: contas pontuais quando necessário.\n\n ## Documentos e pagamento\n * `drive_find_file` e `drive_send_file`: localizar e enviar materiais e orientações.\n * `asaas_create_pix_charge`: gerar a cobrança da consulta particular.\n * `asaas_payment_status`: conferir se o pagamento foi feito.\n * `asaas_payment_link_create`: gerar link de pagamento quando o paciente preferir cartão.\n</ferramentas>\n\n# VALIDAÇÕES E REGRAS DE NEGÓCIO\n\n<validacoes>\n 1. **Agendamento**\n * Apenas dentro do horário de funcionamento.\n * Nunca agende em data passada.\n * Nunca agende em horário que não tenha vindo de `calendar_check_availability`.\n\n 2. **Dados do paciente**\n * Nome completo: pelo menos 2 palavras.\n * Data de nascimento: data válida e no passado.\n * CPF: 11 dígitos (após limpar pontos e traços).\n\n 3. **Limites**\n * Máximo 1 agendamento ativo por paciente.\n * Máximo 3 tentativas de busca de horário antes de escalar.\n\n 4. **Escopo (LGPD e segurança)**\n * NUNCA forneça diagnóstico, interprete exames ou indique medicamentos.\n * NUNCA negocie valores.\n * NUNCA peça senha, número de cartão ou foto de documento. Para confirmar identidade, use só os 4 últimos dígitos do CPF.\n</validacoes>\n\n# EXEMPLOS DE FLUXO\n\n<exemplos>\n **ATENÇÃO**: exemplos ilustrativos. Siga o SOP e personalize. Não copie as mensagens ao pé da letra.\n\n ## Exemplo 1: Agendamento completo\n **Paciente**: Olá, preciso marcar uma consulta\n **Maria**: Olá! Sou a Maria, da Clínica Moreira. Com qual profissional você gostaria de se consultar? Temos Clínico Geral, Cardiologista, Dentista e Odontopediatra.\n **Paciente**: Cardiologista\n **Maria**: Ótimo, é com o *Dr. Roberto Almeida*. Me passa seu nome completo, por favor?\n *[coleta nome, data de nascimento, preferência de data e período, um por mensagem, atualizando o card]*\n **Maria**: *[usa calendar_check_availability]* Encontrei estes horários com o Dr. Roberto esta semana de manhã: *quinta às 09:00* ou *sexta às 10:30*. Qual fica melhor?\n **Paciente**: Quinta às 9\n **Maria**: *[usa calendar_create_event]* Agendamento confirmado: *quinta, 09:00*, com o *Dr. Roberto Almeida*. Para finalizar, preciso do seu CPF para gerar a cobrança.\n **Paciente**: 123.456.789-00\n **Maria**: *[usa asaas_create_pix_charge]* Pronto! A cobrança de *R$ 500,00* foi gerada. Vou te enviar o PIX. Posso ajudar em mais alguma coisa?\n\n ## Exemplo 2: Emergência (escala imediata)\n **Paciente**: Estou com muita dor no peito, o que pode ser?\n **Maria**: *[usa handoff_to_human imediatamente]* Entendo sua preocupação. Vou transferir agora para nossa equipe. Se a dor for intensa, por favor procure um pronto-socorro.\n\n ## Exemplo 3: Cancelamento com reagendamento\n **Paciente**: Preciso cancelar minha consulta\n **Maria**: *[usa calendar_list_events]* Encontrei sua consulta com a *Dra. Ana Silva* amanhã às *14:00*. É essa que deseja cancelar?\n **Paciente**: Sim\n **Maria**: Posso saber o motivo? Isso nos ajuda a melhorar. *[após resposta, usa calendar_cancel_event + private_note]* Cancelado. Quer remarcar para outra data?\n</exemplos>\n\n# CASOS ESPECIAIS\n\n<casos-especiais>\n ## Paciente idoso ou com dificuldade\n * Linguagem mais simples, repita o que for importante, tenha paciência extra.\n\n ## Agendamento para terceiro\n * Se o contato mencionar marcar para outra pessoa, pergunte \"o agendamento é para você mesmo?\" e colete nome e data de nascimento do paciente real.\n\n ## Fora do horário de expediente\n * A clínica atende as mensagens 24 horas. Não mencione horário comercial a menos que o paciente pergunte. Se ele quiser falar com uma pessoa, escale normalmente.\n\n ## Convênios\n * Liste só os convênios aceitos. Para convênio não listado: \"Infelizmente não trabalhamos com esse convênio no momento.\" Não prometa inclusão futura.\n\n ## Recebimento de arquivos\n * Se o paciente enviar um arquivo que você não consegue ler, peça para descrever por texto ou áudio.\n</casos-especiais>\n\n# OBSERVAÇÕES FINAIS\n\n<observacoes-finais>\n ## NUNCA\n 1. NUNCA forneça orientação clínica (sintomas, diagnóstico, interpretação de exames, medicamentos). Preparo de exames e regras administrativas você responde consultando a base.\n 2. NUNCA confirme sucesso de uma ação antes de a ferramenta retornar.\n 3. NUNCA agende em horário não confirmado por `calendar_check_availability`.\n 4. NUNCA exponha problemas técnicos ao paciente.\n 5. NUNCA diga que \"vai verificar\" ou \"vai consultar\": use a ferramenta certa ou escale.\n 6. NUNCA chame o paciente pelo nome do perfil do WhatsApp, só pelo nome que ele informar.\n 7. NUNCA reformule textos que as ferramentas devolvem prontos (instrução de pagamento, orientações): envie como vieram.\n\n ## SEMPRE\n 1. SEMPRE se apresente como Maria na primeira interação.\n 2. SEMPRE confirme os dados antes de agendar.\n 3. SEMPRE colete o CPF antes de `asaas_create_pix_charge` (consulta particular), se ainda não tiver.\n 4. SEMPRE trate emergência e insatisfação grave com escalada imediata.\n 5. SEMPRE consulte a base com `search_knowledge` antes de responder dúvidas sobre preparo de exames, convênios, valores e política de cancelamento.\n 6. SEMPRE termine a mensagem apontando o próximo passo.\n\n ## LEMBRE-SE\n * Você é a Maria, não uma IA. Cada interação impacta a reputação da clínica.\n * Pacientes podem estar ansiosos ou preocupados. Seja empática.\n * Ofereça poucos horários por vez (2 ou 3), nunca a agenda inteira.\n * Sempre localize o agendamento com `calendar_list_events` antes de remarcar ou cancelar.\n</observacoes-finais>\n\n# INFORMAÇÕES DO SISTEMA\n\n<informacoes-sistema>\n **Data e hora atual (aproximada):** {{data_hora_atual}}\n\n Use este valor como referência de \"agora\" para entender datas relativas (\"hoje\", \"amanhã\", \"semana que vem\"). É uma aproximação (atualiza a cada ~30 min); quando precisar do horário exato, use `get_current_time`.\n</informacoes-sistema>\n",
|
|
12
|
+
"modelConfig": {
|
|
13
|
+
"model": "gpt-5.4-mini",
|
|
14
|
+
"provider": "openai",
|
|
15
|
+
"temperature": 0.3,
|
|
16
|
+
"credentialRef": "OpenAI"
|
|
17
|
+
},
|
|
18
|
+
"settings": {
|
|
19
|
+
"stt": {
|
|
20
|
+
"model": "whisper-1",
|
|
21
|
+
"baseURL": null,
|
|
22
|
+
"enabled": true,
|
|
23
|
+
"language": "pt",
|
|
24
|
+
"provider": "openai",
|
|
25
|
+
"credentialRef": "OpenAI"
|
|
26
|
+
},
|
|
27
|
+
"tts": {
|
|
28
|
+
"mode": "mirror",
|
|
29
|
+
"model": "eleven_flash_v2_5",
|
|
30
|
+
"voice": "33B4UnXyTNbgLmdEDh5P",
|
|
31
|
+
"baseURL": null,
|
|
32
|
+
"provider": "elevenlabs",
|
|
33
|
+
"normalize": false,
|
|
34
|
+
"credentialRef": "ElevenLabs"
|
|
35
|
+
},
|
|
36
|
+
"split": {
|
|
37
|
+
"enabled": true,
|
|
38
|
+
"maxChars": 300,
|
|
39
|
+
"maxChunks": 5,
|
|
40
|
+
"typingWpm": 150,
|
|
41
|
+
"maxDelayMs": 8000,
|
|
42
|
+
"minDelayMs": 500
|
|
43
|
+
},
|
|
44
|
+
"limits": {
|
|
45
|
+
"maxToolCalls": 10
|
|
46
|
+
},
|
|
47
|
+
"vision": {
|
|
48
|
+
"model": "",
|
|
49
|
+
"baseURL": null,
|
|
50
|
+
"enabled": true,
|
|
51
|
+
"provider": "gemini",
|
|
52
|
+
"credentialRef": "Google Gemini",
|
|
53
|
+
"extractionPrompt": "Descreva objetivamente o conteúdo deste arquivo, transcrevendo todo o texto visível e os dados relevantes. Responda apenas com o conteúdo, sem comentários nem suposições."
|
|
54
|
+
},
|
|
55
|
+
"handoff": {
|
|
56
|
+
"mode": "route",
|
|
57
|
+
"instructions": "Escale para um humano quando: o paciente pedir um atendente; houver qualquer questão médica (sintomas, diagnóstico, exames, medicamentos); emergência; reclamação séria ou insatisfação; negociação de valores; ou pedido para parar de receber mensagens. Antes de escalar, acolha e avise que um responsável da equipe vai assumir.",
|
|
58
|
+
"targetTeamId": null,
|
|
59
|
+
"targetAgentId": null,
|
|
60
|
+
"targetInstanceId": null
|
|
61
|
+
},
|
|
62
|
+
"debounce": {
|
|
63
|
+
"enabled": true,
|
|
64
|
+
"windowSeconds": 16,
|
|
65
|
+
"maxWindowSeconds": 60,
|
|
66
|
+
"maxMessagesPerBurst": 10
|
|
67
|
+
},
|
|
68
|
+
"followUp": {
|
|
69
|
+
"steps": [
|
|
70
|
+
{
|
|
71
|
+
"delayUnit": "hours",
|
|
72
|
+
"delayValue": 2,
|
|
73
|
+
"instructions": "Toque leve: retome com cuidado e pergunte se ficou alguma dúvida sobre o agendamento ou se quer que eu veja outros horários."
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"delayUnit": "days",
|
|
77
|
+
"delayValue": 1,
|
|
78
|
+
"instructions": "Mais direto: relembre que o agendamento ficou pendente e pergunte se quer que eu reserve um horário."
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
"resolve": true,
|
|
82
|
+
"delayUnit": "days",
|
|
83
|
+
"delayValue": 2,
|
|
84
|
+
"assignLabels": [
|
|
85
|
+
"lead-frio"
|
|
86
|
+
],
|
|
87
|
+
"instructions": "Última tentativa: ofereça ajuda para concluir e avise gentilmente que vai pausar o acompanhamento por ora."
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"enabled": true,
|
|
91
|
+
"pauseWhileAppointment": true
|
|
92
|
+
},
|
|
93
|
+
"serviceWindow": {
|
|
94
|
+
"enabled": true,
|
|
95
|
+
"windowHours": 24,
|
|
96
|
+
"templateName": null,
|
|
97
|
+
"templateParams": [],
|
|
98
|
+
"templateContent": null,
|
|
99
|
+
"templateCategory": "UTILITY",
|
|
100
|
+
"templateLanguage": "pt_BR"
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
"transferWithSummary": true,
|
|
104
|
+
"businessHours": null,
|
|
105
|
+
"followUpHours": "Horário da Clínica Moreira",
|
|
106
|
+
"tools": [
|
|
107
|
+
{
|
|
108
|
+
"source": "NATIVE",
|
|
109
|
+
"enabledTools": [
|
|
110
|
+
"handoff_to_human",
|
|
111
|
+
"private_note",
|
|
112
|
+
"set_custom_attribute",
|
|
113
|
+
"assign_label",
|
|
114
|
+
"resolve_conversation",
|
|
115
|
+
"kanban_move_card",
|
|
116
|
+
"update_kanban_task",
|
|
117
|
+
"set_voice_preference",
|
|
118
|
+
"react_to_message",
|
|
119
|
+
"skip_reply",
|
|
120
|
+
"calculator",
|
|
121
|
+
"get_current_time"
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"source": "RAG",
|
|
126
|
+
"enabledTools": [
|
|
127
|
+
"search_knowledge"
|
|
128
|
+
],
|
|
129
|
+
"knowledgeBases": [
|
|
130
|
+
"Base da Clínica Moreira"
|
|
131
|
+
]
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
"source": "INTEGRATION",
|
|
135
|
+
"catalogType": "GOOGLE_CALENDAR",
|
|
136
|
+
"integration": "Google Calendar",
|
|
137
|
+
"enabledTools": [
|
|
138
|
+
"calendar_list_events",
|
|
139
|
+
"calendar_check_availability",
|
|
140
|
+
"calendar_create_event",
|
|
141
|
+
"calendar_update_event",
|
|
142
|
+
"calendar_cancel_event",
|
|
143
|
+
"calendar_confirm_appointment"
|
|
144
|
+
]
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"source": "INTEGRATION",
|
|
148
|
+
"catalogType": "GOOGLE_DRIVE",
|
|
149
|
+
"integration": "Google Drive",
|
|
150
|
+
"enabledTools": [
|
|
151
|
+
"drive_find_file",
|
|
152
|
+
"drive_send_file"
|
|
153
|
+
]
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"source": "INTEGRATION",
|
|
157
|
+
"catalogType": "ASAAS",
|
|
158
|
+
"integration": "Asaas",
|
|
159
|
+
"enabledTools": [
|
|
160
|
+
"asaas_payment_link_create",
|
|
161
|
+
"asaas_create_pix_charge",
|
|
162
|
+
"asaas_payment_status"
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
],
|
|
166
|
+
"credentials": [
|
|
167
|
+
{
|
|
168
|
+
"name": "OpenAI",
|
|
169
|
+
"kind": "openai"
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
"name": "ElevenLabs",
|
|
173
|
+
"kind": "elevenlabs"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
"name": "Google Gemini",
|
|
177
|
+
"kind": "gemini"
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
"name": "Google OAuth2",
|
|
181
|
+
"kind": "google_oauth"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
"name": "Asaas",
|
|
185
|
+
"kind": "generic"
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
"components": {
|
|
190
|
+
"httpTools": [],
|
|
191
|
+
"mcpServers": [],
|
|
192
|
+
"integrations": [
|
|
193
|
+
{
|
|
194
|
+
"catalogType": "GOOGLE_CALENDAR",
|
|
195
|
+
"name": "Google Calendar",
|
|
196
|
+
"config": {
|
|
197
|
+
"timeZone": "America/Sao_Paulo",
|
|
198
|
+
"calendarIds": [
|
|
199
|
+
"primary"
|
|
200
|
+
],
|
|
201
|
+
"calendarLabels": {
|
|
202
|
+
"primary": "Agenda de consultas"
|
|
203
|
+
},
|
|
204
|
+
"businessHoursId": "2354",
|
|
205
|
+
"slotDurationMinutes": 30,
|
|
206
|
+
"appointmentReminders": {
|
|
207
|
+
"enabled": true,
|
|
208
|
+
"offsetsHours": [
|
|
209
|
+
24,
|
|
210
|
+
1
|
|
211
|
+
],
|
|
212
|
+
"askConfirmationOnLast": true
|
|
213
|
+
},
|
|
214
|
+
"slotGranularityMinutes": 15
|
|
215
|
+
},
|
|
216
|
+
"credentialRef": "Google OAuth2"
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"catalogType": "GOOGLE_DRIVE",
|
|
220
|
+
"name": "Google Drive",
|
|
221
|
+
"config": {
|
|
222
|
+
"folderId": "164Rv2l4SAWATwMq8dlDgFlZBO6c-oN73",
|
|
223
|
+
"folderName": "Arquivos Secretária"
|
|
224
|
+
},
|
|
225
|
+
"credentialRef": "Google OAuth2"
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
"catalogType": "ASAAS",
|
|
229
|
+
"name": "Asaas",
|
|
230
|
+
"config": {},
|
|
231
|
+
"credentialRef": "Asaas"
|
|
232
|
+
}
|
|
233
|
+
],
|
|
234
|
+
"knowledgeBases": [
|
|
235
|
+
{
|
|
236
|
+
"name": "Base da Clínica Moreira",
|
|
237
|
+
"description": "Atendimento: preparo de exames, convênios, política de cancelamento e perguntas frequentes.",
|
|
238
|
+
"embeddingModel": "text-embedding-3-small",
|
|
239
|
+
"chunkSize": 1000,
|
|
240
|
+
"chunkOverlap": 200,
|
|
241
|
+
"documents": [
|
|
242
|
+
{
|
|
243
|
+
"title": "Preparo de exames comuns",
|
|
244
|
+
"sourceType": "text",
|
|
245
|
+
"fileName": null,
|
|
246
|
+
"mimeType": null,
|
|
247
|
+
"content": "# Preparo de exames comuns\n\nEstas orientações valem para os exames mais frequentes. Em caso de dúvida sobre um exame específico, encaminhe o paciente à equipe.\n\n## Exame de sangue (geral / glicemia / colesterol)\n- Jejum de 8 a 12 horas para perfil lipídico e glicemia. Água pode beber normalmente.\n- Manter a medicação de uso contínuo, salvo orientação médica em contrário.\n- Evitar bebida alcoólica nas 72 horas anteriores.\n\n## Eletrocardiograma (ECG)\n- Não precisa de jejum.\n- Evitar cremes ou óleos no tórax no dia do exame.\n- Levar exames cardiológicos anteriores, se tiver.\n\n## Avaliação odontológica\n- Não há preparo específico.\n- Escovar os dentes antes da consulta.\n- Levar exames de imagem recentes (panorâmica), se houver.\n\n## Observação\nA clínica não realiza coleta de exames de imagem (raio-x, ultrassom). Para esses, oriente o paciente a procurar um laboratório parceiro e ofereça encaminhar a um responsável se ele precisar de indicação."
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
"title": "Convênios aceitos e regras de atendimento",
|
|
251
|
+
"sourceType": "text",
|
|
252
|
+
"fileName": null,
|
|
253
|
+
"mimeType": null,
|
|
254
|
+
"content": "# Convênios aceitos\n\nA Clínica Moreira atende pelos seguintes convênios:\n- Bradesco Saúde\n- Unimed\n- SulAmérica\n- Amil\n\n## Regras\n- Para atendimento por convênio NÃO há cobrança: confirme o convênio e oriente o paciente a levar a carteirinha (física ou no app) e um documento com foto no dia.\n- Carência e cobertura dependem do plano do paciente; a clínica não consulta elegibilidade pelo WhatsApp. Em caso de dúvida sobre cobertura, oriente o paciente a confirmar com o próprio convênio.\n- Convênio não listado: \"Infelizmente não trabalhamos com esse convênio no momento.\" Não prometa inclusão futura.\n- Consulta particular: R$ 500,00, com pagamento via PIX, dinheiro ou cartão (débito/crédito). O PIX é gerado pela própria assistente após o agendamento."
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
"title": "Política de cancelamento e remarcação",
|
|
258
|
+
"sourceType": "text",
|
|
259
|
+
"fileName": null,
|
|
260
|
+
"mimeType": null,
|
|
261
|
+
"content": "# Política de cancelamento e remarcação\n\n- Cancelamentos e remarcações podem ser feitos a qualquer momento pelo WhatsApp.\n- Pedimos, sempre que possível, aviso com no mínimo 24 horas de antecedência, para liberar o horário a outro paciente.\n- Não há multa para cancelamento de consulta particular ainda não paga.\n- Consulta particular já paga: o valor fica como crédito para reagendamento dentro de 90 dias.\n- Faltas sem aviso (no-show) são registradas; após 2 faltas seguidas, novos agendamentos podem exigir confirmação prévia.\n- Para remarcar, a assistente localiza o agendamento atual, cancela e abre um novo horário disponível."
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
"title": "Perguntas frequentes (FAQ)",
|
|
265
|
+
"sourceType": "text",
|
|
266
|
+
"fileName": null,
|
|
267
|
+
"mimeType": null,
|
|
268
|
+
"content": "# Perguntas frequentes\n\n## Estacionamento\nA clínica tem estacionamento conveniado no prédio ao lado, com valor promocional para pacientes (informe na recepção). Há também vagas na rua (zona azul).\n\n## O que levar no dia da consulta\n- Documento com foto.\n- Carteirinha do convênio (se for atendimento por convênio).\n- Exames anteriores relacionados à queixa, se tiver.\n- Lista de medicamentos em uso, se houver.\n\n## Primeira consulta\nChegue 15 minutos antes para o cadastro na recepção. A primeira consulta costuma durar mais, pois inclui anamnese completa.\n\n## Atraso\nToleramos até 15 minutos de atraso. Acima disso, o atendimento pode precisar ser remarcado, dependendo da agenda do profissional.\n\n## Acompanhante\nPacientes podem levar um acompanhante. Para crianças e idosos, recomendamos a presença de um responsável.\n\n## Receita e atestado\nReceitas e atestados são emitidos pelo profissional durante a consulta. A clínica não emite ou renova receitas pelo WhatsApp."
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
],
|
|
273
|
+
"businessHours": [
|
|
274
|
+
{
|
|
275
|
+
"name": "Horário da Clínica Moreira",
|
|
276
|
+
"timezone": "America/Sao_Paulo",
|
|
277
|
+
"windows": [
|
|
278
|
+
{
|
|
279
|
+
"day": 1,
|
|
280
|
+
"end": "19:00",
|
|
281
|
+
"start": "08:00"
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
"day": 2,
|
|
285
|
+
"end": "19:00",
|
|
286
|
+
"start": "08:00"
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
"day": 3,
|
|
290
|
+
"end": "19:00",
|
|
291
|
+
"start": "08:00"
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
"day": 4,
|
|
295
|
+
"end": "19:00",
|
|
296
|
+
"start": "08:00"
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
"day": 5,
|
|
300
|
+
"end": "19:00",
|
|
301
|
+
"start": "08:00"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
"day": 6,
|
|
305
|
+
"end": "11:00",
|
|
306
|
+
"start": "08:00"
|
|
307
|
+
}
|
|
308
|
+
],
|
|
309
|
+
"source": "LOCAL"
|
|
310
|
+
}
|
|
311
|
+
]
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Chatwoot admin access-token reader for the fazer.ai agents onboarding. The USER creates the first admin in
|
|
3
|
+
# the Chatwoot onboarding screen (account gate); this reads that admin by email and returns its API access
|
|
4
|
+
# token; it never creates an account or user. Runs a Rails runner INSIDE the Chatwoot container over SSH,
|
|
5
|
+
# base64-piped so the script's own quotes never hit a shell.
|
|
6
|
+
#
|
|
7
|
+
# Output: the admin api_access_token is written to a 0600 file; only metadata is printed. The token is what
|
|
8
|
+
# agents deployment_connect + the Inbox API need. Works on any tier (Coolify, Portainer or plain compose).
|
|
9
|
+
# Python 3 stdlib only (no pip). Network/SSH runs via Bash with dangerouslyDisableSandbox:true (00-prereqs).
|
|
10
|
+
import argparse
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import shlex
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
CONTAINER_RE = re.compile(r"^[A-Za-z0-9_.-]+$")
|
|
21
|
+
|
|
22
|
+
# The email arrives base64-decoded in-container so accents/spaces can't break quoting. RESULT_JSON: marks
|
|
23
|
+
# the line we parse. This READS the admin the user already created (account gate); it never creates one.
|
|
24
|
+
RUBY_PROVISION = r'''
|
|
25
|
+
require 'base64'; require 'json'
|
|
26
|
+
result =
|
|
27
|
+
begin
|
|
28
|
+
email = Base64.strict_decode64("__B64_EMAIL__").force_encoding("UTF-8")
|
|
29
|
+
u = User.find_by(email: email)
|
|
30
|
+
if u.nil?
|
|
31
|
+
{ "error" => "no Chatwoot user with email #{email}: create the admin in the Chatwoot onboarding screen first, then re-run" }
|
|
32
|
+
else
|
|
33
|
+
acc = u.accounts.order(:id).first
|
|
34
|
+
if acc.nil?
|
|
35
|
+
{ "error" => "Chatwoot user #{email} belongs to no account yet (finish the Chatwoot onboarding first)" }
|
|
36
|
+
else
|
|
37
|
+
# The polymorphic AccessToken (owner = the user) is the stable interface across images, whether or
|
|
38
|
+
# not a given image still exposes User#access_token. Idempotent: find_or_create_by! reuses the
|
|
39
|
+
# user's existing token (the model's before-create hook fills a freshly minted one).
|
|
40
|
+
at = AccessToken.find_or_create_by!(owner: u)
|
|
41
|
+
{ "account_id" => acc.id, "user_id" => u.id, "email" => email, "token" => at.token }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
rescue => e
|
|
45
|
+
{ "error" => "#{e.class}: #{e.message}" }
|
|
46
|
+
end
|
|
47
|
+
puts "RESULT_JSON:" + JSON.generate(result)
|
|
48
|
+
'''
|
|
49
|
+
|
|
50
|
+
# Re-runs the fazer.ai "check new versions" job so the hub-side subscription (Kanban/Pro) registers, then
|
|
51
|
+
# reports the subscription config key NAMES present (+ any *status* values); never dumps raw config values,
|
|
52
|
+
# which could hold a secret. jitter_applied:true is mandatory (else the job only reschedules, no sync).
|
|
53
|
+
RUBY_REFRESH = r'''
|
|
54
|
+
require 'json'
|
|
55
|
+
Internal::CheckNewVersionsJob.perform_now(jitter_applied: true)
|
|
56
|
+
names = InstallationConfig.where("name ILIKE '%subscription%' OR name ILIKE 'fazer%'").pluck(:name)
|
|
57
|
+
diag = {}
|
|
58
|
+
%w[FAZER_AI_SUBSCRIPTION_SYNC_ERROR_MESSAGE FAZER_AI_SUBSCRIPTION_VERIFIED_AT].each { |k| diag[k] = InstallationConfig.find_by(name: k)&.value }
|
|
59
|
+
puts "RESULT_JSON:" + JSON.generate({"refreshed" => true, "config_keys" => names, "diagnostics" => diag})
|
|
60
|
+
'''
|
|
61
|
+
|
|
62
|
+
# Lê a identidade da instância que o hub usa pra casar (host = FRONTEND_URL; identifier = UUID de
|
|
63
|
+
# instalação do Chatwoot quando existe). É o input do `agents hub create-instance --identifier <host>`
|
|
64
|
+
# / attach-license, sem precisar do hub MCP no agente. Read-only.
|
|
65
|
+
RUBY_INSTALLATION_ID = r'''
|
|
66
|
+
require 'json'
|
|
67
|
+
ident = (InstallationConfig.find_by(name: 'INSTALLATION_IDENTIFIER')&.value rescue nil)
|
|
68
|
+
host = ENV['FRONTEND_URL']
|
|
69
|
+
host = (InstallationConfig.find_by(name: 'FRONTEND_URL')&.value rescue nil) if host.nil? || host.to_s.strip.empty?
|
|
70
|
+
puts "RESULT_JSON:" + JSON.generate({"installation_identifier" => ident, "frontend_url" => host})
|
|
71
|
+
'''
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def out(obj, code=0):
|
|
75
|
+
print(json.dumps(obj))
|
|
76
|
+
sys.exit(code)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def fail(msg, **extra):
|
|
80
|
+
out({"ok": False, "error": msg, **extra}, code=1)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def b64_pipe(payload, target):
|
|
84
|
+
blob = base64.b64encode(payload.encode("utf-8")).decode("ascii")
|
|
85
|
+
return f"echo '{blob}' | base64 -d | {target}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def split_ssh_opts(opts, _nt=None):
|
|
89
|
+
# POSIX shlex eats backslashes, mangling a Windows key path ("-i C:\Users\me\.ssh\key" ->
|
|
90
|
+
# "C:Usersme.sshkey"). On Windows, tokenize without escape processing and strip our own quotes so the
|
|
91
|
+
# backslashes survive. _nt is injectable for tests.
|
|
92
|
+
nt = (os.name == "nt") if _nt is None else _nt
|
|
93
|
+
if not opts:
|
|
94
|
+
return []
|
|
95
|
+
if nt:
|
|
96
|
+
toks = shlex.split(opts, posix=False)
|
|
97
|
+
return [t[1:-1] if len(t) >= 2 and t[0] == t[-1] and t[0] in "\"'" else t for t in toks]
|
|
98
|
+
return shlex.split(opts)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_ssh(dest, ssh_opts, remote_cmd, timeout):
|
|
102
|
+
argv = ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=15", *split_ssh_opts(ssh_opts), dest, remote_cmd]
|
|
103
|
+
try:
|
|
104
|
+
return subprocess.run(argv, capture_output=True, text=True, timeout=timeout)
|
|
105
|
+
except FileNotFoundError:
|
|
106
|
+
fail("ssh not found on PATH")
|
|
107
|
+
except subprocess.TimeoutExpired:
|
|
108
|
+
fail(f"ssh timed out after {timeout}s", dest=dest)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def cmd_provision(args):
|
|
112
|
+
if not CONTAINER_RE.match(args.container):
|
|
113
|
+
fail(f"invalid --container {args.container!r} (expected [A-Za-z0-9_.-]+)")
|
|
114
|
+
if "@" not in args.email:
|
|
115
|
+
fail("--email must be an email address")
|
|
116
|
+
ruby = RUBY_PROVISION.replace(
|
|
117
|
+
"__B64_EMAIL__", base64.b64encode(args.email.encode("utf-8")).decode("ascii")
|
|
118
|
+
)
|
|
119
|
+
target = f"docker exec -i {args.container} bundle exec rails runner -"
|
|
120
|
+
proc = run_ssh(args.ssh, args.ssh_opts, b64_pipe(ruby, target), args.timeout)
|
|
121
|
+
combined = (proc.stdout or "") + "\n" + (proc.stderr or "")
|
|
122
|
+
match = re.search(r"RESULT_JSON:(\{.*\})", combined)
|
|
123
|
+
if not match:
|
|
124
|
+
fail(
|
|
125
|
+
"Rails runner returned no result (is --container the Chatwoot rails container?)",
|
|
126
|
+
exit_code=proc.returncode,
|
|
127
|
+
stdout=(proc.stdout or "")[-600:],
|
|
128
|
+
stderr=(proc.stderr or "")[-600:],
|
|
129
|
+
)
|
|
130
|
+
try:
|
|
131
|
+
data = json.loads(match.group(1))
|
|
132
|
+
except ValueError:
|
|
133
|
+
fail("could not parse RESULT_JSON", raw=match.group(1)[:200])
|
|
134
|
+
# The runner emits a deliberate {"error": …} when the admin does not exist yet (the user must create
|
|
135
|
+
# it in the Chatwoot UI first). Surface THAT, not the generic "no result": it tells the agent to wait.
|
|
136
|
+
if data.get("error"):
|
|
137
|
+
fail(data["error"])
|
|
138
|
+
dest = Path(args.out)
|
|
139
|
+
dest.write_text(
|
|
140
|
+
json.dumps(
|
|
141
|
+
{
|
|
142
|
+
"account_id": data["account_id"],
|
|
143
|
+
"user_id": data["user_id"],
|
|
144
|
+
"email": data["email"],
|
|
145
|
+
"api_access_token": data["token"],
|
|
146
|
+
},
|
|
147
|
+
ensure_ascii=False,
|
|
148
|
+
indent=2,
|
|
149
|
+
)
|
|
150
|
+
+ "\n",
|
|
151
|
+
encoding="utf-8",
|
|
152
|
+
)
|
|
153
|
+
try:
|
|
154
|
+
dest.chmod(0o600)
|
|
155
|
+
except OSError:
|
|
156
|
+
pass
|
|
157
|
+
out(
|
|
158
|
+
{
|
|
159
|
+
"ok": True,
|
|
160
|
+
"out_file": str(dest),
|
|
161
|
+
"account_id": data["account_id"],
|
|
162
|
+
"user_id": data["user_id"],
|
|
163
|
+
"email": data["email"],
|
|
164
|
+
"note": "api_access_token written to file (chmod 600), not printed",
|
|
165
|
+
}
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def cmd_refresh_subscription(args):
|
|
170
|
+
if not CONTAINER_RE.match(args.container):
|
|
171
|
+
fail(f"invalid --container {args.container!r} (expected [A-Za-z0-9_.-]+)")
|
|
172
|
+
target = f"docker exec -i {args.container} bundle exec rails runner -"
|
|
173
|
+
proc = run_ssh(args.ssh, args.ssh_opts, b64_pipe(RUBY_REFRESH, target), args.timeout)
|
|
174
|
+
combined = (proc.stdout or "") + "\n" + (proc.stderr or "")
|
|
175
|
+
match = re.search(r"RESULT_JSON:(\{.*\})", combined)
|
|
176
|
+
if not match:
|
|
177
|
+
fail(
|
|
178
|
+
"Rails runner returned no result (is --container the Chatwoot rails container?)",
|
|
179
|
+
exit_code=proc.returncode,
|
|
180
|
+
stdout=(proc.stdout or "")[-600:],
|
|
181
|
+
stderr=(proc.stderr or "")[-600:],
|
|
182
|
+
)
|
|
183
|
+
try:
|
|
184
|
+
data = json.loads(match.group(1))
|
|
185
|
+
except ValueError:
|
|
186
|
+
fail("could not parse RESULT_JSON", raw=match.group(1)[:200])
|
|
187
|
+
out({"ok": True, **data})
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def cmd_installation_id(args):
|
|
191
|
+
if not CONTAINER_RE.match(args.container):
|
|
192
|
+
fail(f"invalid --container {args.container!r} (expected [A-Za-z0-9_.-]+)")
|
|
193
|
+
target = f"docker exec -i {args.container} bundle exec rails runner -"
|
|
194
|
+
proc = run_ssh(args.ssh, args.ssh_opts, b64_pipe(RUBY_INSTALLATION_ID, target), args.timeout)
|
|
195
|
+
combined = (proc.stdout or "") + "\n" + (proc.stderr or "")
|
|
196
|
+
match = re.search(r"RESULT_JSON:(\{.*\})", combined)
|
|
197
|
+
if not match:
|
|
198
|
+
fail(
|
|
199
|
+
"Rails runner returned no result (is --container the Chatwoot rails container?)",
|
|
200
|
+
exit_code=proc.returncode,
|
|
201
|
+
stdout=(proc.stdout or "")[-600:],
|
|
202
|
+
stderr=(proc.stderr or "")[-600:],
|
|
203
|
+
)
|
|
204
|
+
try:
|
|
205
|
+
data = json.loads(match.group(1))
|
|
206
|
+
except ValueError:
|
|
207
|
+
fail("could not parse RESULT_JSON", raw=match.group(1)[:200])
|
|
208
|
+
out({"ok": True, **data})
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def build_parser():
|
|
212
|
+
parser = argparse.ArgumentParser(
|
|
213
|
+
prog="chatwoot-admin.py",
|
|
214
|
+
description="Chatwoot admin/account/token + subscription refresh via a Rails runner over SSH.",
|
|
215
|
+
)
|
|
216
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
217
|
+
|
|
218
|
+
ssh = argparse.ArgumentParser(add_help=False)
|
|
219
|
+
ssh.add_argument("--ssh", required=True, metavar="USER@HOST", help="SSH destination, e.g. root@1.2.3.4")
|
|
220
|
+
ssh.add_argument("--container", required=True, help="Chatwoot rails container (from docker ps)")
|
|
221
|
+
ssh.add_argument("--ssh-opts", default="", help="extra ssh options, e.g. '-i ~/.ssh/key -p 2222'")
|
|
222
|
+
ssh.add_argument("--timeout", type=int, default=180)
|
|
223
|
+
|
|
224
|
+
prov = sub.add_parser("provision", parents=[ssh], help="read the admin (by email) the user created + return its API token")
|
|
225
|
+
prov.add_argument("--email", required=True, help="email of the admin the user created in the Chatwoot UI")
|
|
226
|
+
prov.add_argument("--out", required=True, help="file to write the api_access_token to (chmod 600)")
|
|
227
|
+
prov.set_defaults(fn=cmd_provision)
|
|
228
|
+
|
|
229
|
+
refresh = sub.add_parser(
|
|
230
|
+
"refresh-subscription", parents=[ssh], help="run the fazer.ai Refresh job + report subscription config"
|
|
231
|
+
)
|
|
232
|
+
refresh.set_defaults(fn=cmd_refresh_subscription)
|
|
233
|
+
|
|
234
|
+
idcmd = sub.add_parser(
|
|
235
|
+
"installation-id", parents=[ssh], help="read the instance identity the hub matches (host + uuid)"
|
|
236
|
+
)
|
|
237
|
+
idcmd.set_defaults(fn=cmd_installation_id)
|
|
238
|
+
|
|
239
|
+
return parser
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def main():
|
|
243
|
+
args = build_parser().parse_args()
|
|
244
|
+
args.fn(args)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
if __name__ == "__main__":
|
|
248
|
+
main()
|