npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@telepat/otto-sdk

v0.16.0

Published

TypeScript SDK for integrating 3rd-party JavaScript apps as Otto controllers

Readme

@telepat/otto-sdk

用于将第三方 JavaScript 应用程序集成为 Otto 控制器的 TypeScript SDK。通过简洁、类型安全的 API 控制浏览器节点、执行命令、流式传输监听器更新并管理权限。

English | 中文

安装

npm install @telepat/otto-sdk

需要 Node.js 22+ 或任何支持原生 fetchWebSocket 的边缘运行时(Cloudflare Workers、Deno 等)。

快速开始

import { OttoClient } from '@telepat/otto-sdk';

// 使用中继 URL 和控制器凭据创建客户端
const client = new OttoClient({
  relayUrl: 'wss://relay.example.com',
  clientId: 'clt_xxxxxxxxxx',
  clientSecret: 'cs_yyyyyyyyyyyyyyyyyy',
});

// 连接到中继
await client.connect();

// 列出您有权访问的已连接节点
const nodes = await client.nodes.list();
console.log(nodes);

// 在节点上执行命令
const result = await client.commands.run({
  nodeId: nodes[0].nodeId,
  site: 'reddit.com',
  command: 'getFeed',
  input: { subreddit: 'typescript', limit: 10 },
});
console.log(result);

// 流式传输监听器更新
const stream = client.listeners.subscribe({
  nodeId: nodes[0].nodeId,
  listener: 'network.http_intercept',
  options: { site: 'example.com' },
});

// 使用异步迭代
for await (const event of stream) {
  console.log('已接收:', event);
}

// 完成后断开连接
await client.disconnect();

API 参考

OttoClient

与 Otto 中继交互的主要 SDK 类。

构造函数

new OttoClient(options: {
  relayUrl: string;        // WebSocket URL(例如 'wss://relay.example.com')
  clientId: string;        // 控制器客户端 ID(来自中继注册)
  clientSecret: string;    // 控制器客户端密钥(来自中继注册)
})

方法

connect(): Promise<void>

与中继建立连接并进行身份验证。如果未连接,将在第一次 API 调用时自动调用。

await client.connect();
disconnect(): Promise<void>

优雅地关闭 WebSocket 连接。

await client.disconnect();
isConnected(): boolean

如果 WebSocket 已打开并通过身份验证,则返回 true

if (!client.isConnected()) {
  await client.connect();
}

client.nodes

节点列表与管理。

list(): Promise<Node[]>

返回您已获得 ACL 授权且当前已连接到中继的所有节点。

const nodes = await client.nodes.list();
// [{ nodeId: 'node_xyz' }, ...]

返回类型:

interface Node {
  nodeId: string;
}

client.commands

在节点上执行命令。

list(options: { nodeId: string }): Promise<CommandDescriptor[]>

列出节点上所有可用的命令。

const commands = await client.commands.list({ nodeId: 'node_xyz' });

返回类型:

interface CommandDescriptor {
  site: string;           // 目标域名(例如 'reddit.com')
  id: string;             // 命令唯一标识符
  displayName: string;    // 人类可读名称
  description: string;    // 命令功能描述
  tags: string[];         // 可搜索标签
  requiresAuth: boolean;  // 是否需要用户登录该网站
  inputFields?: CommandInputFieldDescriptor[];
}

run(options): Promise<CommandResult>

在节点上执行命令并等待结果。

const result = await client.commands.run({
  nodeId: 'node_xyz',
  site: 'reddit.com',
  command: 'getFeed',
  input: { subreddit: 'typescript', limit: 10 },
  timeoutMs: 30000,
});

选项:

| 字段 | 类型 | 必填 | 说明 | |---|---|---|---| | nodeId | string | ✓ | 目标节点 ID | | site | string | ✓ | 目标站点域名(例如 'reddit.com') | | command | string | ✓ | 命令标识符(例如 'getFeed') | | input | Record<string, unknown> | | 命令输入参数 | | timeoutMs | number | | 最大等待时间(毫秒),默认 30000 |

返回类型:

interface CommandResult {
  ok: boolean;
  data?: unknown;         // 命令输出
  commandOutcome: 'completed' | 'failed' | 'timed_out' | 'cancelled';
  durationMs: number;     // 执行时间(毫秒)
  error?: string;         // 失败时的错误信息
}

抛出异常:

  • OttoCommandError — 命令失败、超时或被取消
  • OttoTimeoutError — SDK 层级超时(在 timeoutMs 内未收到响应)

client.listeners

实时事件的流式监听器订阅。

subscribe(options): StreamSession

订阅节点上的监听器流。返回 StreamSession,该对象同时实现 AsyncIterableEventEmitter 接口。

const stream = client.listeners.subscribe({
  nodeId: 'node_xyz',
  listener: 'network.http_intercept',
  options: { site: 'reddit.com' },
});

StreamSession API:

异步迭代:

for await (const event of stream) {
  console.log(event.type);   // 'listener_update'
  console.log(event.data);   // 事件载荷
}

EventEmitter 风格:

stream.on('data', (event) => { console.log(event.data); });
stream.on('error', (error) => { console.error(error); });
stream.on('end', () => { console.log('流已结束'); });

取消订阅:

await stream.unsubscribe();

client.pairing

节点配对工作流。

listPending(): Promise<PairingChallenge[]>

列出等待批准的待处理配对挑战。

const pending = await client.pairing.listPending();
for (const challenge of pending) {
  console.log(challenge.code, challenge.nodeId);
}

返回类型:

interface PairingChallenge {
  challengeId: string;
  code: string;                              // 6 位批准码
  nodeId: string;
  status: 'pending' | 'approved' | 'expired';
  expiresAt: string;                         // ISO 时间戳
}

approve(options: { code: string }): Promise<void>

通过 6 位配对码批准配对挑战。

await client.pairing.approve({ code: '123456' });

错误处理

SDK 提供特定的错误类型以便更好地处理错误:

import { OttoError, OttoAuthError, OttoTimeoutError, OttoCommandError } from '@telepat/otto-sdk';

try {
  const result = await client.commands.run({
    nodeId: 'node_xyz',
    site: 'reddit.com',
    command: 'getFeed',
  });
} catch (error) {
  if (error instanceof OttoAuthError) {
    console.error('身份验证失败 — 请检查 clientId 和 clientSecret');
  } else if (error instanceof OttoTimeoutError) {
    console.error('命令超时 — 节点未在规定时间内响应');
  } else if (error instanceof OttoCommandError) {
    console.error('命令执行失败:', error.message);
    console.error('结果状态:', error.commandOutcome); // 'failed' | 'timed_out' | 'cancelled'
  } else if (error instanceof OttoError) {
    console.error('中继错误:', error.message);
  } else {
    throw error;
  }
}

错误类层次结构

OttoError          ← 所有 SDK 错误的基类
├── OttoAuthError  ← 凭据无效或令牌已撤销
├── OttoTimeoutError ← 未在超时时间内收到响应
└── OttoCommandError ← 命令以非成功状态结束(包含 commandOutcome 属性)

边缘运行时兼容性

SDK 仅使用原生 API,无 Node.js 特定模块,可在以下环境中运行:

  • Cloudflare Workers
  • Deno
  • Bun
  • 任何支持原生 fetchWebSocket 的运行时
// Cloudflare Workers 示例
import { OttoClient } from '@telepat/otto-sdk';

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const client = new OttoClient({
      relayUrl: env.OTTO_RELAY_URL,
      clientId: env.OTTO_CLIENT_ID,
      clientSecret: env.OTTO_CLIENT_SECRET,
    });

    await client.connect();

    try {
      const nodes = await client.nodes.list();
      if (nodes.length === 0) {
        return Response.json({ error: '没有可用节点' }, { status: 503 });
      }

      const result = await client.commands.run({
        nodeId: nodes[0].nodeId,
        site: 'reddit.com',
        command: 'getFeed',
        input: { subreddit: 'typescript', limit: 25 },
      });

      return Response.json(result.data);
    } finally {
      await client.disconnect();
    }
  },
};

中继注册

使用此 SDK 前,必须先在中继中注册控制器客户端:

通过 Otto CLI(推荐):

otto client register --name "我的应用"

此命令会输出 clientIdclientSecret,请妥善保存,中继不存储密钥明文,无法恢复。

通过 HTTP(需要中继设置 OTTO_ALLOW_REMOTE_CONTROLLER_REGISTRATION=1):

curl -X POST http://localhost:8787/api/controller/register \
  -H 'Content-Type: application/json' \
  -d '{"name": "我的应用", "description": "自动化工作进程"}'
{
  "clientId": "clt_abc123",
  "clientSecret": "cs_xxxxxxxxxxxx",
  "createdAt": 1776162000000
}

类型导出

所有公共类型均从包根部导出:

import type {
  Node,
  CommandDescriptor,
  CommandInputFieldDescriptor,
  CommandResult,
  PairingChallenge,
  ListenerUpdateEvent,
} from '@telepat/otto-sdk';

许可证

MIT