Spaces:
Running
Running
File size: 12,713 Bytes
7154cbf df00a54 7154cbf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | 'use strict';
// Código novo Comenta. Nenhuma chamada de rede, persistência ou execução de entrada.
const norm = s => String(s ?? '').normalize('NFD').replace(/[\u0300-\u036f]/g,'').toLowerCase();
function text(v, label='Texto') { const s=String(v??'').trim(); if(!s)throw Error(label+': preencha o campo.'); if(s.length>30000)throw Error(label+': limite de 30 mil caracteres.');return s; }
function parse(s) {try{return JSON.parse(text(s,'JSON'));}catch(e){throw Error('JSON inválido: use aspas duplas e confira o exemplo.');}}
function list(s) {const a=parse(s);if(!Array.isArray(a)||a.length>300)throw Error('Informe uma lista JSON com até 300 itens.');return a;}
function str(v,label){if(typeof v!=='string'||!v.trim()||v.length>3000)throw Error('Campo '+label+' deve ser texto de 1 a 3000 caracteres.');return v.trim();}
function classify(message) {
const s=norm(text(message));
const defs=[['Financeiro',['boleto','pagamento','cobranca','fatura','reembolso','pix']],['Suporte',['erro','falha','travou','nao funciona','acesso','senha']],['Vendas',['preco','comprar','orcamento','plano','contratar','proposta']]];
const scores=defs.map(([name,words])=>({name,hits:words.filter(w=>s.includes(w))})).sort((a,b)=>b.hits.length-a.hits.length);
const risk=['fraude','vazamento','ameaca','discriminacao','processo','urgente','cancelar'].filter(w=>s.includes(w));
const match=scores[0];const tie=match.hits.length&&scores[1].hits.length===match.hits.length;
return {departamento:match.hits.length&&!tie?match.name:'Triagem humana',prioridade:risk.length?'Alta':'Normal',evidencias:match.hits,alertas:risk,metodo:'Regras por palavras-chave; não usa Claude nem modelo treinado',revisao_humana:!match.hits.length||!!tie||!!risk.length};
}
function summary(transcript, count=4) {
const s=text(transcript);const lines=s.split(/\n+|(?<=[.!?])\s+/).map(x=>x.trim()).filter(Boolean);
const n=Number(count);if(!Number.isInteger(n)||n<1||n>8)throw Error('Escolha de 1 a 8 trechos.');
const freq=new Map();for(const w of norm(s).match(/[a-z]{4,}/g)||[])freq.set(w,(freq.get(w)||0)+1);
const ranked=lines.map((line,i)=>({line,i,score:(norm(line).match(/[a-z]{4,}/g)||[]).reduce((a,w)=>a+(freq.get(w)||0),0)/Math.sqrt(line.length+1)})).sort((a,b)=>b.score-a.score).slice(0,n).sort((a,b)=>a.i-b.i);
return {trechos_literais:ranked.map(x=>x.line),pendencias_literais:lines.filter(x=>/aguard|pendente|precis|retorn|verific|falta/.test(norm(x))).slice(0,5),total_trechos:lines.length,metodo:'Resumo extrativo: seleciona trechos do original; não inventa fatos. Não detecta resolução com garantia.'};
}
function faqLookup(question, faqJSON) {
const q=norm(text(question));const faq=list(faqJSON); const words=new Set((q.match(/[a-z0-9]{3,}/g)||[]).filter(w=>!['qual','como','que','para','uma','por','favor','meu','minha','com','voce','quero','saber'].includes(w)));
let best=null,bestScore=0;
for(const item of faq){str(item.pergunta,'pergunta');str(item.resposta,'resposta');const keys=[...new Set(norm(item.pergunta).match(/[a-z0-9]{3,}/g)||[])];const matches=keys.filter(k=>words.has(k));const score=matches.length/Math.max(1,keys.length);if(score>bestScore){bestScore=score;best={resposta:item.resposta,fonte:item.pergunta,coincidencias:matches};}}
return bestScore>=0.4?{...best,correspondencia:Math.round(bestScore*100)+'% dos termos da pergunta cadastrada'}:null;
}
function reply(question, faq) {
const t=classify(question);const found=faqLookup(question,faq);
return {rascunho:found?'Olá! '+found.resposta+' Posso ajudar em mais alguma dúvida?':'Olá! Recebi sua solicitação. Vou encaminhar para avaliação da equipe. Pode detalhar o que aconteceu, sem incluir senhas ou dados sensíveis?',base:found?.fonte||'Sem resposta na base fornecida',departamento:t.departamento,status:'Rascunho para revisão. Nenhuma mensagem foi enviada.',metodo:'Busca lexical em FAQ fornecida pelo visitante + modelos fixos de resposta'};
}
function handoff(question, faq) {
const t=classify(question);const askHuman=/humano|pessoa|atendente|supervisor/.test(norm(question));const f=faqLookup(question,faq);
const transfer=askHuman||t.prioridade==='Alta'||!f;
return {decisao:transfer?'Encaminhar para pessoa':'Resposta de autoatendimento (prévia)',motivo:askHuman?'Pedido explícito de atendimento humano':t.prioridade==='Alta'?'Sinal de risco ou urgência':!f?'Base fornecida não contém correspondência suficiente':'Pergunta localizada na base fornecida',resposta:transfer?'Uma pessoa da equipe precisa revisar este caso.':f.resposta,pacote_handoff:transfer?{mensagem_original:question,triagem:t}:null,acao_externa:'Nenhuma: não cria ticket, não transfere e não envia mensagem.'};
}
function inbox(input, filter='Todos') {
const items=list(input);const allowed=['WhatsApp','Instagram','Messenger','Chat do site','Telegram','E-mail'];
const groups=new Map();items.forEach((m,i)=>{const contato=str(m.contato,'contato'),canal=str(m.canal,'canal'),mensagem=str(m.mensagem,'mensagem');if(!allowed.includes(canal))throw Error('Canal inválido no item '+(i+1)+'. Use os nomes do exemplo.');if(filter!=='Todos'&&filter!==canal)return;const key=contato;const g=groups.get(key)||{contato,canais:[],mensagens:[]};if(!g.canais.includes(canal))g.canais.push(canal);g.mensagens.push({canal,mensagem,ordem:i+1});groups.set(key,g);});
return {conversas:[...groups.values()],total_mensagens:[...groups.values()].reduce((a,g)=>a+g.mensagens.length,0),criterio:'Agrupamento por identificador contato fornecido; não concilia identidades entre plataformas.'};
}
function moveCards(input,id,status) {
const a=list(input);const states=['Aguardando','Em atendimento','Resolvida'];if(!states.includes(status))throw Error('Status inválido.');let found=false;
const ids=new Set();for(const c of a){str(c.id,'id');str(c.titulo,'titulo');if(ids.has(c.id))throw Error('IDs duplicados.');ids.add(c.id);if(!states.includes(c.status))throw Error('Status inválido no quadro.');if(c.id===id){c.status=status;found=true;}}
if(id&&!found)throw Error('ID não encontrado.');return {cards:a,colunas:Object.fromEntries(states.map(s=>[s,a.filter(c=>c.status===s)])),movimentacao:id?'Status atualizado somente nesta aba.':'Quadro carregado nesta aba.'};
}
function queues(message,input) {
const conf=list(input);const t=classify(message);const q=conf.find(q=>q.departamento===t.departamento);
for(const x of conf){str(x.departamento,'departamento');str(x.equipe,'equipe');str(x.horario,'horario');}
return {triagem:t,fila:q||{departamento:'Triagem humana',equipe:'Responsável não configurado',horario:'Verificar manualmente'},acao:'Roteamento sugerido; não atribui conversas reais. Horário é texto informativo; use o Space Horário para cálculo.'};
}
function hours(dateISO,zone,start,end,days,message) {
const date=new Date(text(dateISO,'Data com fuso'));if(!/([zZ]|[+-]\d\d:\d\d)$/.test(dateISO)||!Number.isFinite(+date))throw Error('Informe data ISO válida com fuso, por exemplo 2026-11-09T10:00:00-04:00.');
const tm=x=>{if(!/^([01]\d|2[0-3]):[0-5]\d$/.test(x))throw Error('Use HH:MM, entre 00:00 e 23:59.');return Number(x.slice(0,2))*60+Number(x.slice(3));};const a=tm(start),b=tm(end);if(a===b)throw Error('Início e fim não podem ser iguais.');
if(!/^[0-6](\s*,\s*[0-6])*$/.test(String(days).trim()))throw Error('Dias: informe ao menos um número de 0 a 6.');const ds=String(days).split(',').map(x=>Number(x.trim()));if(ds.some(x=>!Number.isInteger(x)||x<0||x>6))throw Error('Dias: use números de 0 (domingo) a 6 (sábado), separados por vírgulas.');
let p;try{p=Object.fromEntries(new Intl.DateTimeFormat('en-US',{timeZone:zone,weekday:'short',hour:'2-digit',minute:'2-digit',hourCycle:'h23'}).formatToParts(date).map(x=>[x.type,x.value]));}catch{throw Error('Fuso IANA inválido.');}
const day=['Sun','Mon','Tue','Wed','Thu','Fri','Sat'].indexOf(p.weekday),mins=+p.hour*60+(+p.minute);
const open=a<b?(ds.includes(day)&&mins>=a&&mins<b):((ds.includes(day)&&mins>=a)||(ds.includes((day+6)%7)&&mins<b));
return {aberto:open,horario_local:new Intl.DateTimeFormat('pt-BR',{timeZone:zone,dateStyle:'short',timeStyle:'short'}).format(date),fuso:zone,previa:open?'Dentro do expediente. Encaminhar para a fila.':text(message,'Aviso'),limites:'Considera dias da semana e turnos inclusive noturnos; não considera feriados ou férias. Não envia aviso.'};
}
function shortcuts(message,input) {
const a=parse(input);if(!a||Array.isArray(a)||typeof a!=='object'||Object.keys(a).length>100)throw Error('Atalhos devem ser um objeto JSON com até 100 chaves.');
const pairs=Object.entries(a);for(const [k,v]of pairs){if(!/^\/[a-z0-9_-]{1,30}$/i.test(k))throw Error('Atalho inválido: use /nome.');str(v,'resposta');}
const used=[];const out=text(message).replace(/\/[a-z0-9_-]+/gi,key=>{if(Object.hasOwn(a,key)){used.push(key);return a[key];}return key;});
return {texto_expandido:out,atalhos_usados:used,acao:'Texto local para revisão; nenhum envio.'};
}
function notes(message,tags,note) {
return {visao_interna:{mensagem:text(message),tags:[...new Set(String(tags).split(',').map(x=>x.trim()).filter(Boolean))].slice(0,20),nota_interna:text(note,'Nota')},visao_cliente:{mensagem:text(message)},aviso:'Projeções locais, não controle de acesso. Nesta demonstração o visitante vê ambas. Não registre notas confidenciais reais.'};
}
function team(message,faq) {
const t=classify(message),r=handoff(message,faq);return {etapas:[{agente:'Triagem por regras',saida:t},{agente:'Consulta da base lexical',saida:faqLookup(message,faq)||'Sem correspondência'},{agente:'Supervisor de segurança',saida:r}],agendamento:/agend|reuniao|horario/.test(norm(message))?'Pedido detectado; agenda não conectada. Nenhuma reserva realizada.':'Não solicitado',execucao:'Orquestrador determinístico. Não usa múltiplos LLMs. Nenhuma ação externa.'};
}
function nps(input) {
const a=text(input,'Notas').split(/[,;\s]+/).filter(Boolean).map(Number);if(!a.length||a.length>10000||a.some(n=>!Number.isInteger(n)||n<0||n>10))throw Error('Notas devem ser inteiros de 0 a 10. Limite: 10 mil notas.');
const promoters=a.filter(x=>x>=9).length,detractors=a.filter(x=>x<=6).length;return {respostas:a.length,promotores:promoters,neutros:a.length-promoters-detractors,detratores:detractors,nps:Math.round((promoters-detractors)/a.length*10000)/100,formula:'(% notas 9–10) − (% notas 0–6)',pesquisa_sugerida:'De 0 a 10, quanto você recomendaria nosso atendimento?',acao:'Não dispara pesquisas nem importa avaliações reais.'};
}
function internalChat(input,author,message) {
const a=list(input);if(a.length>=100)throw Error('Limite de 100 mensagens por conversa de avaliação.');for(const m of a){str(m.autor,'autor');str(m.texto,'texto');}
a.push({autor:str(author,'autor'),texto:str(message,'mensagem')});return {conversa_local:a,visao_cliente:[],aviso:'Simulação em uma aba; sem login, criptografia ponta a ponta ou conexão entre colegas. Não use informações internas reais.'};
}
async function webhook(payload,key,signature,attempts) {
parse(payload);const k=text(key,'Chave de teste');if(k.length<8)throw Error('Use chave fictícia com pelo menos 8 caracteres. Não use chave de produção.');const n=Number(attempts);if(!Number.isInteger(n)||n<1||n>8)throw Error('Tentativas: inteiro de 1 a 8.');
const enc=new TextEncoder();const cryptokey=await globalThis.crypto.subtle.importKey('raw',enc.encode(k),{name:'HMAC',hash:'SHA-256'},false,['sign','verify']);
const digest=new Uint8Array(await globalThis.crypto.subtle.sign('HMAC',cryptokey,enc.encode(payload)));const hex=Array.from(digest,b=>b.toString(16).padStart(2,'0')).join('');
let verified=null;if(String(signature).trim()){const s=String(signature).trim().replace(/^sha256=/,'');if(!/^[0-9a-f]{64}$/i.test(s))throw Error('Assinatura esperada: 64 caracteres hexadecimais.');verified=await globalThis.crypto.subtle.verify('HMAC',cryptokey,Uint8Array.from(s.match(/../g),x=>parseInt(x,16)),enc.encode(payload));}
return {algoritmo:'HMAC-SHA256',assinatura:'sha256='+hex,assinatura_informada_valida:verified,bytes_assinados:enc.encode(payload).length,plano_retry_segundos:Array.from({length:n},(_,i)=>Math.min(3600,2**i*5)),requisicao_exemplo:{metodo:'POST',caminho:'/webhooks/comenta',cabecalhos:{'Content-Type':'application/json','X-Comenta-Signature':'sha256='+hex}},aviso:'Calcula assinatura dos bytes exatos do campo. Não fornece API hospedada, não faz requisições, não entrega eventos e não executa retries. Chave fica apenas em memória nesta aba.'};
}
const functions={classify,summary,reply,handoff,inbox,moveCards,queues,hours,shortcuts,notes,team,nps,internalChat,webhook};
if(typeof module!=='undefined')module.exports=functions;
|