# @detail-dev/sahara 中文文档

规范地址：https://npmcn.com/package/%40detail-dev/sahara/

版本：0\.1\.3
npm 元数据更新时间：2026-10-16T10:26:18.586Z
中文译文更新时间：2026-11-12T09:49:25.490Z
软件包许可：Apache-2\.0

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40detail-dev/sahara
原文对照：https://npmcn.com/package/%40detail-dev/sahara/?lang=original
项目主页：https://github.com/usedetail/sahara/tree/main/clients/typescript#readme
源码仓库：https://github.com/usedetail/sahara

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：583 kB，65 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=20
- 运行依赖：0 个
- 首次发布：2026-10-08T22:06:06.249Z
- 最新版本发布：2026-10-16T10:26:18.298Z
- 近 30 天下载量：2618（2026-10-12 至 2026-11-10，npm 官方下载统计）
- 已发布版本数：5（近 12 个月 5 个）

## 中文简介

为撒哈拉公共API所打入的TypeScript客户端:生命周期,处决,SSE事件,TCP隧道和SSH材料.

## 安装

```sh
npm install @detail-dev/sahara
```

也可以使用：`pnpm add @detail-dev/sahara`、`yarn add @detail-dev/sahara`、`bun add @detail-dev/sahara`

## README

# @ detail-dev/shahara (英语)

用于撒哈拉公共控制平面 API 和代理数据的 TypeScript 客户端 飞机 20号节点(针对22号节点开发),零运行时间依赖,双轨 建立无害环境管理。 出版和部署的版本必须从发行收据中确定. 这个包暴露了被支持的消费者SDK,而不是操作员CLI.

安装公开发布 `npm install @detail-dev/sahara`; 预释放使用 `@detail-dev/sahara@next`。用于本地开发,使用一个包装的归档或工作空间 路径。 维护者通过审查的 SDK 版本发布 [SDK 发布](https://npmcn.com/package/%40detail-dev/sahara/PUBLISHING.md),独立于服务/运行时间发布.

沙盒手柄是不透明的字符串(`sb_` 之后是16个小写十六进制数字,即使忘记也永远不再使用. 其他64位身份(执行,化身,流位置, 试图先发制人) `bigint`;线带小数线 并使他们变成正人。 答复文件严格解码: 拒绝未知字段, 匹配服务器的 `deny_unknown_fields` 合同.

```ts
import { SaharaClient } from "@detail-dev/sahara";

const client = new SaharaClient({
    apiBase: "http://control-plane.internal/", // private local example only
    proxyBase: "http://proxy.internal/", // required for files, tunnels and SSH material
    token: process.env.SAHARA_TOKEN!,
    // proxyToken: process.env.SAHARA_PROXY_TOKEN, // when the proxy token differs
});
```

使用TLS或加密地道进行公交或其他不可信交通. 受限制的可信任集群/VPC链接可能使用平文. 私人运行时间/ 帮助交通 单独的认证和栅栏要求; 该客户端不批准 通过公共无记名标志实现内部所有权。 每个有效的公共标志都有完全的服务使用权.

## 失职和错误

承认是确认的持久承诺。 超时,倒出 连接,或 5xx 是 _含糊不清_ 请求可能已经提出。 持久性 变异返回一个持久 `Ack` 改为:`createSandbox`, (中文(简体) ), `exec`, (中文(简体) ), `closeSandbox`, (中文(简体) ), `cancelExecution`, (中文(简体) ), `ackPreemption`在相同的呼叫者选择身份下重试或调和 在限定的预算范围内(默认为 `retry`) (中文(简体) ). 他们从来没有 以新的身份重新发布. `heartbeatSandbox` 重复只更新缓存 同一逻辑的沙盒,处理预算范围内的瞬时故障;它没有 延长绝对过期或提供持久的ACK。 文件写入执行一次有界限的尝试; `mayHaveApplied` 记录不清 发布反应,不重复播放或检查.

-   `409` 表面为 `SaharaConflictError` 与服务器的 `kind` - 包括: 已发现的、身份已与不同意图有关的情况.
-   `401` 是致命的(`SaharaAuthenticationError`从来没有重复过.
-   `404` 实值 `SaharaNotFoundError`。 。 。 .
-   错误的电线文档是 `SaharaContractError` (种类) `"contract"`, (中文(简体) ), 从未再审).
-   呼叫器取消通过 `CallOptions` 或操作的 查询/绑定 `signal`;中止的尝试永远不会被重审.

## 创建、准备、执行和关闭

沙盒形状固定为8 vCPU (8,000 mCPU), 32 GiB RAM (34359738368字节),并有128个GiB稀疏的磁盘(137438953472字节). 没有 资源选择器属于 `CreateSandbox`; 汇总主机过度承诺是一个 分开的录取政策,不是每个沙盒的上限.

```ts
const id = "sb_0000000000000001"; // choose an unused service identity
await client.createSandbox(id, {
    image: 1n, // published image identity available to this service
    expires_at_ms: BigInt(Date.now()) + 3_600_000n,
    metadata: { cohort: "detail-five" },
});

let sandbox = await client.getSandbox(id);
while (sandbox.phase === "Pending" || sandbox.phase === "Booting") {
    await new Promise((resolve) => setTimeout(resolve, 500));
    sandbox = await client.getSandbox(id);
}
if (sandbox.phase !== "Ready") throw new Error(`sandbox phase: ${sandbox.phase}`);

const executionId = 100n;
await client.exec(id, executionId, {
    command: "/work/run.sh",
    env: { MODE: "eval" },
});
let poll = await client.getExecution(executionId);
while (poll.status === "Accepted") {
    await new Promise((resolve) => setTimeout(resolve, 500));
    poll = await client.getExecution(executionId);
}
if (typeof poll.status !== "object" || !("Completed" in poll.status)) {
    throw new Error("execution did not complete");
}
const exitCode = poll.status.Completed;
const output = await client.readExecutionOutput(id, executionId, {
    stream: "stdout",
    offset: 0n,
    limit: 1_048_576n,
});
// output.bytes is bounded; output.complete means the process domain is quiescent.
```

对于大输出, 使用一个代理流而不是请求连续切片 :

```ts
const output = await client.streamExecutionOutput(id, executionId, { stream: "stdout" });
for await (const bytes of output.chunks) await sink.write(bytes);
const receipt = output.receipt(); // exact length + SHA-256, only after verified trailers
```

输出仍保留在来宾磁盘上. 代理认证 本地请求, 解析一次运行,然后从一个保留描述符中接取被绑定的块. 主动执行可以提供经过验证的快照 `complete === false`· ; 完成该快照不能证明进程域为 quescent 。 每个 所请求的拉动有配置的请求超时;调用器处理时间介于 拉不是闲时间 输出方法都不自动重试或退后 到控制飞机的字节路线. `readExecutionOutput` 最多还收 1 MiB在所配置的每试请求截止日期下. 使用 `cancel()` 用于 未消耗的流; 放弃一个活式的转发器取消其响应.

匹配的来宾/运行时间二进制必须使用当前保留输出格式, 包括靴子、沙盒、创造、化身和执行。 旧产出 名称不会从新读者处获得创建身份。 部署和 持续产出的采用需要单独的限定.

整个恢复过程的沙盒手柄保持稳定。 服务可持久地约束接受 执行 ID 到他们的第一次运行; 重试从不重瞄准它们 。 文件调用 解决每个请求的一次运行. `creation` 财务报告和审定财务报表 `incarnation` 是诊断 字段;心跳和SSH只取逻辑沙盒手柄.

## 普查准备页

固定前缀普查,要求每页最多500行,并遵循 `next_cursor` 通过终端页面,包括当该页面为空时:

```ts
let cursor: string | undefined;
let through: bigint | undefined;
let position: bigint | undefined;
const readyIds = new Set<string>();
do {
    const page = await client.listSandboxesPage({ phase: "Ready", limit: 500, cursor });
    if (through !== undefined && page.through !== through) throw new Error("cutoff moved");
    if (position !== undefined && page.position !== position) throw new Error("prefix moved");
    through = page.through;
    position = page.position;
    for (const row of page.sandboxes) readyIds.add(row.id);
    cursor = page.next_cursor ?? undefined;
} while (cursor !== undefined);
```

这是历史性的 承诺前缀观察, 不是证明所有客人 或提供者的能力和详细工作已经过去。 检查精确 身份证, `creation`, (中文(简体) ), `incarnation`,固定资源,运行元数据以及 计数,然后保留单独的运行时间和拆卸证据.

## 事件和预先确认

```ts
const controller = new AbortController();
const signal = controller.signal;
for await (const frame of client.events(sandbox.id, { signal })) {
    if (frame.type === "overflow") {
        // cursor fell outside the retained window — reconcile by reading
        continue;
    }
    const event = frame.event;
    if (event.kind === "preemption") {
        await client.ackPreemption(sandbox.id, event.attempt);
    }
    // frame.id is the durable cursor; persist it to resume with
    // client.events(id, { lastEventId: frame.id })
}
```

执行器将运输故障与 `Last-Event-ID`,宽容 保存相框,检测出摊位, 结束在干净的关闭上.

## TCP 隧道

```ts
const stream = await client.connectTcp(sandbox.id, 8080);
stream.write(payload);
stream.on("data", (chunk) => {
    /* ... */
});
```

或转发回路端口——每个被接受的本地套接字都会得到自己的地道 流线 :

```ts
const forward = await client.forwardLocalPort(sandbox.id, 8080);
console.log(`localhost:${forward.port}`);
await forward.stop();
```

## 逻辑沙盒的 SSH 材料

```ts
const ssh = await client.sshMaterial(sandbox.id);
// { user, host_public_key, port, private_key_pem }
// present only while the resolved runtime is live
```

## 消费者形象

```ts
for await (const entry of client.listImages()) {
    const image = await client.getImage(entry.id);
    console.log(image.id, image.phase);
    // await client.deleteImage(image.id); // tombstones this shared catalog entry
}
// Public list/get include captured images and operator-imported base images.
// Every accepted public token sees the same catalog; there is no per-token owner.
// Public deletion uses the same catalog operation as operator deletion.
// Image import remains an operator API, not a public client operation.
```

## 来宾文件:有界流和最后收据

```ts
import { createHash } from "node:crypto";
import { createReadStream, createWriteStream } from "node:fs";
import { stat } from "node:fs/promises";
import { pipeline } from "node:stream/promises";

const local = "/tmp/report.json";
const { size } = await stat(local, { bigint: true });
const hash = createHash("sha256");
for await (const chunk of createReadStream(local, { highWaterMark: 1 << 20 })) {
    hash.update(chunk);
}
const sha256 = hash.digest("hex");
const binding = {
    path: "/home/user/report.json", // inside the default confined guest roots
    length: size,
    sha256,
    mode: "0640",
};
const written = await client.writeFile(id, binding, () =>
    createReadStream(local, { highWaterMark: 1 << 20 }),
);
if (written.size !== size || written.sha256 !== sha256) throw new Error("receipt mismatch");

const download = await client.readFile(id, {
    path: binding.path,
});
await pipeline(download.chunks, createWriteStream("/tmp/roundtrip.json"));
const receipt = download.receipt(); // only available after final HTTP trailers verify
if (receipt.length !== size || receipt.sha256 !== sha256) {
    throw new Error("file changed between upload and download");
}
await client.closeSandbox(id); // logical close; verify physical teardown separately
```

上传源头在每次明确通话时都会被消耗. 委托人把范围拉大了 拖取并只接受匹配大小/ digest 出版收据 。 丢失的答复 可能隐藏已公布的文件:检查 `mayHaveApplied`,并决定是否另作决定 独立写作是适当的。 没有公开转移身份或检查 终点. `writeFileBytes`编号 :`writeFileText` 处理呼叫者拥有的小型缓冲器; `readFileBytes` 需求 `maxBytes` 并在返回前验证终端完整性. 流出大型文件和舞台下载字节直至 `receipt()` 成功 .

来源必须是 HTTP( S) 根源, 没有用户信息、 路径前缀、 查询或 碎片。 重定向已禁用 。 文件使用代理证书. 普通JSON语 30 秒 15次尝试预算 250毫秒指数 5秒后就关闭了 SSE 重接连接 EOF 和部分 框架来自上一个完全解码光标; 只交付应用程序事件 重新设置其连续失败窗口 。 空成功打开无法重置 . A 类 `closed` 事件或取消结束订阅。 串流设置包括 - 健康订阅没有终身总上限.

上传有单独的 **30分钟** 完全尝试的截止日期,包括字节和发布回复。 覆盖 `attemptTimeoutMs` 在流媒体或字节/文本上传;这绝不允许重放。 普通的JSON电话保留了较短的重试预算.

文件下载使用 `retry.requestTimeoutMs` (默认时为15秒) 对于单取设置,包括任何拒绝体,然后45秒闲置读取,没有完全健康-流的最后期限. 闲置的截止时间仅在执行者请求字节或终端拖车时运行,因此下游回压不会超过健康的一流. 打电话 `cancel()` 或中止信号以阻止它; 部分文件永远不会被重放.

## 认证和装订检查

-   无效的公共令牌失败 `SaharaAuthenticationError`;所有有效符号共享服务访问.
-   重用具有不同不可改变意图冲突的操作ID. 接受的工作从未默默地跟随康复.
-   烂透了 `expected_sha256_before` 先决条件不能覆盖已更改的目的地.
-   路径仍然局限于服务器允许的来宾根.
-   出处测试包括单试出版,粘糊糊糊的模糊性,坏坏的收据和拖车故障等.

沙盒事件流上的压力观测结果为 `event.kind === "pressure"` 边框( e)`resource`, (中文(简体) ), `level`, (中文(简体) ), `cleared`, (中文(简体) ), `value`, (中文(简体) ), `threshold`, (中文(简体) ), `since_ms`) (中文(简体) ).

## 从详细单曲播放

建造焦油球并安装所保留的, 作为文件依赖的源头类文物 :

```sh
cd clients/typescript
pnpm install --frozen-lockfile && pnpm check:generated && pnpm build
pnpm pack && pnpm smoke:installed
```

单位 `apps/backend/package.json`数字 :

```json
{
    "dependencies": {
        "@detail-dev/sahara": "file:../../vendor/detail-dev-sahara-<VERSION>.tgz"
    }
}
```

或者销售整个软件包目录并使用 pnpm 工作空间 `link:` 编号 : `file:` 找到它的道路。 货船 `dist/esm` 与 `.d.ts` 声明。 普通JS消费者必须使用动态 `import()`。 。 。 .

## 复兴合同

`pnpm generate` 运行 `cargo xtask contracts` 重播和重播时 `src/generated/*` (类型+代码+操作表). `pnpm check:generated` 漂移失败; 在服务器侧 API 更改后在 CI 中运行 .

## 测试

```sh
pnpm test    # unit tests + contract suite against owned postgres + valkey
             # containers plus sahara-sim-runtime + sahara-control-plane
             # (binaries built at the repo root; requires Docker)
```

将套房定位为真正的部署——例如 `cargo xtask physical-e2e --hold` 集合,它印出每一个这些 设定 :

| 变量 | 含义 |
| --- | --- |
| `SAHARA_API` | 公共 API 基础; 逗号分离的复制基地被接受, 领导者通过 `/metrics` |
| `SAHARA_TOKEN` | 公共 API 无记名符号 |
| `SAHARA_PROXY` | 代理数据飞机基础(可进行隧道/SSH合同测试) |
| `SAHARA_PROXY_TOKEN` | 当代理无记名符号与 `SAHARA_TOKEN` |
| `SAHARA_OPERATOR_TOKEN` | 运算符无记名符 (默认为 `SAHARA_TOKEN`(中文(简体) ) |
| `SAHARA_IMAGE_ID` | 已发布图像 ID 沙盒规格应参考 |
| `SAHARA_ID_BASE` | 第一个呼叫者选择的沙盒身份( 默认为100000); 重跑时与状态部署相撞 |

SSE恢复ID是精确的字符串:服务输出出一个20位零加法u64位置,一个连字符,以及一个u64正数加法到至少四个数字. 承诺量较大的批量可释放出更宽的正弦,如一万个. 这两个字段被限定为20个小数位和u64最高(1844644073709551615). 客户端也接受未加限的十进制对等,但呼叫者应当不加修改地通过收到的光标,而不是构造或使光标正常化. 客户端发送时保留其精确的字节 `Last-Event-ID`; 贴花与JSON DTOs中使用的克尼克十进制字符串不同.

## 常见问题

### @detail-dev/sahara 是什么？

为撒哈拉公共API所打入的TypeScript客户端:生命周期,处决,SSE事件,TCP隧道和SSH材料\.

### 如何安装 @detail-dev/sahara？

在项目目录运行 npm install @detail-dev/sahara，或使用 pnpm add / yarn add / bun add。

### @detail-dev/sahara 支持 TypeScript 吗？

支持。包内自带 TypeScript 类型声明（package\.json 声明了 types 入口），安装后即可获得类型提示，无需再安装 @types 包。

### @detail-dev/sahara 支持 ESM 还是 CommonJS？

以 ES Module 形式发布，推荐使用 import 引入。在 CommonJS 项目中，Node\.js 20\.19\+ / 22\.12\+ 可以直接 require\(\) 不含顶层 await 的 ES Module，更早的版本需要改用动态 import\(\)。

### @detail-dev/sahara 需要什么版本的 Node\.js？

package\.json 的 engines 字段要求 Node\.js \>=20。版本不符时 npm 默认只给出警告，开启 engine-strict 后会拒绝安装。

### @detail-dev/sahara 可以商用吗？

@detail-dev/sahara 使用 Apache-2\.0 许可。宽松许可：允许商业使用、修改和再分发，并附带明确的专利授权；分发时需保留许可与 NOTICE 文件，修改过的文件要注明改动。以上为概括，不构成法律意见，请以许可原文为准。

### @detail-dev/sahara 的下载量有多少？

根据 npm 官方下载统计，@detail-dev/sahara 在 2026/10/12 – 2026/11/10 这 30 天内被下载 2,618 次，日均约 87 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入，适合横向比较热度，不能直接等同于使用人数。

### @detail-dev/sahara 还在维护吗？

最新版本 0\.1\.3 发布于 2026/10/16（27 天前）；至今共发布 5 个版本，其中过去 12 个月发布了 5 个；首次发布于 2026/10/09。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

