文档
构建 API 客户端集成
使用 Promise SDK、完整的 Effect 合约客户端或生成的 Effect 适配器及其实际输入和错误行为。
从英文自动翻译。 查看英文原文
选择客户
该存储库具有三个相关的客户端界面。它们共享端点声明,但它们的构造、输入形状、运行时依赖性和错误通道不同。有意选择一个并将该表面的示例放在一起。
| 表面 | 切入点 | 执行 |
|---|---|---|
| 无极SDK | @cadcamfun/api/sdk, 导出 CadCam.make | 承诺普通结果; SSE 的异步迭代。使用提供的或全局的获取。 |
| 全合约客户 | ApiClient 从 @cadcamfun/api | 通过其层提供的传输和承载中间件来实现服务。 |
| 生成的效果适配器 | packages/api/src/client/generated/index.ts | 围绕合约客户的扁平化方法;调用者提供所需的传输和中间件。 |
API 包是一个带有源导出的私有工作区。以下进口旨在用于该单一回购或实际提供这些出口的发行版;此页面没有声明单独可用的公共 npm 包。独立客户可以使用 开放API文档 或纯 HTTP。
对于没有客户端库设置的第一个请求,请从 捕获的 HTTP 演练。它确定了成功和失败主体在操作之间的差异。
解决连接问题
使用属于当前 sidecar 启动的 URL 和应用程序承载令牌。金牛座通过以下方式获得它们 get_sidecar_endpoint。手动开发过程在就绪事件中打印其分配的端口。源必须指向 sidecar 或承认的同伴网关,而不是文档站点或 Vite 资产。
Sidecar 重新启动后,再次解析连接。重复使用以前的端口可能会导致连接拒绝或意外寻址另一个进程。重复使用过时的启动令牌会导致授权失败。成功的公共卫生响应并不证明受保护的请求会被接受。
帐户选择、应用程序准入、设备准入和外部代理访问是不同的。可选的 x-session-token 申请录取后选择一个帐户。项目范围的桥接令牌仅限于其操作桥接端点。电话令牌通过网关的允许列表发挥作用。遵循 认证参考 在选择集成连接之前。
无极SDK
导出的命名空间是 CadCam,它的构造函数是 make。提供基本 URL 和默认标头。下面的 create 和 get 方法使用扁平字段而不是嵌套字段 payload 或 params 目的。
import { CadCam } from "@cadcamfun/api/sdk"
const client = CadCam.make({
baseUrl,
headers: { Authorization: "Bearer " + token },
})
const projects = await client.projects.list()
const created = await client.projects.create({
name: "Fixture plate",
description: "An integration-created project",
})
const detail = await client.projects.get({ id: created.id })
console.log(projects.length, detail.name, detail._count.drawings) 每个调用都会直接解析为该端点的成功负载。 projects.list() 返回一个数组; projects.create() 返回项目行。没有通用的 { data, error } 包装纸和没有 throwOnError 此 SDK 中的构造函数选项。一些端点有效负载本身包含 data 或 success 因为那是他们的 HTTP 合同。
SDK 根据端点路径和配置的基本 URL 准备 URL,对路径标识符进行编码,序列化声明的 JSON 输入,并检查操作的确切声明的成功状态。然后它解析 JSON 并返回其类型值。 Promise 运行时不会将 Effect schema 解码器应用于每个返回的有效负载; TypeScript 输出类型不验证任意服务器实现。
申报和运输错误
声明的 HTTP 失败会拒绝解码的响应正文。该值可以是带标签的普通对象,而不是 JavaScript Error 的实例。传输和协议失败使用 SDK ClientError.
import { ClientError } from "@cadcamfun/api/sdk"
try {
await client.projects.create({})
} catch (error) {
if (error instanceof ClientError) {
console.error(error.reason, error.message)
} else if (error && typeof error === "object" && "message" in error) {
console.error(String(error.message))
} else {
console.error("Unrecognized API failure", error)
}
} | 客户端错误原因 | 解释 |
|---|---|
Transport | 获取设置、连接、中止或流读取失败。检查其原因和当前连接。 |
UnexpectedStatus | 服务器返回的状态超出了操作声明的成功和失败状态。 |
UnsupportedContentType | 流响应没有其预期的事件流内容类型。 |
MalformedResponse | 无法解码 JSON 或预期的响应流。 |
SseEventTooLarge | 事件流缓冲区超出了生成的运行时的帧保护。 |
例如,缺少项目名称会导致 HTTP 400 业务失败,即使创建输入类型允许省略。创建项目时重置连接是不同的:它让客户端不确定写入是否完成。在重复突变之前读取资源状态。 SDK 不会添加应用程序范围的幂等性协议或自动重试每个被拒绝的调用。
取消和标题
ClientOptions 接受 baseUrl、可选的获取实现和默认标头。每次通话 RequestOptions 接受信号和额外的标头。对于没有端点输入的方法,请求选项是第一个参数;带有输入的方法在其后面带有请求选项。
const controller = new AbortController()
const pending = client.projects.get(
{ id: projectId },
{
signal: controller.signal,
headers: { "x-session-token": accountSessionToken },
},
)
// Call controller.abort() when the operation is no longer needed.
const project = await pending 请求构建器应用默认标头、端点声明的标头,然后应用每次调用标头。每次调用的值可以覆盖默认值;避免在添加帐户或跟踪标头时意外替换授权。不要将凭据放入复制到日志中的端点 URL 中。
注射 fetch 支持过程中测试传输或受控适配器。适配器仍必须遵守请求标头、正文、取消、内容类型和响应状态语义。桌面自身的效果集成和手机固定的本机传输可以完成额外的工作;替换任意提取不会重现这些本机边界。
全效客户端
ApiClient.layer 构建完整的 HttpApi 客户端、获取传输和承载中间件。它使用底层 HttpApi 请求形状公开端点方法。突变的有效负载下降 payload,并且路径值在下面 params.
import { ApiClient } from "@cadcamfun/api"
import { Effect } from "effect"
const createProject = Effect.gen(function* () {
const api = yield* ApiClient
return yield* api.projects.create({
payload: { name: "Fixture plate" },
})
}).pipe(Effect.provide(ApiClient.layer({ baseUrl, token })))
const project = await Effect.runPromise(createProject) 该客户端源自整个 Api 声明,并且是生成目标中省略的端点的选项。它保留了 Effect 的类型化错误通道和模式感知传输。资源生存期、流中断、层提供属于调用Effect程序;创建 Effect 并不执行它。
可重用层提供应用程序令牌。桌面构建了自己的层 apps/desktop/src/app/client.ts 附加可选的帐户会话状态和注入的传输。在桌面内工作时重用该集成,而不是为每个组件创建第二个连接/运行时。
生成的效果适配器
生成的效果客户端导入真实模式并将方法调整为扁平化字段。它是 CadCam.make 返回一个构建适应客户端的 Effect。它不提供自己的获取传输或应用程序授权层,因此调用者必须提供相应的服务和中间件。
生成的错误适配器将传输/架构/SSE 故障映射到标记的 ClientError 同时保留已声明的域故障。该错误类与 Promise SDK 的 JavaScript 错误类分开,具有 reason 细绳。名称相似并不意味着它们的处理可以互换。
保持输入示例明确。 Promise 和生成的 Effect 表单使用 projects.get({ id });原始合同形式使用 projects.get({ params: { id } })。避免通过未经检查的类型断言将一个表面的输入对象传递给另一个表面。
流媒体
Promise SSE 操作返回异步迭代。迭代开始时进行网络设置。运行时检查成功状态和事件流内容类型,读取增量 UTF-8 数据,标准化帧边界,并对数据帧进行 JSON 解码。格式错误的框架使迭代失败;读取器清理在迭代结束或退出时运行。
运行时的守卫是 16 * 1024 * 1024 缓冲的 JavaScript 字符串单元。它限制帧边界之前的累积;并不保证每个任意 16 MB 有效负载对于应用程序的事件模式都是有用或有效的。
根据鉴别器使用 AI 事件并保留会话标识符。结束的传输并不能证明终端对话事件。使用 AI 流媒体和恢复 用于队列、中止、权限、问题和状态恢复行为。 EventSource 不是带有授权标头和 JSON 正文的 POST 请求的直接替代品。
覆盖和再生
生成器对于它无法表示的形状有明确的省略列表。共享的遗漏包括多部分配置文件/插件上传、具有多个成功模式的 MCP 创建、冲突的输入名称以及在其流上提供响应标头的插件。其他 Promise 遗漏包括原始签名的 Webhook、JavaScript 插件引导、二进制下载和自由格式生成器请求。
阅读当前的确切列表 packages/api/scripts/generate-clients.ts 和 API概览。不存在的生成方法并不表明服务器端点不可用。使用完整合同客户端或适合该操作的原始运输。
bun run generate
bun run generate:check
bun run --filter @cadcamfun/web generate:api-reference
bun run --filter @cadcamfun/web check:api-reference 客户端重新生成和文档重新生成是同一合同的单独输出。端点更改后保持两者最新。不要编辑生成的方法体来更改服务器的输入要求。
生成的代码被写入 packages/api/src/client/generated 和 packages/api/src/sdk/generated。不要手动编辑这些文件;更改契约并重新生成。
集成验证
这里显示的 Promise 创建/读取/简单结果行为和引发的声明失败是使用迁移的一次性数据库针对真实的 sidecar 处理程序堆栈进行的。 HTTP 演练包含相应的观察到的响应。更改生成器、身份验证或端点架构时重复重点检查。
For an integration acceptance check, verify public health; a protected request without a token; the same request with the current token; a valid create/read cycle; a declared validation failure; a cancelled request; and a stream whose terminal state is checked. Use a disposable project and never infer successful persistence from a frontend fixture alone.
实施来源是 packages/api/src/sdk/generated/client.ts, 它是 client-error.ts, packages/api/src/client.ts, packages/api/src/client/generated, 和 packages/httpapi-codegen/src/index.ts。生成器和提交的输出一起建立实际的客户端行为。
Promise SDK 选项参考
| 选项 | 在哪里 | Effect |
|---|---|---|
baseUrl | CadCam.make | 解决了 new URL(path, baseUrl);包括方案和端口。 |
fetch | CadCam.make | 替换传输,例如添加日志记录或注入测试替身。默认为 globalThis.fetch. |
headers | CadCam.make | 随每个请求一起发送。放 authorization 这里。 |
signal | 每次通话 | 中止请求;拒绝是 ClientError("Transport"). |
headers | 每次通话 | 覆盖或添加一次调用的标头,例如 x-session-token。每次通话的价值胜过客户价值。 |
当尸体存在且不存在时 content-type header 已设置,SDK 设置 application/json 并序列化为 JSON.stringify。路径参数始终被传递 encodeURIComponent,所以像这样的键 docs.walkthrough.units 或一个含有 / 保持单个段。
客户端错误原因
| 原因 | 提出时 | 典型反应 |
|---|---|---|
Transport | fetch 抛出(拒绝、重置、DNS、中止)或流读取失败 | 重新解析sidecar URL;重试读取;在重试写入之前重新读取状态。 |
UnexpectedStatus | 该状态既不是成功状态也不是声明的错误状态。原因成立 { status }. | 视为合同不匹配,通常是不同的服务器版本。 |
UnsupportedContentType | 流操作的回答不是 text/event-stream,或非 JSON 类型的 JSON 操作 | 检查代理和服务器版本。 |
MalformedResponse | JSON 为空或无效、主体缺失或 SSE data: 非 JSON 的块 | 记录原始响应;报告它。 |
SseEventTooLarge | 未解析的 SSE 缓冲区超出了 16 MiB | 活动被拒绝;重新启动流。 |
一条消息 ClientError 是原因,后跟原因消息(例如 Transport: fetch failed),所以记录 error.message 仅此一项就已经具有诊断意义。
import { CadCam, ClientError } from "@cadcamfun/api/sdk"
async function withRetry<T>(read: () => Promise<T>, attempts = 3): Promise<T> {
for (let attempt = 1; ; attempt++) {
try {
return await read()
} catch (error) {
const transient = error instanceof ClientError && error.reason === "Transport"
if (!transient || attempt >= attempts) throw error
await new Promise((resolve) => setTimeout(resolve, 250 * 2 ** attempt))
}
}
}
const projects = await withRetry(() => client.projects.list()) 不要盲目重试写入
写入失败 Transport 可能已被应用。再次读取资源,不存在时才重新提交;该 API 没有幂等性密钥。
使用 SDK 使用流
const controller = new AbortController()
const events = client.ai.chatStream(
{ message: "Pocket the 40 x 30 rectangle, 3 mm deep", projectId },
{ signal: controller.signal },
)
for await (const event of events) {
switch (event.type) {
case "delta": render(event); break
case "permission_request": askUser(event); break
case "done": finish(event); break
case "error": showError(event); break
}
} 迭代器解析每个 SSE 块:它连接 data: 块的行,解析 JSON 并生成它。评论和屏蔽没有 data: 被跳过。提前离开循环(break、异常或中止)取消读取器并释放连接。活动内容 type 值是助理推流指南中列出的值;检查生成的 AiChatStreamOutput 键入确切的并集。
示例:通过 SDK 进行偏好设置
const key = "docs.walkthrough.units"
try {
await client.kv.get({ key })
} catch (error) {
if ((error as { _tag?: string })._tag !== "NotFound") throw error // first run: 404
}
const saved = await client.kv.set({ key, value: { units: "mm" } })
// saved: { key, value: { units: "mm" }, updatedAt: "2026-…" }
const { entries } = await client.kv.list({ prefix: "docs." })
await client.kv.remove({ key }) // { message: "…" } 键仅限于 512 个字符,并且必须包含非空白文本;值必须与 JSON 兼容(无循环、函数或非有限数)。