AI Connect/How to create/Multiuser-Consumer-Chatbot
Multiuser-Consumer-Chatbot
Erstellen Sie eine einzige Chatbot-Route, die Tausende von Menschen bedient, jeder mit seinem eigenen verbundenen und isolierten GitHub, Slack und Gmail, von einem einzigen Application Key, und verdrahten Sie sie mit OpenAI über das AI Connect SDK. Keine Plattform hat das je ausgeliefert: jeder Nutzer bringt seine eigenen Konten mit, und Sie speichern nie einen Token.
Das ist der Build, den fast jedes Team zuerst versucht, und derjenige, den die Industrie nie günstig bekommen hat: eine einzige Chatbot-Route, auf der Tausende Menschen ihr eigenes GitHub, Slack und Gmail verbinden, alle isoliert, alle über einen einzigen Application Key. Jeder Nutzer Ihres Produktes betritt sie mit dem gesamten Vinkius-Katalog im Rücken: Tausende KI-Verbindungen ab Tag eins, null von Ihnen gebaute Integrationen, null von Ihnen gespeicherte Tokens, null offengelegte Identität. Das ist die Zukunft, die diese Seite Ihnen übergibt, und sie passt in rund achtzig Zeilen Backend.
Die Alternative, die Ihre Wettbewerber noch leben, ist die Standardantwort der Industrie: ein Heer von OAuth-Flows, ein verschlüsseltes Token-Lager mit Schlüsselisolierung pro Nutzer, ein Refresh-Scheduler mit verteilten Locks und ein Sicherheitsfragebogen, an dem Sie vor Enterprise-Verträgen scheitern. Analysen dieses selbstgebauten Wegs beziffern ihn auf 200.000 bis 250.000 US-Dollar über drei Jahre und über 640 Ingenieursstunden, bevor der erste Tool-Call funktioniert. Ihr Assistent legt das Issue im Repository des Nutzers an, fasst seinen ungelesenen Slack zusammen, bucht in seinem Kalender. Das Modell war nie der schwierige Teil. Die Konnektivität war es, und auf dem AI Connect SDK ist sie bereits erledigt.
Hier ist der „Nutzer" wörtlich gemeint: ein Mensch mit einem Konto in Ihrem Produkt. Dessen external_id ist genau das, was Ihr Login bereits liefert.
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
...Drücken Sie oben auf Run, um einen Nutzer-Turn von Anfang bis Ende zu verfolgen: Die Capabilities werden für den angemeldeten Nutzer geladen, in Tools umgewandelt, das Modell wählt github__create_issue, das SDK führt auf der eigenen Verbindung dieses Nutzers aus. Tauschen Sie die User-Id, und jeder Bereich ändert sich, diese Isolation ist das ganze Produkt.
Was Sie am Ende haben
Ein einziger POST /chat-Endpunkt. Bei Vorgabe von userId und einer Nachricht:
- gibt er nur die Capabilities zurück, die dieser Nutzer verbunden hat,
- übergibt er sie OpenAI als tools,
- führt er das Tool aus, das das Modell wählt,
- und erledigt all das auf Ihrem Server, damit keine Credential ihn verlässt.
1. Ein Client für die gesamte App
Sie erstellen genau eine Instanz von Vinkius. Sie hält die Anwendungskonfiguration, nicht einen aktuellen Nutzer. Importieren Sie sie aus einem einzigen Modul.
// 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_… (nur Server)
timeoutMs: 20_000,
});Die Konstruktion stellt keine Anfrage; sie validiert nur die Präfixe der Credentials. Eine einzige Instanz über alle Anfragen hinweg wiederzuverwenden, ist das vorgesehene Muster.
2. Authentifizieren Sie, dann identifizieren Sie den Akteur
Vertrauen Sie niemals einer userId aus dem Request-Body. Lösen Sie sie aus Ihrer Session auf und übergeben Sie sie an das SDK. Das SDK braucht niemals eine E-Mail oder einen Namen, die Id ist opak, und Vinkius erfährt nichts darüber, wer Ihre Kunden sind.
import { vinkius } from './vinkius';
// Ihre Authentifizierung liefert die stabile Id zurück, mit der Sie die Nutzer angelegt haben
function requireUser(req: Request): string {
const userId = req.headers.get('x-user-id');
if (!userId) throw new Error('not authenticated');
return userId; // z. B. "alice_123"
}const user = vinkius.user(requireUser(req)); // verzögert: keine Netzwerkaufrufe3. Verbinden Sie ein Konto, wenn der Nutzer auf „Connect GitHub" klickt
Geben Sie Ihrem Produkt eine schlanke Provisionierungs-Route. connect() ist idempotent (get-or-create), und Credentials werden write-only gespeichert: Die Antwort meldet, welche Felder konfiguriert sind, und gibt niemals Werte zurück.
// POST /connect/github { token }
async function connectGithub(userId: string, githubToken: string) {
const github = vinkius.user(userId).connector('github');
await github.connect(); // stellt die Verbindung bereit
const schema = await github.credentials.schema(); // was dieser Connector benötigt
await github.credentials.set({ GITHUB_TOKEN: githubToken });
return { status: await github.status(), requires: Object.keys(schema) };
}credentials.schema() liest den Katalog und benötigt keine Verbindung; Sie können also die richtigen Formularfelder rendern, bevor der Nutzer sich überhaupt verbindet. Für OAuth-Connectors gibt es nichts zu setzen, connect() kehrt nach der Provider-Zustimmung zurück und der Status wird zu ready.
4. Laden Sie nur die Capabilities dieses Nutzers
Ein Aufruf aggregiert alle bereiten Connectors des Akteurs. Er fächert nebenläufig auf und ist fehlertolerant: Ein instabiler Connector degradiert, er lässt den Turn nicht scheitern.
const capabilities = await user.capabilities({
include: ['github', 'slack', 'gmail'], // beschränkt auf das, was dieses Produkt nutzt
onConnectorError: (slug, error) => {
console.warn('connector skipped', slug, (error as Error).message);
},
});
if (capabilities.length === 0) {
// noch nichts verbunden — fordern Sie den Nutzer auf, ein Konto zu verbinden
}Zwei Nutzer können dieselbe GitHub-Integration verbinden und völlig getrennte Verbindungen, Credentials und Capabilities erhalten. Nichts überquert die Grenze, und Sie haben keine Zeile dieser Isolationslogik geschrieben.
5. Übergeben Sie die Capabilities an das Modell
Hier löst das Versprechen „funktioniert mit jedem Modell" ein. Der /openai-Adapter wandelt einen Capability-Satz in das tools-Array um, das OpenAI erwartet, und dispatcht einen zurückgegebenen Tool-Call zurück auf die nutzerbezogene Verbindung.
// 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 ?? '' };
}
// führt auf der Verbindung DIESES Nutzers aus; Fehler kommen als Daten zurück
const result = await runOpenAIToolCall(capabilities, call);
return { tool: call.function.name, result };
}Für Anthropic, Gemini, Vercel AI SDK, LangChain, LlamaIndex, Cloudflare Workers AI oder eine neutrale JSON-Schema-Brücke für jede andere Laufzeitumgebung siehe Framework-Adapter. Die Umwandlungszeile ist das Einzige, was sich ändert.
6. Lassen Sie den Agenten bis zur Fertigstellung iterieren
Eine echte Unterhaltung ruft mehrere Tools auf. Spielen Sie die Ergebnisse zurück und lassen Sie das Modell vollenden:
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 'Stopped after too many steps.';
}isError: true bei einem Ergebnis ist ein Connector-Ausgang (die Aktion ist fehlgeschlagen), kein Absturz. Den fehlgeschlagenen Inhalt zurück an das Modell zu geben, ist genau das, was es sich erholen lässt, erneut versuchen, ein anderes Tool wählen oder den Nutzer informieren. Heben Sie sich Ihren try/catch für geworfene VinkiusError-Subklassen auf: Authentifizierung, Kontingent und Transport. Siehe Fehlerbehandlung.
7. Erholen Sie sich von „noch nicht verbunden"
Wenn ein Nutzer kein Konto verbunden hat, gibt findCapability nichts zurück oder die Ausführung wirft ConnectorNotConnectedError. Machen Sie daraus einen Produktmoment, keinen 500:
import { ConnectorNotConnectedError } from '@vinkius/connect';
try {
const result = await runOpenAIToolCall(capabilities, call);
} catch (error) {
if (error instanceof ConnectorNotConnectedError) {
return { needsConnection: error.message };
// UI: „Verbinden Sie GitHub, um das zu tun" → Ihre /connect/github-Route
}
throw error;
}Fortgeschrittene Muster, die aus dem Prototypen ein Produkt machen
Die Grundlagen funktionieren. Vier Fähigkeiten, die das SDK bereits mitliefert, trennen einen frühen Prototypen von einem produktionsreifen System.
Machen Sie jeden Schreibvorgang idempotent, begrenzt und abbrechbar
Die Dispatch-Helfer (runOpenAIToolCall) führen das vom Modell gewählte Tool aus, hängen aber keinen Idempotency-Key, kein Timeout pro Aufruf und kein Abort-Signal an. Für alles, was die Welt verändert, lösen Sie die Capability selbst auf und übergeben Sie diese Steuerungen direkt an execute():
const capability = capabilities.findCapability(call.function.name);
const result = await capability?.execute(
JSON.parse(call.function.arguments || '{}'),
{
idempotencyKey: `chat:${messageId}`, // ein wiederholter Turn erstellt niemals ein doppeltes Issue
timeoutMs: 15_000, // ein langsames Tool erhält seine eigene Frist
signal, // der Nutzer hat den Tab geschlossen: abbrechen und keine Kosten mehr verursachen
},
);idempotencyKey zu deklarieren, macht ein nicht idempotentes POST genau dadurch retry-sicher: Es schaltet die Transport-Retries des SDK für diesen Aufruf ein, und der Server dedupliziert Wiederholungen. Ohne sie werden transiente 429/502/503/504 bei einem Schreibvorgang niemals wiederholt.
Behandeln Sie runtime_url wie ein Geheimnis
connect() gibt ein Connection zurück, dessen runtime_url den Data-Plane-Token vk_live_* dieses Nutzers einbettet. Er wird einmal ausgehändigt und authentifiziert jeden Aufruf, protokollieren Sie ihn niemals, persistieren Sie ihn nicht im Browser und bauen Sie ihn niemals in einen Modell-Prompt ein. Für Sichtbarkeit brauchen Sie das nicht: Registrieren Sie hooks, und das SDK bereinigt Authorization, credential-artige Felder und den vk_live_*-Pfad, bevor Ihr Callback überhaupt läuft.
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),
},
});Halten Sie Tool-Namen für Ihr Modell zulässig
toOpenAITools wirft vorab einen ConfigError, wenn ein namespaced Name wie github__create_issue die 64-Zeichen-Regel [A-Za-z0-9_-] von OpenAI verletzt, statt eines kryptischen 400 des Providers mitten im Gespräch. Verkleinern Sie den Namespace bei der Konstruktion, wenn Ihre Connectors wortreich sind:
new Vinkius({
appId,
apiKey,
namespaceCapability: (connector, name) =>
`${connector}_${name}`.replace(/[^a-zA-Z0-9_]/g, '_').slice(0, 64),
});Lesen Sie die getypte Ausgabe, wenn sie vorhanden ist
Manche Capabilities liefern neben dem Text strukturierte Daten zurück. result.structuredContent gibt sie unverändert wieder (das SDK parst sie niemals), sodass ein Tool „fassen Sie meinen ungelesenen Slack zusammen" Ihrer UI ein sauberes Objekt übergeben kann statt eines Strings, den Sie erneut parsen müssen:
const result = await capability!.execute(args);
const data = result.structuredContent; // getyptes Objekt, wenn der Connector es liefertProduktions-Checkliste
- [ ] Das SDK läuft ausschließlich auf Ihrem Server; Ihr Browser/Mobil greift auf Ihre Routen zu, niemals auf Vinkius.
- [ ]
external_idstammt aus Ihrer authentifizierten Session, niemals aus Client-Eingaben. - [ ] Leiten Sie stabile Ids ab (ein DB-Schlüssel) und halten Sie sie unter 255 Zeichen ohne
/,\oder Leerzeichen. - [ ] Übergeben Sie
includeancapabilities(), damit das Modell nur die Tools sieht, die dieses Produkt nutzen soll. - [ ] Geben Sie jedem verändernden Aufruf einen stabilen
idempotencyKey, damit Retries keine doppelten Ausführungen auslösen. - [ ] Hängen Sie nicht-vertrauliche Metadaten mit
vinkius.user(id).ensure({ plan })an, wenn Sie nach Plan segmentieren.
Sie haben nun eine einzige Chatbot-Route, die jeden Nutzer bedient, jeder mit seinen eigenen isolierten Connectors und Capabilities, verdrahtet mit dem Modell Ihrer Wahl. Die sechsstellige Integrationsschicht, die Ihre Wettbewerber noch von Hand bauen, haben Sie gegen einen Schlüssel, ein SDK und einen Nachmittag getauscht. Sie konkurrieren mit Ihrem Produkt. Das Plumbing ist erledigt.
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.
