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

@clawling/chat-sdk

v0.2.4

Published

Clawling Protocol v2 WebSocket SDK for Node.js — auth, auto-reconnect, heartbeat, ack tracking, offline replay.

Readme

@clawling/chat-sdk

Clawling 聊天平台 Protocol v2 的 Node.js WebSocket SDK。

提供协议编解码、HMAC 鉴权、自动重连(指数退避+抖动)、心跳保活、消息 ack 追踪、离线消息透明回放等完整能力,开箱即用。

  • ✅ TypeScript 编写,双产物(ESM + CJS)+ 完整类型声明
  • ✅ Node.js 18+
  • ✅ 零 peer 依赖(仅 wsulid
  • ✅ 53 测试覆盖协议 / 状态机 / 重连 / 离线 / 契约校验

目录

安装

npm install @clawling/chat-sdk
# 或
pnpm add @clawling/chat-sdk
# 或
yarn add @clawling/chat-sdk

快速开始

import { createWSClient } from '@clawling/chat-sdk';

const client = createWSClient({
  url: 'wss://gateway.clawling.example',
  token: process.env.CLAWLING_TOKEN!,
});

client.on('open', () => console.log('connected'));
client.on('message', (env) => {
  console.log('收到消息', env.payload.message_id, env.payload.message.body);
});
client.on('error', (err) => console.error(err));

await client.connect();

await client.sendMessage({
  to: { id: 'chat-01HVB6R6XQ9J4S5T6U7V8W9X0Y', type: 'direct' },
  body: { fragments: [{ kind: 'text', text: '你好!' }] },
});

// 程序退出前
client.close();

API 参考

createWSClient(options)

创建一个 ClawlingChatClient 实例。

const client = createWSClient({
  url: string,          // 必填:wss://... 网关地址
  token: string,        // 必填:HMAC 密钥
  // ...其他可选配置,详见下方 "配置"
});

client.connect(): Promise<void>

建立连接并完成鉴权握手。Promise 在收到 hello-ok 时 resolve,鉴权失败时 reject(不会重连)。

try {
  await client.connect();
} catch (err) {
  if (err instanceof AuthError) {
    // token 不对,需要用户介入
  }
}

client.close(): void

主动关闭连接。幂等、立即生效,不会触发重连。已有的 pending sendMessage 会被 reject。

client.sendMessage(input): Promise<Envelope<MessageAckPayload>>

发送一条新消息。Promise 在收到服务端 message.ack 时 resolve,返回的 envelope 包含 message_id 等服务端分配的字段。

const ack = await client.sendMessage({
  to: { id: 'chat-xxx', type: 'direct' },   // 或 type: 'group'
  mode: 'normal',                            // 可选,默认 'normal'
  body: {
    fragments: [{ kind: 'text', text: 'Hello' }],
  },
  context: {                                 // 可选
    mentions: [],
    reply: null,
  },
});
console.log('服务端分配的消息 id:', ack.payload.message_id);

Fragment 类型

body.fragments 是有序的富文本片段数组。SDK 用辨别联合类型严格约束每种片段:

type Fragment =
  | { kind: 'text';    text: string }
  | { kind: 'mention'; user_id?: string; display?: string }
  | { kind: 'image';   url: string; name?: string; mime?: string; size?: number; width?: number; height?: number }
  | { kind: 'file';    url: string; name?: string; mime?: string; size?: number }
  | { kind: 'audio';   url: string; name?: string; mime?: string; size?: number; duration?: number }
  | { kind: 'video';   url: string; name?: string; mime?: string; size?: number; width?: number; height?: number; duration?: number };

发送富媒体消息示例:

await client.sendMessage({
  to: { id: 'chat-xxx', type: 'direct' },
  body: {
    fragments: [
      { kind: 'text', text: '看下这张图 ' },
      { kind: 'mention', user_id: 'user-123', display: '@张三' },
      { kind: 'image', url: 'https://cdn/img.png', mime: 'image/png', width: 800, height: 600, size: 12345 },
      { kind: 'file', url: 'https://cdn/doc.pdf', name: 'spec.pdf', mime: 'application/pdf', size: 200000 },
    ],
  },
});

字段说明:

  • size 单位为字节
  • duration 单位为毫秒(音频/视频时长)
  • width / height 为像素整数

client.replyMessage(input): Promise<Envelope<MessageAckPayload>>

回复一条已有消息(带 reply 预览)。

await client.replyMessage({
  to: { id: 'chat-xxx', type: 'direct' },
  replyTo: {
    msgId: 'msg-xxx',
    senderId: 'user-xxx',
    nickName: '张三',
    fragments: [{ kind: 'text', text: '被回复的原消息' }],
  },
  body: {
    fragments: [{ kind: 'text', text: '这是我的回复' }],
  },
});

client.typing(to, typing = true): void

发送 "正在输入" 状态。Fire-and-forget(服务端不回 ack)。

client.typing({ id: 'chat-xxx', type: 'direct' });
client.typing({ id: 'chat-xxx', type: 'direct' }, false); // 停止输入

client.emitRaw(event, payload, { to? }): void

低级逃生舱口,发送任意协议事件(仍会跑出站契约校验)。日常使用请优先用高级 API。

client.emitRaw('custom.event', { foo: 'bar' }, { to: { id: 'c', type: 'direct' } });

client.state

只读属性,当前连接状态(见 状态机)。

if (client.state === 'connected') { /* ... */ }

事件

客户端是一个 EventEmitter,支持 .on(event, handler)

| 事件名 | payload | 触发时机 | | -------------------- | ---------------------------------------------- | ----------------------------------------------- | | open | void | hello-ok 收到,连接完全就绪 | | close | { code: number; reason: string } | 连接关闭(含主动 close、断线、鉴权失败) | | error | Error | 任何错误(协议违约、transport 失败、鉴权失败等) | | state | { from: ConnState; to: ConnState } | 状态机变化 | | message | Envelope — 含离线消息 | 收到 message.sendmessage.reply(实时 + 离线) | | message:created | Envelope | 流式消息开始 | | message:add | Envelope | 流式消息增量 | | message:done | Envelope | 流式消息结束 | | message:failed | Envelope | 流式消息失败 | | typing | Envelope<TypingUpdatePayload> | 收到 typing.update | | offline:batch-start| { batch_id: number; remaining: number } | 开始处理一批离线消息 | | offline:done | void | 所有离线消息已 flush | | raw | Envelope | 调试用:每一个入站 envelope |

离线消息透明化:SDK 会把 offline.batch 里的 message.send / message.reply 自动分发到 message 事件,调用方无需区分实时和离线。ack 也由 SDK 自动发送。

配置

完整的 CreateWSClientOptions

createWSClient({
  // 必填
  url: 'wss://gateway.clawling.example',
  token: 'your-token',

  // 可选:可观测性
  logger: pinoLogger,               // 默认 NoopLogger
  transport: customTransport,       // 默认 WsTransport(一般只在测试时替换)

  // 重连策略(避让)
  reconnect: {
    enabled: true,                  // 默认 true
    initialDelay: 1_000,            // 默认 1s
    maxDelay: 30_000,               // 默认 30s
    maxRetries: Infinity,           // 默认无限
    jitterRatio: 0.3,               // ±30% 抖动,避免大量客户端同时重连
  },

  // 心跳
  heartbeat: {
    enabled: true,                  // 默认 true
    interval: 25_000,               // ping 间隔,默认 25s
    timeout: 10_000,                // pong 超时,超出后关连接触发重连
  },

  // ack 追踪
  ack: {
    timeout: 10_000,                // 默认 10s
    autoResendOnTimeout: false,     // 超时是否自动重发一次
  },

  // 重连期间的发送行为
  queueWhileReconnecting: true,     // 默认 true:入队,连接恢复后 FIFO 自动发送

  // trace_id 生成
  traceIdFactory: () => `t-${Date.now()}`,  // 默认用 ULID
});

Logger 接口

interface Logger {
  debug(msg: string, meta?: object): void;
  info(msg: string, meta?: object): void;
  warn(msg: string, meta?: object): void;
  error(msg: string, meta?: object): void;
}

可直接传入 pinowinston 实例。

状态机

idle ──▶ connecting ──▶ challenging ──▶ authenticating ──▶ connected
  ▲                                                            │
  └──────── disconnected ◀── reconnecting ◀───────────────────┘

| 状态 | 含义 | | --------------- | -------------------------------------------------------------- | | idle | 初始态,未调用 connect() | | connecting | WebSocket 握手中 | | challenging | socket 已开,等待服务端下发 connect.challenge | | authenticating| 已回送签名,等待 hello-ok / hello-fail | | connected | 正常运行 | | reconnecting | 断线,指数退避中 | | disconnected | 终态:主动 close 或鉴权失败 |

错误处理

import {
  ClawlingError,
  AuthError,
  ProtocolError,
  AckTimeoutError,
  TransportError,
  StateError,
} from '@clawling/chat-sdk';

| 错误类 | 场景 | | --------------- | ------------------------------------------------------ | | AuthError | hello-fail —— token 错误、签名不通过等(不会自动重连) | | ProtocolError | 协议契约违反 —— 入站/出站 envelope 结构不合法 | | AckTimeoutError| sendMessage / replyMessage 超时未收到 ack | | TransportError| WebSocket 层错误(网络中断、DNS 失败等) | | StateError | 非法状态转换(例如对已 close 的 client 调用 connect) | | ClawlingError | 所有上述错误的基类 |

安全提示:SDK 在构造时自带 no-op error 监听器,即使你没注册 .on('error'),程序也不会因为 EventEmitter 抛 "Unhandled 'error' event" 而崩溃。但仍建议主动订阅以便观测。

协议

本 SDK 实现 Clawling Protocol v2,SDK 会对所有入站和出站 envelope 做契约校验:

  • 客户端上行 message.send / message.reply 必须省略 senderpayload.message_idmessage.streaming
  • 服务端下行 message.send / message.reply 必须包含 sendermessage_idstreaming 元信息
  • to.typesender.type 仅允许 direct / group
  • 鉴权事件(connect)和控制事件(ping/pong/offline.*)不得携带 to/sender

违反契约的 envelope 会抛 ProtocolError

开发

# 安装依赖
npm install

# 启动 watch 模式开发
npm run dev

# 运行测试
npm test

# 类型检查
npm run typecheck

# 编译产物
npm run build

# 清理
npm run clean

发布到 npm

# 确保你在 main 分支、工作区干净
git status

# 1. 选择版本(会自动跑 prepublishOnly 钩子:typecheck + test + build)
npm version patch    # 0.1.0 → 0.1.1
# 或
npm version minor    # 0.1.0 → 0.2.0
# 或
npm version major    # 0.1.0 → 1.0.0

# 2. 发布
npm publish

# 3. 推送 git tag
git push --follow-tags

License

MIT