# @byokit/relay 中文文档

规范地址：https://npmcn.com/package/%40byokit/relay/

版本：0\.8\.4
npm 元数据更新时间：2026-10-29T08:13:10.157Z
中文译文更新时间：2026-11-08T21:33:30.240Z
软件包许可：Apache-2\.0

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

原始 npm 页面：https://www.npmjs.com/package/%40byokit/relay
原文对照：https://npmcn.com/package/%40byokit/relay/?lang=original
项目主页：https://github.com/umeranjum17/byokit#relay
源码仓库：https://github.com/umeranjum17/byokit

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

- 包体积（解压后）：176 kB，26 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=22\.18
- 运行依赖：3 个
- 首次发布：2026-09-25T06:57:26.084Z
- 最新版本发布：2026-10-29T08:13:09.827Z
- 近 30 天下载量：7000（2026-10-09 至 2026-11-07，npm 官方下载统计）
- 已发布版本数：22（近 12 个月 22 个）

## 中文简介

TypeScript套件让人们通过他们已经支付的人工智能计划,在网络,电话和桌面上登录到你的应用程序.

## 安装

```sh
npm install @byokit/relay
```

也可以使用：`pnpm add @byokit/relay`、`yarn add @byokit/relay`、`bun add @byokit/relay`

## README

# @ byokit/ 转发

[![npm](https://img.shields.io/npm/v/@byokit/relay?style=flat&label=npm)](https://www.npmjs.com/package/@byokit/relay) [![CI](https://img.shields.io/github/actions/workflow/status/umeranjum17/byokit/ci.yml?style=flat&branch=main)](https://github.com/umeranjum17/byokit/actions/workflows/ci.yml) [![Apache 2.0](https://img.shields.io/badge/license-Apache--2.0-666?style=flat)](https://npmcn.com/package/%40byokit/relay/LICENSE) ![Node | device side anywhere](https://img.shields.io/badge/platform-Node%20%7C%20device%20side%20anywhere-666?style=flat)

**盲转 [`@byokit/pair`](https://npmcn.com/package/%40byokit/pair):家用电脑拨出后,电话通过中继器到达.**  
家用计算机 **东道主**) 不需要开放端口。 中继路由对按主机地址加密的帧,不能 读他们。 它也保留了睡眠电话需要什么(推出通知), 一个简短的代码,一个人可以输入找到主机, 和所有者列表 哪些主机可以使用它。 对于将手机或浏览器配对的应用软件 远离家乡到达它.

"不能读"的一个例外:推出通知文本. 中继器可以读取标题和主机的任何内容 明确包括.

只有节点(设备侧, `@byokit/relay/device`,在任何地方运行: Node APIs,所以浏览器和React Industrial也). 取决于 [`@byokit/pair`](https://npmcn.com/package/%40byokit/pair) (见 [软件包.json](https://npmcn.com/package/%40byokit/relay/package.json) 它不会改变它的电线 格式或密码.

## 安装

```sh
npm install @byokit/relay @byokit/pair
```

[![npm](https://img.shields.io/npm/v/@byokit/relay?style=flat&label=)](https://www.npmjs.com/package/@byokit/relay) • 妇女 [最近发布](https://github.com/umeranjum17/byokit/releases?q=relay-v) • 妇女 [所有释放](https://github.com/umeranjum17/byokit/releases)

## 快速启动

```sh
npm install @byokit/relay @byokit/pair
```

与内存状态的回转中继, 链接主机注册通过 `RelayClient`,并找到 主机通过短码和与之相配:

```ts
import { createServer } from 'node:http';
import { Host, keyPair, pairWithCode, DeviceLink } from '@byokit/pair';
import { Relay, RelayClient, type RelayState } from '@byokit/relay';
import { findHost } from '@byokit/relay/device';

// The relay: loopback, state kept in memory.
let saved: RelayState | undefined;
const relay = await Relay.open({ store: { load: () => saved, save: (s) => { saved = s; } } });
const server = createServer();
relay.attach(server);
await new Promise<void>((r) => server.listen(7300, '127.0.0.1', r));

// The host: a link Host that dials out to the relay.
const host = await Host.open({
  keys: keyPair(), name: 'Kitchen computer',
  confirm: () => true, handle: (req) => ({ pong: req.op }),
});
await relay.admit(host.keys.publicKey, 'Kitchen computer');
const client = new RelayClient(host, { url: 'ws://127.0.0.1:7300/relay/v1/host', onStatus: (s) => console.log('host:', s) });
const { code: short } = await client.code();
const { code } = host.code({ role: 'control' });
console.log('short code:', short, ' link code:', code);

// The device: types both codes.
const url = await findHost('http://127.0.0.1:7300', short);
console.log('findHost:', url);
const grant = await pairWithCode(url, code, { name: 'Pixel 9', onWords: (w) => console.log('verify words:', w) });
const link = new DeviceLink(grant);
console.log('reply:', await link.request('ping'));

link.stop(); client.stop(); host.close(); relay.close(); server.closeAllConnections(); server.close();
```

实际输出( 代码、 主机 id 和单词在每次运行时不同 ) :

```text
host: online
short code: RMWWK7  link code: 8Y75-TSWX-9BH9
findHost: ws://127.0.0.1:7300/link/v1/GG0AKCQjt_SpiZKdG129tg
verify words: galaxy creek
reply: { pong: 'ping' }
```

## API 一览

| 导出 | 它会做什么 |
| --- | --- |
| `Relay` | 中继服务器 : `Relay.open(options)`, (中文(简体) ), `attach(server)` (或 `upgrade` 编号 : `request`, (中文(简体) ), `admit`, (中文(简体) ), `enrolment`, (中文(简体) ), `hosts`, (中文(简体) ), `revoke`, (中文(简体) ), `count`, (中文(简体) ), `close` |
| `RelayClient` | 主机侧:一个外出插座,可以证明主机的密钥并重新连接; `code`, (中文(简体) ), `subscribe`, (中文(简体) ), `unsubscribe`, (中文(简体) ), `notify`, (中文(简体) ), `revoke`, (中文(简体) ), `pending`, (中文(简体) ), `self`, (中文(简体) ), `leave`, (中文(简体) ), `stop` |
| `ownerClient` | 车主HTTP API: `hosts`, (中文(简体) ), `enrolment`, (中文(简体) ), `revoke`; 接受注入 `fetch` |
| `claimProvision` | 新的电脑应用:通过链接本身的耐用赠款,给手机钥匙绑定一个供货符 |
| `RelayOwnerError` | HTTP 用数字拒绝 `status` (单位:千美元) `code`数字 : `forbidden` (403个), `not-found` (404个)或 `request-failed` |
| `RelayLookupError` | 中继器拒绝或未回答主机查询: a `code` 联 合 国 `wrong-code`, (中文(简体) ), `unreachable` 或者说 `timeout` |
| `findHost` | 设备的侧面:一个简短的代码 ~ 要拨号的链接地址, 以 `timeoutMs` (默认 8 s; 也来自 `@byokit/relay/device`(中文(简体) ) |
| `linkUrl` | 中继 URL 和已知主机 id ~ 要拨号的链接地址( 也来自 `@byokit/relay/device`(中文(简体) ) |
| `JobChannel` | 拥有主机的限定任务历史 : `create`, (中文(简体) ), `follow`, (中文(简体) ), `drop`; 在光标后重放命令框 |
| `readJobStream` | 手提式订购文本/图像/用户/终端阅读器(也来自 `@byokit/relay/device`(中文(简体) ) |
| `LIMITS` | 每分钟每个客户地址限额 |
| `CLOSE` | WebSocket 关闭中继器使用的代码( 替换、 未注册、 不良证明、 注册、 撤销、 过多) |
| `isAllowedEndpoint`, (中文(简体) ), `isExpoToken` | 检查Web Push 端点和 Expo 推令牌 |
| `MAX_ACTION_REPLY` | UTF-8 最多字节的通知动作已封存 `reply` 可能携带(8192) |
| 类型 | `RelayOptions`, (中文(简体) ), `RelayStore`, (中文(简体) ), `RelayState`, (中文(简体) ), `HostRecord`, (中文(简体) ), `Enrolment`, (中文(简体) ), `ProvisionBinding`, (中文(简体) ), `RelayClientOptions`, (中文(简体) ), `RelayStatus`, (中文(简体) ), `PushAction`, (中文(简体) ), `Notification`, (中文(简体) ), `PushRecord`, (中文(简体) ), `Subscription`, (中文(简体) ), `WebSubscription` |

## 中继

```ts
import { createServer } from 'node:http';
import { Relay } from '@byokit/relay';

const relay = await Relay.open({
  store: { load: () => db.relayState(), save: (s) => db.setRelayState(s) },  // a 0600 file or a database row
  ownerToken: process.env.RELAY_OWNER_TOKEN,                                // turns on the owner's HTTP routes
  push: { subject: 'mailto:you@example.com' },
});
const server = createServer();
relay.attach(server);           // or call relay.upgrade(req, socket, head) and relay.request(req, res) from your own routes
server.listen(7300, '127.0.0.1');   // put TLS in front (Tailscale Serve, Caddy, …); set trustProxy if it adds X-Forwarded-For
```

路线 :

| 路线 | 谁 | 什麽 |
| --- | --- | --- |
| `WS /link/v1/<host id>` | 设备 | 与主机连接的空链接框架 |
| `WS /relay/v1/host` | 主办单位 | 证明它的密钥,然后随身携带其设备的帧为 `{c, f}` 编号 : `{c, end}` |
| `GET /relay/v1/codes/<code>` | 设备 | 短码 ^ `{ host }` |
| `POST /relay/v1/push/action` | 设备 | `{ token, action, reply? }`: 通知的按钮, 带有可选的封口回复, 由主机回答 |
| `POST /relay/v1/enrolments`, (中文(简体) ), `DELETE /relay/v1/enrolments/<id>`, (中文(简体) ), `GET /relay/v1/hosts[?account=]`, (中文(简体) ), `DELETE /relay/v1/hosts/<id>` | 拥有者(`Authorization: Bearer <ownerToken>`(中文(简体) ) | 制作并撤回一个注册、列表( 选择通过提供账户) 并撤销主机 |

使用打出的所有者客户端而不是把这些请求组合起来 :

```ts
import { ownerClient, RelayOwnerError } from '@byokit/relay';

const owner = ownerClient('https://relay.example', ownerToken);  // optional third argument: { fetch }
const hosts = await owner.hosts();             // HostRecord plus online and devices
const { token, expires } = await owner.enrolment({ name: 'Build server' }); // name is optional
const removed = await owner.revoke(hostId);    // false if already absent
```

HTTP 拒绝 `RelayOwnerError`数字 : `status` 是 HTTP 状态,并且 `code` 实值 `forbidden` 为403, `not-found` 用于404,或 `request-failed` 其他错误。 缺少一个主机 `revoke` 返回时 `false`作为 服务器的删除为同位素。 获取/ 网络错误通过 。 客户拒绝重定向并接受 HTTP或HTTPS接力URL;使用HTTPS外接回. 将所有者代号从设备和公共浏览器代码中保留出. BYOKit提供库;应用程序选择并运行其中继器.

## 哪些主机可能注册

主机的地址是链接的 `host.id`它的公用钥匙的一散 要注册,一个主机证明它持有这个关键:继电器 发送一款新鲜的X25519密钥和一款"非",主机以由他们的DH键的HMAC来回答. 证据对这个有好处 套接字,所以不能被重放,没有人可以在另一个主机的地址下注册. 中继器从来不相信哪里 连接来自:隧道后面的一切看起来都是回回回回回. 东道主必须在其你好预算范围内回答 (单位:千美元)`helloMs` 内 `Relay.open`,默认为 10 s; 较慢的问候结束 `4408 too slow`旁有自备继电器 一个拖住负载的机器可以设定更长的预算.

还必须允许东道主:

-   **自行主办,除一主办方外:** `await relay.admit(host.keys.publicKey, 'Kitchen computer')`。 。 。 .
-   **在 VPS 上共享继电器 :** 拥有者一次性注册,持续5分钟 (单位:千美元)`relay.enrolment({ name })`,或 `POST /relay/v1/enrolments`),并给相机以代号. 这台机器 登记一次;在此之后,它的钥匙就足够了。 索赔只保留了一段散文.

`relay.revoke(id)` 删除主机: 它的注册、 推动订阅和代码, 以及它的套接字和设备' 插座关闭 。 同一主机(同名密钥)的第二个副本取代了第一个副本,该副本以 `4000 replaced by a newer host` 停止而不是反击.

## 东道主

```ts
import { RelayClient } from '@byokit/relay';

const relay = new RelayClient(host, {         // host: a link Host
  url: 'wss://relay.example/relay/v1/host',
  enrol: token,                                // first time only, on a shared relay
  onStatus: (s) => log(s),                     // connecting, online, offline (it retries), replaced, refused
  onAction: ({ device, event, action }) => app.answer(event, action),
  store: { load: () => db.revoking(), save: (d) => db.setRevoking(d) },  // pending unsubscribes; default: memory
});
host.offer({ role: 'control', urls: [`wss://relay.example/link/v1/${host.id}`] });
```

它与后退相接(1s至30 s). 它向中继器提出的请求`code`, (中文(简体) ), `subscribe`, (中文(简体) ), `unsubscribe`, (中文(简体) ), `notify`等待时间 在套接字下架时,在64个队列中,无论中继器没有回答,都会在下一个套接字上再次发送.

## 通过继电器打入配对

Link的打入代码需要主机地址,没有人会打入. 主机向中继请求一个短代码( 六) 用链接的代码显示:

```ts
const { code: short } = await relay.code();   // e.g. K7M2QX, for the relay
const { code } = host.code({ role: 'control' });  // e.g. 7KQ4-M2XP-9RTH, for link
```

```ts
import { findHost, linkUrl } from '@byokit/relay/device';
import { pairWithCode } from '@byokit/pair';
const url = await findHost('https://relay.example', short);   // wss://relay.example/link/v1/<host id>
const grant = await pairWithCode(url, code, { name: 'Pixel 9', onWords: (words) => console.log('Verify on host:', words) });
// With a host id already known, no lookup is needed:
const known = linkUrl('https://relay.example', hostId);
```

中继器只知道 短代码的宿主指向哪个 永远不要链接代码.

搜索被绑定: 接受请求但从未拒绝的中继器 `RelayLookupError` (单位:千美元)`code: 'timeout'`之后 `timeoutMs` (违约8 s, `RELAY_LOOKUP_MS`)并中止请求,所以一个已放弃的中继器 无法进行对接旋转。 错误或使用的代码拒绝 `wrong-code`,一个回答但不能解决拒绝的中继 `unreachable`,而一个从未到达中继器的网络故障仍然以获取错误拒绝.

## 推动通知

设备在链接上向主机发送其推倒地址;主机存储在该设备的中继器上(其授权) 同上,并通知:

```ts
await relay.subscribe(device.id, { expo: 'ExponentPushToken[…]' });            // or { web: pushSubscription.toJSON() }
await relay.notify({ id: 'evt-42', title: 'Agent update', to: [device.id], actions: ['yes', 'no'] });
await relay.revoke(device.id);   // link's revoke, then remove relay subscriptions
```

`revoke` 将设备保存到客户端 `store` 在它放弃赠款之前, 然后发送未订阅的每个 连接(与后退重试,1 s至30 s,如果中继失败)直到中继确认,然后解决. A 类 中继, 主机数不再得到确认 : 它的驱动地址随它而去. `pending()` 列出设备 还在等 `onRevoked(device)` 每起被证实的火灾; `subscribe` 拒绝一个仍未解决的设备。 如果 先停止客户端, `revoke` 拒绝, 但未订阅者仍然保存, 下一位客户端也在相同位置 `store` 发送它. 默认的商店是内存,所以通过一个耐用(一个0600文件或一个数据库行,像中继器的自有).

浏览器订阅中继器的 Web Push 密钥(`relay.vapidKey`,通过链接发送给它。 通知 不变 `id` 在继电器运行时被调试;重启可以两次发送再试通知。 曝光令牌设备 弃(`DeviceNotRegistered`),并去掉了已去掉的Web Push订阅(404,410).

Web Push 端点必须使用 HTTPS `fcm.googleapis.com`,一个子域 `push.apple.com`, (中文(简体) ), `updates.push.services.mozilla.com`,或 `notify.windows.com`。 。 。 . `push.hosts` 内 `Relay.open` 能够缩小 这些目的地(包括精确的子域)从不扩展。 未遵循指令,且不允许 从存储中恢复的订阅在交付前被移除.

`RelayClient.notify` 发送标题但省略 `body` 财务报告和审定财务报表 `data` 默认,即使提供。 过 `{ includeContent: true }` 作为提出这些建议的第二个论据。 也选择通用标题;参见 [警卫](https://npmcn.com/package/%40byokit/relay/SECURITY.md) 用于取消推入的边界和装置.

### 在睡觉的电话上封住通知

封存设备的真实内容( S)`sealNotice` 从 `@byokit/seal`并发送为 `data` 带有通称. 对于 博览会标志,设备在订阅时也会报告其OS,通知要求每个平台让应用 替换通用文本:

```ts
import { RelayClient } from '@byokit/relay';
import { sealNotice } from '@byokit/seal';
declare const relay: RelayClient, deviceId: string, token: string, os: 'ios' | 'android', devicePublicKey: Uint8Array;
declare const question: { agent: string; text: string };

await relay.subscribe(deviceId, { expo: token, platform: os }); // the device sends its token and OS over the link
await relay.notify({
  id: 'ask-42', title: 'An agent needs you', data: sealNotice(question, devicePublicKey), urgency: 'high',
  mutableContent: true,  // iOS: the app's Notification Service Extension may rewrite the alert before it shows
  categoryId: 'agent.ask', // the category (buttons) the app registered
  dataOnly: true,        // Android: no title, body or sound; the app opens `data` and presents the notification
}, { includeContent: true });
```

| 选项 | 博览场 | 效果 |
| --- | --- | --- |
| `mutableContent` | `mutableContent` (iOS) (中文(简体) ) | APN 网络 `mutable-content`。没有通知服务扩展,提示显示发送. `false` 以 `false`。 。 。 . |
| `categoryId` | `categoryId` | 注册类别, `[A-Za-z0-9][A-Za-z0-9._:-]{0,63}`。不发送仅包含数据的信息. |
| `dataOnly` | 标题、正文、声音和类别省略 | 只有订阅的符号 `platform: 'android'`其他(iOS,或无) `platform`)获得可见警报. |

博览会 `data` 在APNs有效载荷下 `body` 在 iOS 上键,所以扩展读取通知 `userInfo["body"]["data"]` (单位:千美元)`@byokit/seal`斯威夫特的打开器是的。 只管理数据消息的应用程序背景 通知任务;Android可能延迟它(Doze),从不将其交付给被强制停止的应用程序,因此无法保证. 无 `includeContent`,只包含 id 、 通用标题和任何动作符以及 app 通过链接获取细节。 应用程序注册任务( T)`Notifications.registerTaskAsync`)并应当显示 ,或Android可能降低后一种数据的优先权。 这个 无内容预设删除所有三个选项。 网推未变:服务员展示开口.

与 `actions`,每个设备的通知都有自己的一次性用途 `action` 符号 。 正在按下按钮 `{ token, action }` 改为 `/relay/v1/push/action`;继电器问主机(`onAction`并等待最多15秒的时间 回答,它会回到设备.

媒体也可以携带 `reply`: 一个被密封的密码文本(例如,一个被封入主机盒密钥的自由文本答案). 一定是一串不空的线 `MAX_ACTION_REPLY` (8192) UTF-8 字节的密码; 任何其它内容都被拒绝 所以纠正后的媒体仍然有效 中继器将字符串转发到未更改的 `onAction` 作为 `PushAction.reply` 永远不要读或存储它。 主机用自己的密钥打开它;中继器的内容边界为 不变.

## 限制

每个客户端的地址,每分钟,比如在muxr的中继器: 60个WebSocket连接,10个短码浏览, 20个按键,10个入学申请和10个失败的主机证明. 每个主机最多256个活装置.

## 链接

-   [字节](https://npmcn.com/package/README.md): 其他软件包
-   [`@byokit/pair`](https://npmcn.com/package/%40byokit/pair): 配对和加密链接
-   [警卫](https://npmcn.com/package/%40byokit/relay/SECURITY.md): 中继器能够和不能看到的东西
-   [变动情况](https://npmcn.com/package/%40byokit/relay/CHANGELOG.md)

## 许可证

阿帕奇2.0 (英语). 见 [自由主义](https://npmcn.com/package/%40byokit/relay/LICENSE) 财务报告和审定财务报表 [通知](https://npmcn.com/package/NOTICE)。 。 。 .

### 自办报名和通知政策

嵌入式应用程序运行服务器并提供这些选项;工具包没有运行中继:

```ts
import { Relay, contentFreeNotify } from '@byokit/relay';

const relay = await Relay.open({
  signup: { open: true, maxHosts: 1000 },
  notify: contentFreeNotify('There is news'),
  limitKey: ({ host }) => host, // proven enrolment or valid action token; otherwise per-IP
});
```

默认注册到 `'enrol'`。 。 。 . `{ open: false, maxHosts: 1000 }` 还要求入学. 打开注册需要验证主机密钥, 并限制自动注册当前总主机 数,在解剖上与坚持. 现有主机以容量重新连接 。 撤销主机释放一个槽; 打开 注册启用, 该密钥可以再次注册 。 拥有者 `admit()` 仍然是注册上限的明显取消. 打开接力时无效的盖( 而非正的安全整数) 失败 .

`notify(notification, host)` 得到有效的通知和经认证的主机代号。 退回通知 改变它,或者 `undefined` 制止恐怖主义`{ sent: 0 }`承诺得到支持。 例外和无效输出 拒绝请求而不予送达。 无内容预设使用固定标题, SHA-256 重试 IDs, 没有正文, 数据或 按钮,并保留收件人选择、紧急和TTL。 重复使用被过滤的 id; 保持转换 平稳地通过回旋。 Hashes隐藏可读的ID,但不保护可猜到的ID不受字典攻击.

`limitKey({ kind, request, host })` 在每个已有的1分钟内选择一桶 `LIMITS` 类别。 正在返回 `undefined` 使用通常的每个IP桶(`trustProxy` 控制传输地址). `host` 只在有效后存在 在登记期间或对于未过期的推动动作符号提供关键证据。 WebSocket 升级、 失败的证明和代码 检查没有经过认证的主机 。 应用程序可以使用自己信任的认证来映射租户桶的请求; 永远不要相信打电话者声称的主人或房客头。 自定义密钥替换每个IP桶, 所以保持预认证 除非有可靠的租户边界.

为重新连接调节,提供持久的 `RelayClient.store`,通过 `RelayClient.revoke`,并在网上重新发送当前订阅。 尚未订阅记录 连接直到确认为止;中继器的存储也必须保持持久.

## 用户注册元数据和自助服务

拥有者最多可附加4096个 UTF-8 字节的 JSON 通过 `relay.enrolment({ meta })` 或者说 `ownerClient(url, token).enrolment({ meta })`。工具包将其视为不透明数据;URL含义属于应用程序:

```ts
import type { Host } from '@byokit/pair';
import { ownerClient, RelayClient, type RelayClientStore } from '@byokit/relay';

async function relayExample(host: Host, ownerToken: string, pendingStore: RelayClientStore) {
  const enrolment = await ownerClient('https://relay.example', ownerToken).enrolment({
    name: 'Kitchen computer',
    meta: { enrolmentUrl: 'https://relay.example/enrol', webUrl: 'https://app.example' },
  });
  const client = new RelayClient(host, { url: 'wss://relay.example/relay/v1/host', enrol: enrolment.token, store: pendingStore });
  const { host: record, devices } = await client.self();
  // record.meta is also available as client.meta after ready, including on reconnect.
  // devices counts live relay connections; the host alone knows which link grants authenticate them.
  await client.leave();
}
```

元数据留在主人和注册主机一边. 中继器既不获取这些 URL 也不发送元数据到 电话. `self()` 需要主机的密钥证明,而不是中继所有者的代号,只返回 `{ host: HostRecord, devices }`。 。 。 . `leave()` 删除主机注册、所有推取订阅、动作符号和短码、关闭设备, 已确认尚未从客户端商店订阅, 并停止 。 失败的中继保存拒绝 移除权威。 在客户端运行时请求重新连接时重试; 在主机重新启动应用程序调用后 `leave()` 再来一次. 它不取消本地链接赠款。 由于默认只注册入学,离开后返回需要新的所有者注册. 明确开放的注册政策允许密钥重新注册,但需遵守主机上限. 开放注册仍然会消耗明确提供的注册,并保留其所有者设置的名称和元数据.

## 循环工作

Link的字节流通过此中继带递入输出,包括二进制数据. 字节流以 然而,它的套接字: 单独打开它无法恢复离线时丢失的输出 . `JobChannel` 添加主机拥有 框架日志。 每个相框都一样 `job` id和不断增加 `seq` 从 1 起, 加上 a `type` 联 合 国 `text`, (中文(简体) ), `image`, (中文(简体) ), `usage` 或者说 `end`。 。 。 . `follow` 仅在框后重放 `after` ,然后跟踪新输出。 重开一串流 不重新启动执行 。 工作属于认证的链接授权代号; 其它设备即使读取也无法读取 猜测工作编号。 继续使用 `Host.allow` 你的应用操作政策.

该频道选择不提供商,也不进行模型或工具调用. 应用程序从所选运行时间输入输出 装入写作机 以平话和默认方式提供订阅。 标记任意 API 密钥路由 **API 密钥 (按用途计)** 并需要明确选择进入,然后开始。 签名令牌和提供者密钥留在 执行设备:从不将其放入工作输入,帧,ID,出错文本,通知或日志. 用法是一个数字 运行时提供的记录,例如 `{ inputTokens, outputTokens }`;频道从不估计.

主机集成( 通过此主机到 `RelayClient` 如快速启动:

```ts
import { Host, type KeyPair, type PairRequest } from '@byokit/pair';
import { JobChannel, type JobPart, type JobWriter, type JobCursor } from '@byokit/relay';

type Output = Exclude<JobPart, { type: 'end' }>;

async function jobHost(
  keys: KeyPair,
  confirm: (request: PairRequest) => boolean | Promise<boolean>,
  // App adapter to its selected runtime kit; validate input and billing consent before returning output.
  execute: (input: unknown, device: string) => AsyncIterable<Output>,
) {
  const jobs = new JobChannel({ maxJobs: 64, maxFrames: 4096, maxBytes: 8 * 1024 * 1024 });
  async function produce(job: string, device: string, writer: JobWriter, input: unknown) {
    try {
      for await (const part of execute(input, device)) writer.append(part);
      // The adapter emits the runtime's final usage record before completion.
      writer.append({ type: 'end' });
    } catch {
      // Only public words cross the link; never transmit the caught exception.
      try { writer.append({ type: 'end', error: 'Your computer could not finish that.' }); }
      catch { jobs.drop(job, device); } // full history: explicit failure, no silent lost frames
    }
  }
  const host = await Host.open({
    keys, name: 'Your computer', confirm,
    allow: (request, device) => device.role === 'control' && ['job.start', 'job.follow'].includes(request.op),
    handle: (request, device) => {
      const { job, input } = request.args as { job: string; input: unknown };
      const writer = jobs.create(job, device.id); // create once, before any execution
      void produce(job, device.id, writer, input);
      return { job };
    },
    stream: (stream, request, device) => jobs.follow(stream, request.args as JobCursor, device.id),
  });
  return { host, jobs };
}
```

设备以应用程序生成的ID开始一个工作,然后在链接在线时打开一个后继器. 这个例子也 在浏览器或 React 本地操作; 导入读取器 `@byokit/relay/device`,避开节点服务器条目:

```ts
import type { DeviceLink } from '@byokit/pair';
import { readJobStream, type JobFrame } from '@byokit/relay/device';

async function startJob(link: DeviceLink, job: string, input: unknown) {
  await link.request('job.start', { job, input });
  return job;
}

async function followJobOnce(
  link: DeviceLink, job: string, after: number,
  // Apply output and persist frame.seq together; skip already-applied seq values on local retry.
  applyAndSave: (frame: JobFrame) => Promise<void>,
) {
  const stream = await link.stream('job.follow', { job, after });
  return readJobStream(stream, { job, after }, async (frame) => {
    await applyAndSave(frame);
    // text: append frame.text; image: frame.data is Uint8Array with frame.mime;
    // usage: save frame.usage; end: finish the job, displaying only the public frame.error if present.
  });
}
```

在套接字丢失时,读者拒绝。 等着 `DeviceLink`是因为 `onStatus('online')`,重新装入最后一个 **应用** 顺序, 打电话 `followJobOnce` ; 序列化这些尝试并处理他们的拒绝. 不要打电话 `job.start` 改为 简历。 链接在同一设备会话内重试一个未解答的启动请求; 无法创建保留 id 两次 如果开始时间模糊不清,则在决定是否开始新工作之前遵循已知的工作编号. 从读取器返回意味着运输干净地结束,包括在完成的工作结束时已经有一个光标. 使用 `end` 在您保存状态中作为任务完成信号的框。 失败文本是应用程序提供的公开文本.

图像有线字段为加密流内的base64url;读者返回原字节. 一个框是 限于 `MAX_JOB_FRAME_BYTES` (1 MiB of UTF-8 JSON,不包括其新线),所以大图像必须作为 订购零件,由应用程序组装. 链接块流独立写; 它的256 KB 窗口仍然提供 后压。 文本边界被保留,即使一个UTF-8字符或帧交叉链接块.

历史在宿主的记忆中 以 `maxJobs`,每个工作 `maxFrames` 总计 `maxBytes`完整历史 拒绝一个不消耗序列的附加; 配置预期输出的限制并处理拒绝 。 日志 不悄悄地驱逐框架。 打电话 `jobs.drop(job, deviceId)` 当您的保留或取消政策允许时 它,在移除该设备时;它释放出内存并结束现有的追随者. 于 `end` 或者说 `drop` 失败。 缺少的工作或超出保留历史的光标会以公有错误结束跟随者.

在主机运行时重新连接恢复工作 `JobChannel` 即使中继器重新启动, 也要活着。 主机进程崩溃 恢复、持久执行、取消基本运行时间、供应商的选择、开具账单的同意、申请 输入验证和交易输出/cursor存储属于嵌入式app或运行时套件. 此频道 不添加数据库、主机队列或当运输重联时执行第二次运行。 接力还是盲的 用于工作标识、框架、图像和使用; 通道没有记录工作证书或内容.

## 提供一台新的计算机

QR需要一个人在两端。 新的计算机(或没有屏幕的云机) 代号:登录的电话薄荷将一个单一用途的代号捆绑在它的账户及其链接密钥上,以及计算机 当它第一次拨出。 中继器将新主机绑定在该账户和密钥上; 计算机的应用授权 电话在账户下找到电脑并连接 通过中继器。 中继路线将噪声框封住,没有秘密:只是主张的散文 并且捆绑的钥匙是公共材料.

```ts
import { DeviceLink, Host, b64url, keyPair } from '@byokit/pair';
import { RelayClient, claimProvision, linkUrl, ownerClient } from '@byokit/relay';

const ownerToken = process.env.RELAY_OWNER_TOKEN ?? '';
// The signed-in phone mints the token and shows it once for the new computer. Five minutes, one use.
const owner = ownerClient('https://relay.example', ownerToken);
const phoneKeys = keyPair();  // the phone keeps these in secure storage
const { token } = await owner.enrolment({ name: 'Cloud machine',
  provision: { account: 'umer', deviceKey: b64url(phoneKeys.publicKey) } });

// The computer dials out to the hosted relay and presents the token: no QR, no approval tap.
const computer = await Host.open({ keys: keyPair(), name: 'Cloud machine',
  confirm: () => false, handle: (req) => ({ pong: req.op }) });
const dialled = new RelayClient(computer, { url: 'wss://relay.example/relay/v1/host', enrol: token });
await new Promise<void>((resolve) => {
  const t = setInterval(() => { if (dialled.status === 'online') { clearInterval(t); resolve(); } }, 50);
});
const granted = await claimProvision(computer, dialled.provision);

// The phone finds its computer under its account and connects through the relay.
const mine = (await owner.hosts({ account: 'umer' })).find((h) => h.id === computer.id);
const phone = new DeviceLink({ v: 1, secretKey: b64url(phoneKeys.secretKey), host: b64url(computer.keys.publicKey),
  hostName: 'Cloud machine', urls: [linkUrl('https://relay.example', mine!.id)],
  device: { id: granted!.id, name: granted!.name, role: granted!.role } });
```

拨出时拒绝使用过期、再利用或撤回的代币(`refused`这把钥匙不是受约束的 `not-paired` 在链接中,主机不会被其他账户所忽略。 在使用前撤回一个符号 与 `owner.revokeEnrolment(id)` (该部分在 `.`),并随后撤销宿主 `owner.revoke(id)` 和往常一样 一个信物通过一个频道 其所有者信任的, 从来没有记录, 并且没有给予任何东西 除了这个 电脑和这个手机钥匙.

## 主机继电器

`packages/relay/hosted/` 运行一台中继计算机拨出:`Dockerfile`),一个小型服务器 (单位:千美元)`server.mjs`)和一个配置文件(`config.example.json`) (中文(简体) ). 一个复制品,在TLS后面 它不会终止自己 (Tailscale Service, Caddy,.),在挂载音量上的状态文件:

```sh
cp packages/relay/hosted/config.example.json /srv/relay/config.json
printf '%s' "$RELAY_OWNER_TOKEN" > /srv/relay/secrets/owner-token
chmod 600 /srv/relay/secrets/owner-token /srv/relay/config.json
docker build -t hosted-relay packages/relay/hosted
docker run -d --name relay --restart unless-stopped -p 127.0.0.1:7300:7300 -v /srv/relay:/srv/relay hosted-relay
curl http://127.0.0.1:7300/health   # {"ok":true,"hosts":0,"devices":0}
```

卫生是 `GET /health`数字 : `200 { ok, hosts, devices }`里面没有秘密 因为集装箱是自己的 `HEALTHCHECK` 和负载平衡器。 限制是继电器每分钟的地址(`LIMITS`: 连接, 代码查询、动作、入学申请、失败证明),加上只注册注册和0600状态文件;设置 `trustProxy: true` 你只跑到代理机后面,所以.. `X-Forwarded-For` 地址归你 BYOK 它没有操作 主机: 运行在您已有的基础设施上 .

## 常见问题

### @byokit/relay 是什么？

TypeScript套件让人们通过他们已经支付的人工智能计划,在网络,电话和桌面上登录到你的应用程序\.

### 如何安装 @byokit/relay？

在项目目录运行 npm install @byokit/relay，或使用 pnpm add / yarn add / bun add。

### @byokit/relay 支持 TypeScript 吗？

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

### @byokit/relay 支持 ESM 还是 CommonJS？

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

### @byokit/relay 需要什么版本的 Node\.js？

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

### @byokit/relay 可以商用吗？

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

### @byokit/relay 的下载量有多少？

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

### @byokit/relay 还在维护吗？

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

