Merge remote-tracking branch 'origin/main' into tri-2008

This commit is contained in:
melgarafael
2026-09-30 12:46:18 -03:00
8 changed files with 79 additions and 23 deletions
-13
View File
@@ -1,13 +0,0 @@
---
impacto: capacidade_nova
secao: adicionado
titulo: O tempo que o agente espera antes de responder passa a ser ajustável por número
---
A proteção de envio de cada número (em Conexões › Proteção de envio) ganhou quatro campos para o "tempo de pensar" do agente antes da primeira mensagem da resposta: o tempo para ver a notificação, o tempo por caractere digitado, o mínimo e o máximo. Campo vazio mantém exatamente o ritmo de antes (0,9 s + 22 ms por caractere, entre 1,2 s e 7,5 s). Um mínimo acima do máximo é recusado na hora, com a explicação na tela.
Quando a resposta sai em várias mensagens, o intervalo entre elas passa a seguir o "intervalo entre mensagens" configurado para o número, em vez de um valor fixo de 1,2 s a 2 s. Quem nunca mudou esse intervalo não nota diferença; quem mudou vê as mensagens da resposta seguindo o valor escolhido.
Não é preciso fazer nada na instalação: a atualização acrescenta as colunas sozinha.
Contribuição de @webtecnica (#1996), fechando a #653.
@@ -0,0 +1,7 @@
---
impacto: nada_mudou
secao: corrigido
titulo: O agente respeita os funis autorizados ao atualizar a etapa de um negócio
---
Ao confirmar uma etapa durante a conversa, o agente agora passa ao espelho do CRM os funis autorizados em sua versão publicada. Um agente sem acesso ao funil não move mais o cartão, inclusive quando retoma um caso interno; agentes com o funil autorizado continuam podendo atualizar a etapa. O estado da conversa e a resposta ao cliente seguem seu fluxo normal.
@@ -1,8 +0,0 @@
---
impacto: capacidade_nova
secao: adicionado
titulo: Espera antes de responder (janela de rajada) configurável por agente
---
No cadastro do agente, em "Freios de segurança", agora dá para ajustar quanto tempo o agente espera antes de responder mensagens do mesmo contato que chegam em partes (ex.: "Obrigada" e, 14 s depois, "Tem piscina?"). Deixar o campo vazio usa a janela padrão da instalação (8 s), e o valor máximo é 60 segundos — nada de travar o atendimento sem querer. Não muda nada para quem não mexer: o campo é opcional e o comportamento de antes (janela padrão da instalação) segue valendo quando ele está vazio.
Contribuição de @webtecnica (#1997).
+18 -1
View File
@@ -8,6 +8,22 @@ Se você roda o DeskcommCRM numa VPS, **leia a seção da versão para a qual es
## [Não lançado]
## [1.67.0] — 2026-09-30
### Adicionado
- **O tempo que o agente espera antes de responder passa a ser ajustável por número** A proteção de envio de cada número (em Conexões › Proteção de envio) ganhou quatro campos para o "tempo de pensar" do agente antes da primeira mensagem da resposta: o tempo para ver a notificação, o tempo por caractere digitado, o mínimo e o máximo. Campo vazio mantém exatamente o ritmo de antes (0,9 s + 22 ms por caractere, entre 1,2 s e 7,5 s). Um mínimo acima do máximo é recusado na hora, com a explicação na tela.
Quando a resposta sai em várias mensagens, o intervalo entre elas passa a seguir o "intervalo entre mensagens" configurado para o número, em vez de um valor fixo de 1,2 s a 2 s. Quem nunca mudou esse intervalo não nota diferença; quem mudou vê as mensagens da resposta seguindo o valor escolhido.
Não é preciso fazer nada na instalação: a atualização acrescenta as colunas sozinha.
Contribuição de @webtecnica (#1996), fechando a #653.
- **Espera antes de responder (janela de rajada) configurável por agente** No cadastro do agente, em "Freios de segurança", agora dá para ajustar quanto tempo o agente espera antes de responder mensagens do mesmo contato que chegam em partes (ex.: "Obrigada" e, 14 s depois, "Tem piscina?"). Deixar o campo vazio usa a janela padrão da instalação (8 s), e o valor máximo é 60 segundos — nada de travar o atendimento sem querer. Não muda nada para quem não mexer: o campo é opcional e o comportamento de antes (janela padrão da instalação) segue valendo quando ele está vazio.
Contribuição de @webtecnica (#1997).
## [1.66.1] — 2026-09-30
### Corrigido
@@ -9645,7 +9661,8 @@ Primeira versão marcada do DeskcommCRM. O projeto vinha sendo desenvolvido publ
- **Node 22 é obrigatório para desenvolvimento.** A suíte de invariantes instancia o cliente do Supabase, que exige o `WebSocket` global — nativo apenas a partir do Node 22. Isso não afeta quem apenas hospeda: a VPS roda a imagem pronta.
[Não lançado]: https://github.com/melgarafael/DeskcommCRM/compare/v1.66.1...HEAD
[Não lançado]: https://github.com/melgarafael/DeskcommCRM/compare/v1.67.0...HEAD
[1.67.0]: https://github.com/melgarafael/DeskcommCRM/compare/v1.66.1...v1.67.0
[1.66.1]: https://github.com/melgarafael/DeskcommCRM/compare/v1.66.0...v1.66.1
[1.66.0]: https://github.com/melgarafael/DeskcommCRM/compare/v1.65.0...v1.66.0
[1.65.0]: https://github.com/melgarafael/DeskcommCRM/compare/v1.64.1...v1.65.0
+1
View File
@@ -3463,6 +3463,7 @@ async function executarTurnoDoAgente(
tenantId,
leadId,
toStage: update.transition.to,
...(agentConfig !== null ? { pipelineIds: agentConfig.pipelineIds } : {}),
...(update.transition.reason !== undefined
? { reason: update.transition.reason }
: {}),
@@ -24,6 +24,39 @@ describe('mirrorLeadStageToCrm', () => {
});
});
it('respeita os funis da versão publicada — sem funil autorizado o card não se move', async () => {
const sync = vi.fn().mockResolvedValue({
moveu: false, motivo: 'fora_do_escopo', detalhe: 'nenhum funil liberado para este assistente',
});
const r = await mirrorLeadStageToCrm(
db, cfg as never,
{ tenantId: 'org-1', leadId: 'contato-1', toStage: 'qualified', pipelineIds: [] },
{ sync },
);
expect(sync).toHaveBeenCalledWith(cfg.supabase, {
organizationId: 'org-1', contactId: 'contato-1', passo: 'qualified', escopoDeFunis: [],
});
expect(r).toMatchObject({ ok: false, reason: 'fora_do_escopo' });
});
it('leva ao sincronizador somente os funis que o agente publicado pode alterar', async () => {
const sync = vi.fn().mockResolvedValue({ moveu: true, motivo: 'movido' });
const r = await mirrorLeadStageToCrm(
db, cfg as never,
{ tenantId: 'org-1', leadId: 'contato-1', toStage: 'qualified', pipelineIds: ['funil-comercial'] },
{ sync },
);
expect(r).toEqual({ ok: true });
expect(sync).toHaveBeenCalledWith(cfg.supabase, {
organizationId: 'org-1', contactId: 'contato-1', passo: 'qualified',
escopoDeFunis: ['funil-comercial'],
});
});
it('estágio já ocupado é sucesso, não falha', async () => {
const sync = vi.fn().mockResolvedValue({ moveu: false, motivo: 'ja_esta_la' });
const r = await mirrorLeadStageToCrm(
+9 -1
View File
@@ -237,7 +237,14 @@ interface Deps {
export async function mirrorLeadStageToCrm(
_db: Queryable,
cfg: CrmEdgeConfig,
input: { tenantId: string; leadId: string; toStage: LeadStage; reason?: string },
input: {
tenantId: string;
leadId: string;
toStage: LeadStage;
reason?: string;
/** Funis autorizados na versão publicada; ausente só no caminho legado. */
pipelineIds?: readonly string[];
},
deps: Deps = {},
): Promise<MirrorResult> {
const sync = deps.sync ?? sincronizaEstagioDoAgente;
@@ -249,6 +256,7 @@ export async function mirrorLeadStageToCrm(
organizationId: input.tenantId,
contactId: input.leadId,
passo: input.toStage,
...(input.pipelineIds !== undefined ? { escopoDeFunis: input.pipelineIds } : {}),
});
if (r.moveu || r.motivo === 'ja_esta_la') return { ok: true };
@@ -88,3 +88,14 @@ describe("todo montador de ferramentas do turno passa o escopo", () => {
).toEqual([]);
});
});
describe("o espelho de etapa recebe o escopo do agente publicado", () => {
it("o turno não omite os funis ao chamar mirrorLeadStageToCrm", () => {
const fonte = readFileSync(resolve(RAIZ, "lib/agent-engine/agent/inbound-turn.ts"), "utf8");
const chamada = fonte.match(/const mirror = await mirrorLeadStageToCrm\([\s\S]*?\n\s*\}\);/);
expect(chamada, "o turno não chama o espelho do CRM").not.toBeNull();
expect(chamada?.[0], "o espelho ignoraria os funis autorizados na versão publicada").toMatch(
/pipelineIds: agentConfig\.pipelineIds/,
);
});
});