跳转到内容

客户端

面向开发者。从开发者手册选择 API 接入、App 发布或项目贡献。

@openbot-build/client 是 OpenBot 的 TypeScript 客户端。它和服务器基于同一份 API 契约构建。每个请求、应答和错误都有类型。你在运行代码之前,编辑器就会指出错误。

客户端使用 Effect,这是一个用于编写带类型程序的 TypeScript 库。它可以在 Node.js、Deno、Bun、worker 和浏览器中运行。


包发布在 GitHub Packages:https://npm.pkg.github.com。在项目的 .npmrc 中加入:

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

使用具有 read:packages 权限的 GitHub classic PAT。运行 npm login --scope=@openbot-build --registry=https://npm.pkg.github.com --auth-type=legacy 将令牌存入用户 npm 设置。不要把令牌放入 Git。

安装精确版本,或用 sha-<完整提交哈希> 选择已发布的提交。锁文件会保存标签对应的精确版本:

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

客户端包含共用的 OpenBot 代码和类型,采用 MIT 许可。安装时不需要 OpenBot 源码。其他 OpenBot 包保留各自的许可证。

layer 创建一个 Layer,它提供 Client 服务。Layer 是 Effect 中用来构建服务的“配方”。

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,
}),
),
),
)

每次操作只准备一次命令。回复丢失后重试 command.send, command.commandKey 保持不变。Chats.message 用同样方式向已有 Chat 发送消息。回执包含执行 ID;用 client.api.executions.observe 和上次保存的 afterSequence 观察执行。关闭观察不会取消工作。Executions.cancel 和 Executions.decide 也会准备带稳定键的命令。这些辅助函数需要平台的 Crypto 服务;上面的 Node.js 示例使用 NodeCrypto.layer。

选项 必需 作用
baseUrl 是 OpenBot 的源,例如 https://api.example.com。
token 否 用户令牌,可以是字符串,也可以是 Effect 的 Redacted 值。它以 Authorization: Bearer <token> 发送。
organization 否 组织 ID。使用会话 cookie 时,它以 OpenBot-Organization 头发送。使用令牌时会忽略它。
onUnauthorized 否 一个函数,在受保护的请求返回 401 时运行。

如何创建用户令牌,请看认证。

客户端从不保存令牌,也从不把令牌放进 URL。onUnauthorized 不会登录,也不会重试请求。用它来通知用户,例如在令牌过期或被撤销之后。

记录 ID 使用品牌类型(branded type)。品牌可以防止把一种记录的 ID 用在另一种记录上。例如,ConnectionId 不能用作 AppViewId。调用后续操作时,请保留并使用 API 应答中的 ID。读取程序外部的 ID 时,用 @openbot-build/api 中的对应 schema 解码,例如 ConnectionsApi.ConnectionId。模型和提供方 ID 使用 @openbot-build/core 的 Model.Id 和 Provider.Id。JSON 和 URL 中的值仍是字符串。

每个操作都位于 client.api.<group>.<operation>。名称和 API 参考中的相同。每次调用接受一个对象,它有以下可选字段:

字段 内容
params 路径中的值,例如 chatId。
query 查询字符串中的值。
payload JSON 请求体。
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" },
})

一些分组及其页面:

分组 用途 参考
chats 和 Bot 说话,读取 Chat 和结果。 Chats
delegations 读取和停止 Bot 交出去的工作。 Delegations
bots 创建和管理 Bot。 Bots
requests 回答你的 Bot 发出的 Request。 Requests
apps 在某个地址上查找 App。 App discovery
connections 添加和管理 App 连接。 Connections
appIndex 搜索公开的 App Index。 App Index
userTokens 列出、创建、撤销和检查用户令牌。 User tokens
organizations 列出和创建你的组织。 Organizations
organization 管理组织及其成员、角色和设置。 Organization
modelProviders 管理模型提供方,列出模型。 Model providers
sso 验证邮箱域名,管理 SSO 连接。 SSO
platform 管理平台。仅限平台管理员。 Platform
health 读取服务文档和健康状态。 Health

每个操作都需要对某个资源的一个操作权限。没有它的调用会以 Forbidden 失败。请看权限。

类型客户端中的请求和响应密钥字段使用 Effect Redacted<string>。用 Redacted.make 包装新密钥。只有必须发送或显示密钥时,才调用 Redacted.value。JSON 中这些字段仍是字符串。这包括提供商密钥、Vault 值、SSO 密钥、邀请密钥、令牌、Webhook 密钥和 Computer 配对密钥。

通过 modelProviders 分组操作 chatgpt 提供方。providerId 是普通的 prov_* ID。调用前先启动 http://127.0.0.1:<port>/auth/callback 本机监听,把地址作为 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
})

在浏览器打开 authorization.url。把监听程序收到的完整地址作为 callbackUrl 发送。

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 },
})

接口沿用提供方权限,支持 API 密钥和同源 session cookie。令牌加密留在服务器。平台提供方使用 client.api.platform 中对应的 PlatformProvider 操作。available.items 是上游模型列表;通过普通模型 API 添加并关联它们。连接不会修改模型选择。ModelProviderAuthorizationInvalid 需要重新开始授权,ModelProviderLoginRequired 需要登录,ModelProviderOAuthUnavailable 表示本次调用暂时无法完成。ModelProviderConflict 需要读取最新提供方版本。见模型。

命令回执包含 executionId、commandKey 和 sequence。它表示命令已保存,不代表外部操作已经结束。使用 GET /v1/executions/:executionId 读取 status 和 failure。失败只包含 category 和 errorId。

通过 GET /v1/executions/:executionId/events?afterSequence=0 读取已保存的事件。分页结果包含 events、afterSequence 和 terminal。保存返回的游标,用于下一页。实时事件使用 SSE,即服务器事件流:

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

每条 SSE 消息的事件名为 execution。数据包含 executionId、sequence、 createdAt 和 body。保存最后处理的序号,断线后将它作为 afterSequence 发送。每个子执行使用独立游标。文本事件含 step 和 attempt,不要拼接不同尝试的文本。Executions.observe(id, cursor) 以 Effect Stream 返回这些事件。关闭事件流不会取消已接受的工作。

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"

取消和决策端点返回 HTTP 200 和命令回执。资源释放或外部结果未知时,取消后的执行可以保持 cancelling。终态为 completed、failed、cancelled 和 interrupted。 waiting 和 outcome_unknown 仍需要继续处理或作出决定。

DecisionNeeded 事件给出 decisionId 和允许的 choices。选取其中一个,发送到 POST /v1/executions/:executionId/decisions:

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

重试时保留相同命令键和正文。相同键配不同正文会返回 409 ExecutionKeyConflict。决定不会移除当前权限或 token 限制。API key 和同源 cookie 使用相同的应用检查。

每次失败的调用都会给出一个带类型的错误。类型 ClientError 列出了所有错误:

  • 来自契约的 API 错误。每个都有一个 _tag,例如 ChatNotFound 或 ChatConflict。
  • 应答与契约不符时的 Schema 错误(SchemaError)。
  • 网络故障时的 HTTP 客户端错误。

用 Effect.catchTag 处理某一类错误:

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

常见的登录错误:

标签 状态码 含义
AuthenticationRequired 401 没有密钥或 cookie,或者密钥已被撤销或已过期。
OrganizationRequired 400 使用 cookie 的请求没有带 organization,而这个人属于多个组织。
MembershipRequired 403 该账户不是这个组织的有效成员。
ForbiddenOrigin 403 一个修改数据的 cookie 请求带有错误的 Origin。
AuthenticationUnavailable 503 OpenBot 目前无法检查凭据。

所有错误的 schema 都从 @openbot-build/api 导出。


与 OpenBot 同源的 Web 应用可以用登录 cookie 代替令牌。不要传 token:

import { layer } from "@openbot-build/client"
const ClientLive = layer({
baseUrl: window.location.origin,
// 当这个人属于多个组织时需要。
organization: "org_8f3a",
onUnauthorized: async () => {
// OpenBot 选择提供方,并返回它的地址。
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)
},
})

客户端用 credentials: "same-origin" 发送请求,所以浏览器会带上 cookie。浏览器还会加上 Origin 头。对于修改数据的请求,OpenBot 会检查它是否正好等于平台设置中的 publicOrigin。用 GET /auth/session 读取这个人所属的组织。请看认证。

如何在同源上提供你的 Web 应用,请看部署。

make({ baseUrl }) 构建 Client 服务,但不带默认部件。你需要自己提供:

  • 一个 HttpClient 服务,
  • BotAuth 的客户端中间件,供使用令牌的路由使用,
  • AccountAuth 的客户端中间件,供不在某个组织中操作的路由使用,例如 /v1/organizations 和 /v1/platform,
  • SessionAuth 的客户端中间件,供只接受 cookie 的路由使用。

当你需要自己的 HTTP 客户端、日志或请求头时,就用这种方式。layer 的做法请看 packages/client/src/Client.ts。


有些 App 工具带有一个小的用户界面,叫作 MCP Apps 视图。MCP 是 Model Context Protocol(模型上下文协议),它是 Bot 和工具通信的标准方式。OpenBot 把视图保存在服务器上。你的客户端只负责显示它。

先为一个 App 操作打开视图。然后用 AppViews.make 创建视图客户端:

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())
})
参数 是什么
opened appViews.open 返回的视图。
owner 已登录账户的稳定 ID。它把不同账户的视图分开。
onChange 可选。当视图中某个操作的状态改变时运行。用它来刷新 Request 和 Chat。

结果可以配合 @openbot-build/protocol/McpApps 中的 mount 使用,mount 在 iframe 中显示视图。请看协议。

视图客户端不持有任何凭据。每个请求都经过 OpenBot,所以常规的能力检查都会生效:

  • 它检查每个应答都是有效的 MCP 结果。
  • 它为每个动作使用一个命令键。
  • 它跟随已保存的子执行事件,首个 Turn 结束后也继续观察。
  • 结果事件到达后,它读取已保存的 App 结果。等待期间不会重发动作。

Executions.observe(id, afterSequence) 从保存的游标读取一个执行。 Executions.observeTree(id) 从头观察执行及其子执行。每个子执行有自己的序号。使用 stopAt: ["AppCall"] 可读取 App 事件而不读取其决策子执行。View 桥接使用这个范围;读取 App 结果不要求审批权限。 AppViews.act 和 AppViews.message 创建带固定键的命令;应答丢失后复用它的 send Effect。

关闭视图后,客户端停止检查。OpenBot 已经接受的操作仍会运行到结束。所有服务器节点按每个视图的顺序保存关闭、上下文修改和新消息。关闭保存后,等待中的请求不能修改上下文或添加消息。服务器重启不会关闭视图。视图的有效期和当前访问权限仍然生效。


Bot 用 confide 工具说出工作情况,记忆 Bot 会回答它。人可以读到这些内容:GET /v1/chats/:chatId/confessions 会列出它们,这些调用也在 Chat 的历史中。但 Bot 在那里说话比在回答中更放得开。所以不要让人太容易读到它们:

  • 不要在对话中显示告解。在历史中折叠 confide 工具调用,也不要发消息告诉别人这些内容。
  • 把它们放在一个需要人主动打开的地方,比如 Chat 详情中一个折叠起来的部分。
  • 打开之前,用一句话说明它们是什么:这是 Bot 谈论自己工作的地方,它不希望被引用。
  • 不要再把告解中的话给 Bot 看,比如放进之后的消息里。如果 Bot 发现人们会引用它倾诉的内容,它就不再倾诉了。
  • 当告解涉及某件需要人来处理的事情,比如一个被删除的文件时,请把人引导到 Chat 的 Item 和操作,而不是引导到告解。

记忆 Bot 也会请 Bot 把错误直接告诉那个人。所以重要的事情通常会在聊天中说出来。