TypeScript Client SDK 参考
July 15, 2026 · View on GitHub
hotplex-client— HotPlex Gateway 的官方 TypeScript 客户端,基于wsWebSocket 和eventemitter3实现 AEP v1 全双工协议。
SDK 位置:Go SDK 位于仓库根目录的
client/(独立 Go module),Python / TypeScript / Java SDK 位于examples/目录。
安装
npm install hotplex-client
# 或从 examples/typescript-client/ 本地引用
依赖:ws(WebSocket 客户端)、eventemitter3(类型安全事件分发)。
快速开始
import { HotPlexClient, WorkerType } from 'hotplex-client'
const client = new HotPlexClient({
url: 'ws://localhost:8888/ws',
workerType: WorkerType.ClaudeCode,
apiKey: process.env.HOTPLEX_API_KEY,
})
// 流式输出
client.on('delta', (data) => process.stdout.write(data.content))
// 任务完成
client.on('done', (data) => {
console.log('\n--- done ---', 'success:', data.success)
client.disconnect()
})
// 错误处理
client.on('error', (data) => {
console.error('error:', data.code, data.message)
client.disconnect()
})
// 连接并发送任务
const ack = await client.connect()
console.log('session:', ack.session_id)
await client.sendInputAsync('用 TypeScript 写一个 REST API 示例')
核心 API
创建客户端
const client = new HotPlexClient({
url: 'ws://localhost:8888/ws', // 必填:Gateway WebSocket 地址
workerType: WorkerType.ClaudeCode, // 必填:Worker 类型
apiKey: 'ak-xxx', // X-API-Key header(可选)
authToken: 'your-api-key', // 延迟浏览器认证(可选)
reconnect: {
enabled: true, // 启用自动重连(默认 true)
maxAttempts: 10, // 最大重连次数(默认 10)
baseDelayMs: 1000, // 退避基数 1s
maxDelayMs: 60000, // 退避上限 60s
},
heartbeat: {
pingIntervalMs: 54000, // 心跳间隔(默认 54s)
pongTimeoutMs: 60000, // Pong 超时(默认 60s)
maxMissedPongs: 3, // 最大丢失 Pong 数
},
})
连接与会话
// 新建 session
const ack = await client.connect()
console.log(ack.session_id, ack.state, ack.server_caps)
// ack.session_id — 服务端分配的 session ID
// ack.state — 初始状态(created / running)
// ack.server_caps — 服务端能力集
// 恢复已有 session
const ack2 = await client.resume('sess_xxxx')
// 断开连接
client.disconnect() // 或 client.close()
只读属性
| 属性 | 类型 | 说明 |
|---|---|---|
sessionId | string | null | 当前 session ID |
state | SessionState | 当前 session 状态 |
connected | boolean | 是否已连接 |
reconnecting | boolean | 是否正在重连 |
发送方法
| 方法 | 用途 |
|---|---|
sendInput(content, metadata?) | 发送用户输入(fire-and-forget) |
sendInputAsync(content, metadata?) | 发送并等待 done/error(Promise,超时 10 分钟) |
sendPermissionResponse(id, allowed, reason?) | 响应权限请求 |
sendToolResult(id, output, error?) | 返回工具执行结果 |
sendControl(action) | 发送控制指令 |
terminate() | 终止 session(等价于 sendControl(ControlAction.Terminate)) |
delete() | 删除 session |
事件处理
HotPlexClient 继承 EventEmitter<HotPlexClientEvents>,所有事件回调参数类型安全:
// 流式内容
client.on('delta', (data: MessageDeltaData, env: Envelope) => {
process.stdout.write(data.content)
})
// 完整消息
client.on('message', (data: MessageData, env: Envelope) => {
console.log(data.role, data.content)
})
// 流式生命周期
client.on('messageStart', (data, env) => { /* data.id, data.role, data.content_type */ })
client.on('messageEnd', (data, env) => { /* data.message_id */ })
// 工具调用
client.on('toolCall', (data: ToolCallData, env: Envelope) => {
console.log('tool:', data.name, data.input)
})
client.on('toolResult', (data: ToolResultData, env: Envelope) => {
if (data.error) console.error('tool error:', data.error)
})
// Agent 推理
client.on('reasoning', (data, env) => { /* data.content */ })
// 执行步骤
client.on('step', (data, env) => { /* data.id, data.step_type */ })
// 任务完成
client.on('done', (data: DoneData, env: Envelope) => {
console.log('success:', data.success, 'dropped:', data.dropped)
if (data.stats) {
console.log('tokens:', data.stats.total_tokens)
console.log('cost: $', data.stats.cost_usd)
}
})
// 错误
client.on('error', (data: ErrorData, env: Envelope) => {
console.error('error:', data.code, data.message)
})
// 状态变更
client.on('state', (data: StateData, env: Envelope) => {
console.log('state ->', data.state)
})
// 权限请求
client.on('permissionRequest', (data: PermissionRequestData, env: Envelope) => {
client.sendPermissionResponse(data.id, true) // 自动批准
})
// 心跳响应
client.on('pong', (data: PongData, env: Envelope) => { /* data.state */ })
生命周期事件
// 连接成功(含重连成功)
client.on('connected', (ack: InitAckData) => {
console.log('session:', ack.session_id, 'worker:', ack.server_caps.worker_type)
})
// 连接断开
client.on('disconnected', (reason: string) => {
console.log('disconnected:', reason)
})
// 正在重连
client.on('reconnecting', (attempt: number) => {
console.log(`reconnecting... attempt ${attempt}`)
})
// 服务端发起重连指令
client.on('reconnect', (data: ControlData, env: Envelope) => {
console.log('server requested reconnect:', data.reason)
})
// Session 失效
client.on('sessionInvalid', (data: ControlData, env: Envelope) => {
console.log('session invalid, recoverable:', data.recoverable)
})
// 限流通知
client.on('throttle', (data: ControlData, env: Envelope) => {
console.log('throttled:', data.suggestion?.backoff_ms, 'ms backoff')
})
事件类型
AEP v1 事件类型一览
| EventKind | Data 类型 | 方向 | 说明 |
|---|---|---|---|
init | InitData | C→S | 握手初始化 |
init_ack | InitAckData | S→C | 握手响应 |
error | ErrorData | S→C | 错误通知 |
state | StateData | S→C | Session 状态变更 |
input | InputData | C→S | 用户输入 |
message.start | MessageStartData | S→C | 流式消息开始 |
message.delta | MessageDeltaData | S→C | 流式内容片段 |
message.end | MessageEndData | S→C | 流式消息结束 |
message | MessageData | S→C | 完整消息 |
tool_call | ToolCallData | S→C | Worker 调用工具 |
tool_result | ToolResultData | S→C | 工具执行结果 |
reasoning | ReasoningData | S→C | Agent 推理过程 |
step | StepData | S→C | 执行步骤标记 |
done | DoneData | S→C | 任务完成 |
permission_request | PermissionRequestData | S→C | 请求用户授权 |
control | ControlData | S→C | 服务端控制指令 |
pong | PongData | S→C | 心跳响应 |
注意 1:上表中部分事件类型(如
question_request、question_response、elicitation_request、elicitation_response、context_usage、skills_list、mcp_status、worker_command)在 AEP 协议中定义,但 TS SDKconstants.ts中未导出对应的EventKind常量。处理这些事件时需使用字符串形式匹配event.type字段。
注意 2:
control事件的reset和gcaction 在 AEP 协议中定义(Client → Server),但 TS SDK 的ControlAction常量中未导出这两个值。如需发送reset/gc指令,请直接使用字符串client.sendControl('reset')/client.sendControl('gc')。
Session 状态
import { SessionState } from 'hotplex-client'
// SessionState.Created = 'created'
// SessionState.Running = 'running'
// SessionState.Idle = 'idle'
// SessionState.Terminated = 'terminated'
// SessionState.Deleted = 'deleted'
自动重连
启用 reconnect.enabled 后,SDK 自动处理以下断连场景:
- 触发条件:WebSocket 关闭、服务端
control.reconnect指令 - 退避算法:指数退避 + 10% 随机抖动(base 1s → max 60s)
- 最大重试:10 次(可通过
reconnect.maxAttempts配置) - 停止条件:客户端主动
disconnect()、达到最大重试次数、服务端发送session_invalid/delete
client.on('reconnecting', (attempt) => {
console.log(`reconnect attempt ${attempt}`)
})
SESSION_BUSY 自动重试
Gateway 返回 SESSION_BUSY 错误时,SDK 自动延迟 500ms 重发 input,无需手动处理。此行为仅在 sendInput 发出的消息中生效。
SESSION_ALREADY_CONNECTED 与 SESSION_BUSY 不同:它拒绝整个 WebSocket
连接,客户端必须停止自动重连,待原 owner 关闭后由用户显式重试连接。
错误处理
错误类层级
HotPlexError (base)
├── ConnectionError — WebSocket 连接失败
├── SessionError — Session 相关错误(含 code)
├── TimeoutError — 操作超时
└── ProtocolError — 协议违规(含 code)
错误码
import { ErrorCode } from 'hotplex-client'
client.on('error', (data) => {
switch (data.code) {
case ErrorCode.SessionBusy:
// Session 忙,SDK 自动重试
break
case ErrorCode.SessionAlreadyConnected:
// 当前连接不可用;等待原连接关闭后由用户显式重试
break
case ErrorCode.Unauthorized:
// Token 过期
break
case ErrorCode.WorkerCrash:
// Worker 崩溃
break
case ErrorCode.SessionNotFound:
// Session 不存在
break
default:
console.error(`[${data.code}] ${data.message}`)
}
})
Envelope 工具函数
SDK 导出一组低级 Envelope 操作函数,供高级用户自定义协议处理:
import {
newEventId, // 生成 evt_xxx ID
newSessionId, // 生成 sess_xxx ID
createEnvelope, // 通用 Envelope 构造
createInitEnvelope, // init 握手 Envelope
createInputEnvelope, // 用户输入 Envelope
createPingEnvelope, // 心跳 Envelope
createControlEnvelope, // 控制指令 Envelope
createPermissionResponseEnvelope, // 权限响应 Envelope
serializeEnvelope, // Envelope → NDJSON 字符串
deserializeEnvelope, // NDJSON → Envelope
isInitAck, // 类型守卫
isError,
isState,
isDone,
isDelta,
isControl,
} from 'hotplex-client'
完整示例
权限处理与流式输出
import { HotPlexClient, WorkerType, ErrorCode } from 'hotplex-client'
const client = new HotPlexClient({
url: 'ws://localhost:8888/ws',
workerType: WorkerType.ClaudeCode,
authToken: process.env.HOTPLEX_TOKEN,
reconnect: { enabled: true, maxAttempts: 5 },
})
// 安全工具列表,自动批准
const SAFE_TOOLS = ['read_file', 'grep', 'glob', 'bash']
client.on('delta', (data) => process.stdout.write(data.content))
client.on('toolCall', (data) => {
console.log(`\n[tool call: ${data.name}]`)
})
client.on('permissionRequest', (data) => {
const approved = SAFE_TOOLS.includes(data.tool_name)
console.log(`\n ${approved ? 'Approved' : 'Denied'}: ${data.tool_name}`)
client.sendPermissionResponse(data.id, approved, approved ? '' : 'Not in allowlist')
})
client.on('done', (data) => {
console.log('\n--- Task completed ---')
if (data.stats) {
console.log(` Duration: ${data.stats.duration_ms}ms`)
console.log(` Tokens: ${data.stats.total_tokens}`)
console.log(` Cost: $${data.stats.cost_usd}`)
}
client.disconnect()
})
client.on('error', (data) => {
if (data.code === ErrorCode.SessionBusy) return // 自动重试
console.error(`Error: [${data.code}] ${data.message}`)
client.disconnect()
})
// 连接并发送任务
const ack = await client.connect()
console.log(`Session: ${ack.session_id}\n`)
await client.sendInputAsync('Read go.mod and list all dependencies')
Session 恢复
// 首次连接,保存 session ID
const ack = await client.connect()
const sessionId = ack.session_id
// ... 使用 session ...
// 断开后恢复
client.disconnect()
// 稍后使用同一 session ID 恢复
const ack2 = await client.resume(sessionId)
console.log('Resumed session:', ack2.session_id)