feat(engine): adiciona 3ª engine Baileys 7 oficial (baileys7) para teste

Instala baileys@7.0.0-rc13 sob alias 'baileys-official' (coexiste com a
'infinite' = baileys 7.0.0-rc.9 e a 'official' = @whiskeysockets/baileys 6.x).
Nova engine 'baileys7' (LID/username, app-state moderno) selecionável por
instância; validação do PATCH /engine e rich-message (botões nativos v7)
atualizados; opção adicionada ao seletor da ThinSidebar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
VPS 4 Deploy Agent
2026-06-28 04:42:42 +02:00
parent e980097fbe
commit 5170a31aac
7 changed files with 472 additions and 456 deletions
@@ -1,15 +1,47 @@
import React from 'react';
import React, { useState, useCallback } from 'react';
import {
MessageCircle,
Megaphone,
Users,
Settings,
ShieldCheck,
LayoutGrid,
Sparkles,
X,
Loader2,
Check,
} from 'lucide-react';
import { getAvatarProxyUrl } from '../utils/inboxUtils';
import { useRouter } from 'next/router';
export type TabType = 'chats' | 'broadcasts' | 'groups' | 'settings';
type Engine = 'official' | 'infinite' | 'baileys7';
// Seletor de API (engine de envio). Mapeamento dos rótulos para o usuário final:
// official → Baileys oficial → "API estável" (sem botões)
// infinite → InfiniteAPI → "API instável" (com botões)
const ENGINES: {
value: Engine; titulo: string; desc: string; hint: string;
icon: any; iconCls: string; activeCls: string; checkCls: string;
}[] = [
{
value: 'official', titulo: 'API estável', desc: 'sem botões', hint: 'Recomendada',
icon: ShieldCheck, iconCls: 'text-emerald-500',
activeCls: 'border-emerald-400 bg-emerald-50', checkCls: 'text-emerald-500',
},
{
value: 'infinite', titulo: 'API instável', desc: 'com botões', hint: 'Experimental',
icon: LayoutGrid, iconCls: 'text-amber-500',
activeCls: 'border-amber-400 bg-amber-50', checkCls: 'text-amber-500',
},
{
value: 'baileys7', titulo: 'Baileys v7 oficial', desc: 'LID/username', hint: 'Teste (rc13)',
icon: Sparkles, iconCls: 'text-indigo-500',
activeCls: 'border-indigo-400 bg-indigo-50', checkCls: 'text-indigo-500',
},
];
interface ThinSidebarProps {
activeTab: TabType;
setActiveTab: (tab: TabType) => void;
@@ -19,6 +51,64 @@ interface ThinSidebarProps {
export default function ThinSidebar({ activeTab, setActiveTab, activeInstance }: ThinSidebarProps) {
const router = useRouter();
const isConnected = activeInstance?.status === 'connected' || activeInstance?.status === 'open';
const instanceId: string | null = activeInstance?.instance ?? null;
// ── Seletor de API (engine) ───────────────────────────────────────────────
const [showEngine, setShowEngine] = useState(false);
const [engine, setEngine] = useState<Engine | null>(null);
const [loading, setLoading] = useState(false);
const [saving, setSaving] = useState<Engine | null>(null);
const [error, setError] = useState<string | null>(null);
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8008';
const authHeader = (): Record<string, string> => {
const token = typeof window !== 'undefined' ? localStorage.getItem('token') : null;
return token ? { Authorization: `Bearer ${token}` } : {};
};
const toggleSelector = useCallback(async () => {
const opening = !showEngine;
setShowEngine(opening);
setError(null);
if (!opening || !instanceId) return;
// Ao abrir, descobre a engine atual para destacá-la.
setLoading(true);
try {
const res = await fetch(`${API_URL}/api/instances`, { headers: authHeader() });
if (res.ok) {
const list = await res.json();
const inst = Array.isArray(list) ? list.find((i: any) => i.id === instanceId) : null;
setEngine((inst?.engine as Engine) ?? null);
}
} catch {
/* ignora — o seletor ainda funciona, só não destaca a atual */
} finally {
setLoading(false);
}
}, [showEngine, instanceId, API_URL]);
const selectEngine = useCallback(async (value: Engine) => {
if (!instanceId || saving || value === engine) return;
setSaving(value);
setError(null);
try {
const res = await fetch(`${API_URL}/api/instances/${instanceId}/engine`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json', ...authHeader() },
body: JSON.stringify({ engine: value }),
});
if (res.status === 429) {
const d = await res.json().catch(() => ({}));
throw new Error(d.error ?? 'Aguarde o intervalo entre trocas.');
}
if (!res.ok) throw new Error('Falha ao trocar a API.');
setEngine(value);
} catch (e: any) {
setError(e?.message ?? 'Erro ao trocar a API.');
} finally {
setSaving(null);
}
}, [instanceId, saving, engine, API_URL]);
const renderIcon = (id: TabType, IconComponent: any, title: string) => {
const isActive = activeTab === id;
@@ -46,7 +136,7 @@ export default function ThinSidebar({ activeTab, setActiveTab, activeInstance }:
};
return (
<div className="w-[60px] h-full bg-[#f0f2f5] border-r border-[#d1d7db] flex flex-col items-center py-4 flex-shrink-0 z-30">
<div className="relative w-[60px] h-full bg-[#f0f2f5] border-r border-[#d1d7db] flex flex-col items-center py-4 flex-shrink-0 z-30">
{/* Top Icons */}
<div className="flex-1 w-full flex flex-col items-center">
{renderIcon('chats', MessageCircle, 'Conversas')}
@@ -56,8 +146,17 @@ export default function ThinSidebar({ activeTab, setActiveTab, activeInstance }:
{/* Bottom Icons */}
<div className="w-full flex flex-col items-center pb-2">
{renderIcon('settings', Settings, 'Configurações')}
{/* Configurações — abre o seletor de API (engine de envio) */}
<button
title="Configurações — API de envio"
onClick={toggleSelector}
className={`w-10 h-10 mb-2 rounded-full flex items-center justify-center transition-colors ${
showEngine ? 'bg-[#d9dbdf]' : 'hover:bg-[#d9dbdf]/60'
}`}
>
<Settings className={`w-6 h-6 ${showEngine ? 'text-[#111b21]' : 'text-[#54656f]'}`} />
</button>
<div className="relative mt-2 p-1 cursor-pointer">
<div className="w-8 h-8 rounded-full overflow-hidden bg-gray-200 border border-gray-300">
{(() => {
@@ -93,6 +192,68 @@ export default function ThinSidebar({ activeTab, setActiveTab, activeInstance }:
></div>
</div>
</div>
{/* ── Popover: seletor de API (engine) ─────────────────────────────── */}
{showEngine && (
<>
{/* backdrop para fechar ao clicar fora */}
<div className="fixed inset-0 z-40" onClick={() => setShowEngine(false)} />
<div className="absolute left-[64px] bottom-3 z-50 w-64 rounded-xl bg-white shadow-xl border border-[#e9edef] p-3">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-semibold text-[#111b21]">API de envio</span>
<button
onClick={() => setShowEngine(false)}
className="text-[#54656f] hover:text-[#111b21]"
title="Fechar"
>
<X size={16} />
</button>
</div>
{!instanceId ? (
<p className="text-xs text-[#667781] py-2">Selecione uma instância primeiro.</p>
) : (
<div className="flex flex-col gap-2">
{ENGINES.map((opt) => {
const Icon = opt.icon;
const ativa = engine === opt.value;
const salvando = saving === opt.value;
return (
<button
key={opt.value}
disabled={!!saving}
onClick={() => selectEngine(opt.value)}
className={`flex items-center gap-3 rounded-lg border p-2.5 text-left transition-colors disabled:opacity-60 ${
ativa ? opt.activeCls : 'border-[#e9edef] hover:bg-[#f5f6f6]'
}`}
>
<Icon className={`w-5 h-5 shrink-0 ${opt.iconCls}`} />
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-[#111b21]">
{opt.titulo}{' '}
<span className="text-[#667781] font-normal">· {opt.desc}</span>
</div>
<div className="text-[11px] text-[#8696a0]">{opt.hint}</div>
</div>
{salvando ? (
<Loader2 className="w-4 h-4 animate-spin text-[#54656f]" />
) : ativa ? (
<Check className={`w-4 h-4 ${opt.checkCls}`} />
) : null}
</button>
);
})}
</div>
)}
{loading && <p className="text-[11px] text-[#8696a0] mt-2">Carregando configuração atual</p>}
{error && <p className="text-[11px] text-red-500 mt-2">{error}</p>}
<p className="text-[10px] text-[#8696a0] mt-2 leading-snug">
A troca reinicia a conexão da instância. um intervalo mínimo entre trocas.
</p>
</div>
</>
)}
</div>
);
}