AI Connect/Como criar/Chatbot de consumo multiusuário
Chatbot de consumo multiusuário
Construa uma única rota de chatbot que atende milhares de pessoas, cada uma com o próprio GitHub, Slack e Gmail conectados e isolados, a partir de uma única Application Key, e ligue ao OpenAI com o AI Connect SDK. Nenhuma plataforma jamais entregou isso: cada usuário traz as próprias contas, e você nunca armazena um token.
Este é o projeto que quase todo time tenta primeiro, e o que a indústria nunca conseguiu tornar barato: uma única rota de chatbot em que milhares de pessoas conectam o próprio GitHub, Slack e Gmail, todas isoladas, todas a partir de uma única Application Key. Cada usuário do seu produto entra com o catálogo inteiro da Vinkius atrás dele: milhares de conexões de IA desde o primeiro dia, zero integrações construídas por você, zero tokens armazenados por você, zero identidade exposta a alguém. Esse é o futuro que esta página entrega, e ele cabe em cerca de oitenta linhas de backend.
A alternativa, aquela em que os seus concorrentes ainda vivem, é a resposta padrão da indústria: um exército de fluxos de OAuth, um cofre de tokens criptografado com isolamento de chave por usuário, um agendador de refresh com locks distribuídos e um questionário de segurança em que você falha na frente de contratos enterprise. Análises desse caminho caseiro estimam entre US$ 200 mil e US$ 250 mil ao longo de três anos, e mais de 640 horas de engenharia antes de a primeira chamada de tool funcionar. O seu assistente cria a issue no repositório do usuário, resume o Slack não lido dele, agenda na agenda dele. O modelo nunca foi a parte difícil. A conectividade era, e no AI Connect SDK ela já está pronta.
Aqui o "usuário" é tomado no sentido literal: uma pessoa com uma conta no seu produto. O external_id dela é o que o seu login já fornece.
vinkius.user('alice_123').capabilities({ include: ['github'] })GET /apps/vk_app_xxx/users/alice_123/tools?connector=githubconst capabilities = await vinkius
.user('alice_123')
.capabilities({ include: ['github'] });CapabilitySet (6)
github__list_issues read-only
github__create_issue POST /repos/{owner}/{repo}/issues
github__list_pull_requests read-only
github__search_code read-only
...Clique em Run acima para ver um turno do usuário de ponta a ponta: as capacidades carregam para o usuário logado, viram tools, o modelo escolhe github__create_issue, o SDK executa na conexão própria daquele usuário. Troque o id de usuário e cada painel muda, esse isolamento é o produto inteiro.
O que você termina tendo
Um único endpoint POST /chat. Dado userId e uma mensagem, ele:
- retorna somente as capacidades que aquele usuário conectou,
- entrega-as ao OpenAI como tools,
- executa a tool que o modelo escolher,
- e faz tudo no seu servidor, para que nenhuma credencial saia de lá.
1. Um cliente, para o app inteiro
Você cria exatamente uma instância do Vinkius. Ela guarda a configuração da aplicação, não um usuário atual. Importe-a de um único módulo.
// server/vinkius.ts
import { Vinkius } from '@vinkius/connect';
export const vinkius = new Vinkius({
appId: process.env.VINKIUS_APP_ID!, // vk_app_…
apiKey: process.env.VINKIUS_APP_KEY!, // vk_app_sk_… (só no servidor)
timeoutMs: 20_000,
});A construção não faz requisição alguma; apenas valida os prefixos das credenciais. Reaproveitar uma única instância em todas as requisições é o padrão desejado.
2. Autentique e então identifique o ator
Nunca confie em um userId vindo do corpo da requisição. Resolva-o a partir da sua sessão e passe-o ao SDK. O SDK jamais precisa de e-mail ou nome, o id é opaco e o Vinkius não descobre nada sobre quem são seus clientes.
import { vinkius } from './vinkius';
// sua autenticação devolve o id estável com que você criou os usuários
function requireUser(req: Request): string {
const userId = req.headers.get('x-user-id');
if (!userId) throw new Error('não autenticado');
return userId; // ex.: "alice_123"
}const user = vinkius.user(requireUser(req)); // lazy: zero chamadas de rede3. Conecte uma conta quando o usuário clicar em "Conectar GitHub"
Dê ao produto uma rota enxuta de provisionamento. O connect() é idempotente (get-or-create), e as credenciais ficam write-only: a resposta informa quais campos estão configurados e nunca retorna valores.
// POST /connect/github { token }
async function connectGithub(userId: string, githubToken: string) {
const github = vinkius.user(userId).connector('github');
await github.connect(); // provisiona a conexão
const schema = await github.credentials.schema(); // o que este conector exige
await github.credentials.set({ GITHUB_TOKEN: githubToken });
return { status: await github.status(), requires: Object.keys(schema) };
}credentials.schema() lê o catálogo e não exige uma conexão, então você pode renderizar os campos certos do formulário antes de o usuário conectar. Para conectores OAuth não há nada a definir, o connect() retorna após o consentimento no provedor e o status passa a ready.
4. Carregue só as capacidades deste usuário
Uma chamada agrega todos os conectores prontos do ator. Ela se distribui de forma concorrente e é tolerante a falhas: um conector instável degrada, não faz o turno falhar.
const capabilities = await user.capabilities({
include: ['github', 'slack', 'gmail'], // limita ao que o produto usa
onConnectorError: (slug, error) => {
console.warn('conector ignorado', slug, (error as Error).message);
},
});
if (capabilities.length === 0) {
// nada conectado ainda — incentive o usuário a conectar uma conta
}Dois usuários podem conectar a mesma integração do GitHub e obter conexões, credenciais e capacidades totalmente separadas. Nada cruza a fronteira, e você não implementou nenhuma parte dessa lógica de isolamento.
5. Entregue as capacidades ao modelo
É aqui que a promessa de "funciona com qualquer modelo" se concretiza. O adapter /openai converte o conjunto de capacidades no array tools que o OpenAI espera e faz o dispatch da tool retornada de volta para a conexão com escopo do usuário.
// server/chat.ts
import OpenAI from 'openai';
import { vinkius } from './vinkius';
import { toOpenAITools, runOpenAIToolCall } from '@vinkius/connect/openai';
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY! });
export async function handleChat(userId: string, message: string) {
const capabilities = await vinkius.user(userId).capabilities({
include: ['github', 'slack', 'gmail'],
});
const completion = await openai.chat.completions.create({
model: '[MODEL_ID]',
messages: [{ role: 'user', content: message }],
tools: toOpenAITools(capabilities),
tool_choice: 'auto',
});
const call = completion.choices[0]?.message.tool_calls?.[0];
if (!call) {
return { text: completion.choices[0]?.message.content ?? '' };
}
// executa na conexão DESTE usuário; erros voltam como dados
const result = await runOpenAIToolCall(capabilities, call);
return { tool: call.function.name, result };
}Para Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Cloudflare Workers AI ou uma ponte neutra em JSON Schema para qualquer outro runtime, veja Framework adapters. A linha de conversão é a única coisa que muda.
6. Faça o agente iterar até terminar
Uma conversa real chama várias tools. Devolva os resultados e deixe o modelo concluir:
export async function runTurn(userId: string, messages: object[]) {
const capabilities = await vinkius.user(userId).capabilities();
const tools = toOpenAITools(capabilities);
for (let step = 0; step < 6; step++) {
const completion = await openai.chat.completions.create({
model: '[MODEL_ID]',
messages: messages as never,
tools,
});
const msg = completion.choices[0].message;
messages.push(msg as object);
if (!msg.tool_calls?.length) return msg.content;
for (const call of msg.tool_calls) {
const result = await runOpenAIToolCall(capabilities, call);
messages.push({
role: 'tool',
tool_call_id: call.id,
content: JSON.stringify(result.content),
});
}
}
return 'Parei após passos demais.';
}isError: true em um resultado é um desfecho do conector (a ação falhou), não uma falha do sistema. Devolver o conteúdo do erro ao modelo é justamente o que permite a ele se recuperar, repetir, escolher outra tool ou avisar o usuário. Reserve o try/catch para subclasses de VinkiusError lançadas: autenticação, cota e transporte. Veja Tratamento de erros.
7. Recupere de "ainda não conectado"
Quando o usuário não conectou uma conta, findCapability não retorna nada ou a execução lança ConnectorNotConnectedError. Transforme isso em um momento de produto, não em um 500:
import { ConnectorNotConnectedError } from '@vinkius/connect';
try {
const result = await runOpenAIToolCall(capabilities, call);
} catch (error) {
if (error instanceof ConnectorNotConnectedError) {
return { needsConnection: error.message };
// UI: "Conecte o GitHub para isso" → sua rota /connect/github
}
throw error;
}Recursos avançados que transformam o protótipo em produto
O básico funciona. Quatro recursos que o SDK já entrega separam um protótipo inicial de um sistema pronto para produção.
Torne toda escrita idempotente, limitada e cancelável
Os helpers de despacho (runOpenAIToolCall) executam a tool que o modelo escolheu, mas não anexam chave de idempotência, timeout por chamada nem sinal de abort. Para qualquer coisa que mude o mundo, resolva a capacidade você mesmo e passe esses controles direto ao execute():
const capability = capabilities.findCapability(call.function.name);
const result = await capability?.execute(
JSON.parse(call.function.arguments || '{}'),
{
idempotencyKey: `chat:${messageId}`, // um turno repetido nunca cria uma issue duplicada
timeoutMs: 15_000, // uma tool lenta obtém seu próprio prazo
signal, // o usuário fechou a aba: cancele a execução e evite custos adicionais
},
);Declarar idempotencyKey é exatamente o que torna um POST não-idempotente seguro para repetir: ela liga os retries de transporte do SDK para aquela chamada e o servidor deduplica os replays. Sem ela, 429/502/503/504 jamais são repetidos numa escrita.
Trate runtime_url como segredo
O connect() devolve um Connection cujo runtime_url embute o token de dados vk_live_* daquele usuário. Ele é entregue uma única vez e autentica toda chamada, nunca o logue, nunca o persista no navegador, nunca o coloque num prompt. Para ter visibilidade você não precisa disso: registre hooks, e o SDK limpa Authorization, campos que parecem credenciais e o path vk_live_* antes de o seu callback ser executado.
const vinkius = new Vinkius({
appId: process.env.VINKIUS_APP_ID!,
apiKey: process.env.VINKIUS_APP_KEY!,
hooks: {
onRequest: ({ method, attempt }) => metrics.count(method, attempt),
onResponse: ({ status, requestId }) => trace.record(status, requestId),
},
});Mantenha nomes de tool válidos para o modelo
O toOpenAITools lança ConfigError de antemão quando um nome com namespace como github__create_issue viola a regra de 64 caracteres [A-Za-z0-9_-] do OpenAI, em vez de um 400 críptico do provedor no meio da conversa. Encolha o namespace na construção se seus conectores forem verbosos:
new Vinkius({
appId,
apiKey,
namespaceCapability: (connector, name) =>
`${connector}_${name}`.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 64),
});Leia a saída estruturada quando ela existir
Algumas capacidades devolvem dados estruturados junto do texto. O result.structuredContent apresenta esse conteúdo sem qualquer tratamento (o SDK não o interpreta), então uma ferramenta "resuma meu Slack não lido" pode entregar à sua interface um objeto estruturado em vez de uma string que você precisa voltar a interpretar.
Checklist de produção
- [ ] O SDK só roda no seu servidor; navegador/mobile falam com suas rotas, nunca com o Vinkius.
- [ ] O
external_idvem da sua sessão autenticada, nunca de entrada do cliente. - [ ] Derive ids estáveis (uma chave do banco) e mantenha-os com menos de 255 caracteres, sem
/,\nem espaços. - [ ] Passe
includeemcapabilities()para que o modelo só veja as tools que o produto deve usar. - [ ] Dê a toda chamada mutável um
idempotencyKeyestável, para que as repetições não gerem execuções duplicadas. - [ ] Anexe metadados não secretos com
vinkius.user(id).ensure({ plan })se você segmenta por plano.
Você agora tem uma única rota de chatbot atendendo todo usuário, cada um com conectores e capacidades isolados, ligada ao modelo que escolher. A camada de integração de seis dígitos que os seus concorrentes ainda constroem à mão, você trocou por uma chave, um SDK e uma tarde. Você compete no seu produto. A infraestrutura está pronta.
What you just got
Not a pitch: the properties this build inherits automatically.
Connections and capabilities resolve only inside one external_id. No cross-actor leakage is possible, and you wrote none of that enforcement.
Your server stores secrets and can read back which fields are configured, never the values. Not your code, the model, or a dashboard can exfiltrate them.
Every connection owns a vk_live_* token, so cost and revocation are per connection. One call to disconnect() is a complete, auditable stop.
One CapabilitySet converts to OpenAI, Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Workers AI or neutral JSON Schema. Only the last line changes.
idempotencyKey, timeoutMs and AbortSignal per call; automatic full-jitter retries on transient failures; typed VinkiusError branches. No bespoke harness.
HandleChat(userId, ...) serves your whole base. Adding a user is one external_id, never a new integration.
Give it to your AI agent
An Agent Skill (SKILL.md) for this build. Preview the first lines below, then copy or download it into your repo under .claude/skills/: Claude Code, Cursor or any Agent-Skills-compatible agent follows it to implement this pattern correctly.
