Zum Inhalt springen

Client

Für Entwickler. Wähle im Entwicklerhandbuch zwischen API-Anbindung, App-Veröffentlichung und Projektarbeit.

@openbot-build/client ist der TypeScript-Client für OpenBot. Er entsteht aus demselben API-Vertrag wie der Server. Jede Anfrage, jede Antwort und jeder Fehler hat einen Typ. Dein Editor zeigt Fehler, bevor du den Code ausführst.

Der Client nutzt Effect, eine TypeScript-Bibliothek für typisierte Programme. Er läuft in Node.js, Deno, Bun, Workern und Browsern.


Die Pakete liegen in GitHub Packages unter https://npm.pkg.github.com. Trage den Scope in die .npmrc des Projekts ein:

@openbot-build:registry=https://npm.pkg.github.com

Nutze einen GitHub-Token (classic PAT) mit read:packages. Mit npm login --scope=@openbot-build --registry=https://npm.pkg.github.com --auth-type=legacy speicherst du ihn in deinen npm-Einstellungen. Lege den Token nicht in Git ab.

Installiere eine genaue Version. Mit sha-<vollständiger-Commit-Hash> wählst du einen veröffentlichten Commit. Die Lockdatei hält die genaue Version fest:

Terminal window
pnpm add --save-exact @openbot-build/client@0.1.2 effect@4.0.7

Der Client enthält gemeinsamen OpenBot-Code und Typen unter MIT. Du brauchst keine Kopie des OpenBot-Quellcodes. Andere OpenBot-Pakete behalten ihre Lizenzen.

layer erstellt einen Layer, der dir den Dienst Client gibt. Ein Layer ist ein Rezept in Effect, das einen Dienst baut.

import { Chats, layer } from "@openbot-build/client"
import { NodeCrypto } from "@effect/platform-node"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const command = yield* Chats.botMessage(botId, {
input: "Write the release notes.",
})
const receipt = yield* command.send
return receipt.chat
})
const id = await Effect.runPromise(
program.pipe(
Effect.provide(NodeCrypto.layer),
Effect.provide(
layer({
baseUrl: "https://api.example.com",
token: process.env.OPENBOT_TOKEN,
}),
),
),
)

Bereiten Sie einen Befehl einmal vor. command.commandKey bleibt bei einem neuen Versuch mit command.send gleich. Chats.message sendet so an einen bestehenden Chat. Der Beleg nennt die Ausführung. Beobachten Sie sie über client.api.executions.observe mit der letzten afterSequence. Das Schließen der Beobachtung bricht die Arbeit nicht ab. Executions.cancel und Executions.decide bereiten auch Befehle mit festen Schlüsseln vor. Geben Sie diesen Helfern einen Crypto-Dienst der Plattform. Das Node.js-Beispiel nutzt NodeCrypto.layer.

Option Pflicht Was sie tut
baseUrl Ja Der Origin von OpenBot, zum Beispiel https://api.example.com.
token Nein Ein User Token, als String oder als Redacted-Wert von Effect. Er wird als Authorization: Bearer <token> gesendet.
organization Nein Eine Organisations-ID. Mit dem Session-Cookie wird sie als Header OpenBot-Organization gesendet. Ein Token ignoriert sie.
onUnauthorized Nein Eine Funktion, die läuft, wenn eine geschützte Anfrage 401 zurückgibt.

Wie du ein User Token erstellst, steht unter Authentifizierung.

Der Client speichert das Token nie und setzt es nie in eine URL. onUnauthorized meldet nicht an und versucht die Anfrage nicht noch einmal. Nutze die Funktion, um den Benutzer zu informieren, zum Beispiel wenn ein Token abläuft oder jemand es widerruft.

Datensatz-IDs haben Markentypen (branded types). Eine Marke trennt die IDs verschiedener Arten von Datensätzen. So kann eine ConnectionId nicht als AppViewId dienen. Nutze die ID aus einer API-Antwort für weitere Aufrufe. Lies eine ID von außerhalb des Programms mit ihrem Schema aus @openbot-build/api, etwa ConnectionsApi.ConnectionId. Modell- und Anbieter-IDs nutzen Model.Id und Provider.Id aus @openbot-build/core. In JSON und URLs bleiben diese Werte Zeichenketten.

Jede Operation liegt unter client.api.<group>.<operation>. Die Namen sind dieselben wie in der API-Referenz. Jeder Aufruf nimmt ein Objekt mit diesen optionalen Feldern:

Feld Was es enthält
params Werte im Pfad, zum Beispiel chatId.
query Werte im Query-String.
payload Den JSON-Body.
const chats = yield * client.api.chats.listChats({ query: {} })
const view =
yield *
client.api.chats.getChat({
params: { chatId: id },
})
const tasks =
yield *
client.api.chats.listChats({
query: { kind: "task" },
})

Einige Gruppen und ihre Seiten:

Gruppe Nutze sie, um Referenz
chats Mit Bots zu sprechen und Chats und Ergebnisse zu lesen. Chats
delegations Die Arbeit zu lesen und zu stoppen, die Bots abgegeben haben. Delegations
bots Bots zu erstellen und zu verwalten. Bots
requests Die Requests deiner Bots zu beantworten. Requests
apps Eine App unter einer Adresse zu finden. App discovery
connections App-Verbindungen hinzuzufügen und zu verwalten. Connections
appIndex Den öffentlichen App Index zu durchsuchen. App Index
userTokens User Tokens aufzulisten, zu erstellen, zu widerrufen und zu prüfen. User tokens
organizations Deine Organisationen aufzulisten und zu erstellen. Organizations
organization Die Organisation, ihre Mitglieder, Rollen und Einstellungen zu verwalten. Organization
modelProviders Model Providers zu verwalten und Models aufzulisten. Model providers
sso E-Mail-Domains zu bestätigen und SSO-Verbindungen zu verwalten. SSO
platform Die Plattform zu verwalten. Nur für Plattform-Admins. Platform
health Das Dienstdokument und den Zustand zu lesen. Health

Jede Operation braucht eine Aktion auf einer Ressource. Ein Aufruf ohne sie schlägt mit Forbidden fehl. Siehe Berechtigungen.

Der typisierte Client nutzt Effect Redacted<string> für geheime Felder in Anfragen und Antworten. Packen Sie neue Geheimnisse mit Redacted.make ein. Nutzen Sie Redacted.value nur, wenn Sie den Wert senden oder zeigen müssen. In JSON bleiben diese Felder Strings. Das gilt für Anbieterschlüssel, Vault-Werte, SSO-Geheimnisse, Einladungen, Tokens, Webhooks und Computer-Kopplungen.

Nutze die Gruppe modelProviders für einen chatgpt-Anbieter. providerId ist seine normale prov_*-ID. Starte vorher einen lokalen Listener unter http://127.0.0.1:<port>/auth/callback. Seine Adresse ist redirectUri.

const start = Effect.gen(function* () {
const provider = yield* client.api.modelProviders.getModelProvider({
params: { providerId },
})
const authorization =
yield* client.api.modelProviders.startProviderAuthorization({
params: { providerId },
payload: { redirectUri, expectedVersion: provider.version },
})
return authorization
})

Öffne authorization.url im Browser. Sende die vollständige callbackUrl, die dein Listener erhält.

const finish = Effect.gen(function* () {
const connected =
yield* client.api.modelProviders.completeProviderAuthorization({
params: { providerId, authorizationId: authorization.id },
payload: { callbackUrl },
})
const available = yield* client.api.modelProviders.getProviderAvailableModels(
{
params: { providerId },
},
)
return { connected, available }
})
const disconnect = client.api.modelProviders.disconnectProviderOAuth({
params: { providerId },
})

Es gelten normale Anbieterrechte mit API-Schlüssel oder Session-Cookie desselben Origins. Tokens bleiben verschlüsselt auf dem Server. Plattformanbieter nutzen die entsprechenden PlatformProvider-Operationen unter client.api.platform. available.items enthält Upstream-Modelle. Lege sie über die normale Modell-API an und verknüpfe sie. Die Anmeldung ändert keine Modellauswahl. ModelProviderAuthorizationInvalid braucht einen neuen Versuch, ModelProviderLoginRequired eine Anmeldung. ModelProviderOAuthUnavailable bedeutet, dass der Aufruf gerade nicht möglich ist. Bei ModelProviderConflict lies die aktuelle Anbieterversion. Siehe Models.

Ein Befehlsbeleg enthält executionId, commandKey und sequence. Er bestätigt das Speichern des Befehls, nicht das Ende einer externen Handlung. Lies GET /v1/executions/:executionId für status und failure. Ein Fehler enthält nur category und errorId.

Lies gespeicherte Ereignisse mit GET /v1/executions/:executionId/events?afterSequence=0. Die Seite enthält events, afterSequence und terminal. Behalte den zurückgegebenen Cursor für die nächste Seite. Für laufende Ereignisse nutze SSE, einen Server-Ereignisstrom:

Terminal window
curl -N -H "Authorization: Bearer $OPENBOT_TOKEN" \
"https://api.example.com/v1/executions/$EXECUTION_ID/observe?afterSequence=0"

Jede SSE-Nachricht hat den Ereignisnamen execution. Ihre Daten enthalten executionId, sequence, createdAt und body. Speichere die zuletzt verarbeitete Nummer. Sende sie nach einem Verbindungsabbruch als afterSequence. Jedes Kind braucht einen eigenen Cursor. Textereignisse haben step und attempt. Halte verschiedene Versuche getrennt. Executions.observe(id, cursor) gibt die Ereignisse als Effect Stream. Das Schließen des Streams bricht angenommene Arbeit nicht ab.

Terminal window
curl -X POST -H "Authorization: Bearer $OPENBOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"commandKey":"cancel-work-1"}' \
"https://api.example.com/v1/executions/$EXECUTION_ID/cancel"

Abbruch- und Entscheidungsendpunkte geben HTTP 200 mit einem Befehlsbeleg. Arbeit kann cancelling bleiben, solange Ressourcen freigegeben werden oder ein externes Ergebnis unbekannt ist. Endzustände sind completed, failed, cancelled und interrupted. waiting und outcome_unknown brauchen noch Fortschritt oder eine Entscheidung.

Ein DecisionNeeded-Ereignis nennt decisionId und die erlaubten choices. Sende eine davon an POST /v1/executions/:executionId/decisions:

{
"commandKey": "answer-work-1",
"decisionId": "<saved decision id>",
"answer": "stop"
}

Nutze bei einem neuen Versuch denselben Schlüssel und Inhalt. Ein anderer Inhalt mit demselben Schlüssel ergibt 409 ExecutionKeyConflict. Eine Entscheidung hebt aktuelle Rechte oder Token-Grenzen nicht auf. API-Schlüssel und Cookies vom gleichen Ursprung nutzen dieselben Prüfungen der Anwendung.

Jeder fehlgeschlagene Aufruf gibt einen typisierten Fehler. Der Typ ClientError listet sie alle auf:

  • API-Fehler aus dem Vertrag. Jeder hat einen _tag, zum Beispiel ChatNotFound oder ChatConflict.
  • Schema-Fehler (SchemaError), wenn eine Antwort nicht zum Vertrag passt.
  • HTTP-Client-Fehler, wenn das Netzwerk ausfällt.

Nutze Effect.catchTag, um eine Art von Fehler zu behandeln:

const view =
yield *
client.api.chats
.getChat({ params: { chatId: id } })
.pipe(Effect.catchTag("ChatNotFound", () => Effect.succeed(undefined)))

Häufige Fehler bei der Anmeldung:

Tag Status Bedeutung
AuthenticationRequired 401 Kein Schlüssel und kein Cookie, oder der Schlüssel ist widerrufen oder abgelaufen.
OrganizationRequired 400 Eine Cookie-Anfrage ohne organization, und die Person gehört zu mehr als einer Organisation.
MembershipRequired 403 Das Konto ist kein aktives Mitglied der Organisation.
ForbiddenOrigin 403 Eine Cookie-Anfrage, die Daten ändert, hat den falschen Origin.
AuthenticationUnavailable 503 OpenBot kann Zugangsdaten gerade nicht prüfen.

Alle Fehler-Schemas werden aus @openbot-build/api exportiert.


Eine Web-App auf demselben Origin wie OpenBot kann das Anmelde-Cookie statt eines Tokens nutzen. Lass token weg:

import { layer } from "@openbot-build/client"
const ClientLive = layer({
baseUrl: window.location.origin,
// Nötig, wenn die Person zu mehr als einer Organisation gehört.
organization: "org_8f3a",
onUnauthorized: async () => {
// OpenBot wählt den Anbieter und antwortet mit seiner Adresse.
const response = await fetch("/auth/sign-in", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ returnTo: "/" }),
})
const { url } = await response.json()
window.location.assign(url)
},
})

Der Client sendet Anfragen mit credentials: "same-origin". So fügt der Browser das Cookie hinzu. Der Browser fügt auch den Header Origin hinzu. Bei Anfragen, die Daten ändern, prüft OpenBot, dass er genau publicOrigin aus den Plattform-Einstellungen ist. Die Organisationen der Person liest du mit GET /auth/session. Siehe Authentifizierung.

Wie du deine Web-App auf demselben Origin bereitstellst, steht unter Betrieb.

make({ baseUrl }) baut den Dienst Client ohne die Standard-Teile. Du gibst ihm dann:

  • einen Dienst HttpClient,
  • Client-Middleware für BotAuth, genutzt von Token-Routen,
  • Client-Middleware für AccountAuth, genutzt von Routen, die nicht in einer einzelnen Organisation handeln, zum Beispiel /v1/organizations und /v1/platform,
  • Client-Middleware für SessionAuth, genutzt von Routen nur mit Cookie.

Nutze das, wenn du deinen eigenen HTTP-Client, eigenes Logging oder eigene Header brauchst. Wie layer das macht, siehst du in packages/client/src/Client.ts.


Manche App-Werkzeuge haben eine kleine Benutzeroberfläche, eine sogenannte MCP-Apps-Ansicht. MCP ist das Model Context Protocol, ein Standardweg, auf dem Bots mit Werkzeugen sprechen. OpenBot hält die Ansicht auf dem Server. Dein Client zeigt sie nur an.

Öffne zuerst die Ansicht für eine App-Operation. Erstelle dann mit AppViews.make einen Ansichts-Client:

import { AppViews, Client } from "@openbot-build/client"
import { Effect } from "effect"
const program = Effect.gen(function* () {
const client = yield* Client
const opened = yield* client.api.appViews.open({
params: { operationId },
payload: { chatId, itemId },
})
return yield* AppViews.make(opened, accountId, () => refresh())
})
Argument Was es ist
opened Die Ansicht, die appViews.open zurückgegeben hat.
owner Eine feste ID des angemeldeten Kontos. Sie hält Ansichten verschiedener Konten getrennt.
onChange Optional. Läuft, wenn eine Aktion in der Ansicht ihren Status ändert. Nutze es, um Requests und den Chat neu zu laden.

Das Ergebnis funktioniert mit mount aus @openbot-build/protocol/McpApps. Das zeigt die Ansicht in einem iframe. Siehe Protokoll.

Der Ansichts-Client hält keine Zugangsdaten. Jede Anfrage geht über OpenBot. Darum gelten die normalen Prüfungen der Fähigkeiten:

  • Er prüft, dass jede Antwort ein gültiges MCP-Ergebnis ist.
  • Er sendet jede Aktion mit einem Befehlsschlüssel.
  • Er folgt gespeicherten Ereignissen der Kind-Ausführungen, auch nach dem Ende des ersten Turns.
  • Beim Ergebnis-Ereignis liest er das gespeicherte App-Ergebnis. Beim Warten sendet er die Aktion nicht erneut.

Executions.observe(id, afterSequence) liest eine Ausführung ab dem gespeicherten Cursor. Executions.observeTree(id) folgt ihr und ihren Kind-Ausführungen von Anfang an. Jedes Kind hat eigene Folgenummern. Mit stopAt: ["AppCall"] lesen Sie App-Ereignisse ohne deren Entscheidungs-Kinder. Die View-Brücke nutzt diese Grenze; das Lesen eines App-Ergebnisses verlangt kein Recht zur Genehmigung. AppViews.act und AppViews.message erstellen einen Befehl mit festem Schlüssel. Geht die Antwort verloren, verwenden Sie seinen send-Effect erneut.

Wenn du die Ansicht schließt, hört der Client mit dem Prüfen auf. Eine Aktion, die OpenBot schon angenommen hat, läuft trotzdem bis zum Ende. Alle Server speichern das Schließen, Änderungen am Kontext und neue Nachrichten für jede Ansicht in einer festen Reihenfolge. Nach dem Schließen können wartende Anfragen den Kontext nicht ändern und keine Nachrichten hinzufügen. Ein Neustart schließt eine Ansicht nicht. Ihre Laufzeit und die aktuellen Zugriffsrechte gelten weiter.


Der Bot nutzt das Tool confide, um zu sagen, wie seine Arbeit läuft, und der Memory-Bot antwortet ihm. Personen dürfen das lesen: GET /v1/chats/:chatId/confessions listet es, und die Aufrufe sind im Verlauf des Chats. Aber der Bot spricht dort freier als in seinen Antworten. Mach es darum nicht zu leicht, sie zu lesen:

  • Zeige Beichten nicht im Gespräch. Klappe die Aufrufe von confide im Verlauf zu, und erzähle Personen in keiner Nachricht davon.
  • Lege sie an einen Ort, den eine Person absichtlich öffnen muss, zum Beispiel in einen zugeklappten Teil der Details eines Chats.
  • Sage vor dem Öffnen in einem Satz, was sie sind: ein Ort, an dem der Bot über seine Arbeit spricht und an dem er nicht erwartet, zitiert zu werden.
  • Zeige die Worte einer Beichte nicht wieder dem Bot, zum Beispiel in einer späteren Nachricht. Wenn der Bot merkt, dass Personen zitieren, was er anvertraut, vertraut er sich nicht mehr an.
  • Wenn eine Beichte etwas betrifft, das eine Person reparieren muss, wie eine gelöschte Datei, führe die Person zu den Items und Aktionen des Chats, nicht zur Beichte.

Der Memory-Bot bittet den Bot auch, der Person offen von Fehlern zu erzählen. So kommen die wichtigen Dinge meist im Chat zur Sprache.