客户端
面向开发者。从开发者手册选择 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-<完整提交哈希>
选择已发布的提交。锁文件会保存标签对应的精确版本:
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 配对密钥。
连接 ChatGPT 账户
Section titled “连接 ChatGPT 账户”通过 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
需要读取最新提供方版本。见模型。
观察和控制执行
Section titled “观察和控制执行”命令回执包含 executionId、commandKey 和
sequence。它表示命令已保存,不代表外部操作已经结束。使用
GET /v1/executions/:executionId 读取 status 和 failure。失败只包含
category 和 errorId。
通过 GET /v1/executions/:executionId/events?afterSequence=0
读取已保存的事件。分页结果包含 events、afterSequence 和
terminal。保存返回的游标,用于下一页。实时事件使用 SSE,即服务器事件流:
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 返回这些事件。关闭事件流不会取消已接受的工作。
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 导出。
在浏览器中使用客户端
Section titled “在浏览器中使用客户端”与 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 视图
Section titled “显示 App 视图”有些 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 把错误直接告诉那个人。所以重要的事情通常会在聊天中说出来。