Go Client SDK 参考
July 15, 2026 · View on GitHub
github.com/hrygo/hotplex/client— HotPlex Gateway 的官方 Go 客户端,实现 AEP v1 WebSocket 全双工协议。
SDK 位置:Go SDK 位于仓库根目录的
client/(独立 Go module),Python / TypeScript / Java SDK 位于examples/目录。
安装
go get github.com/hrygo/hotplex/client
依赖:gorilla/websocket(WebSocket)。
快速开始
package main
import (
"context"
"fmt"
"log"
"os"
"os/signal"
"syscall"
"github.com/hrygo/hotplex/client"
)
func main() {
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
c, err := client.New(ctx,
client.URL("ws://localhost:8888"),
client.WorkerType("claude_code"),
client.APIKey(os.Getenv("HOTPLEX_API_KEY")),
client.AutoReconnect(true),
)
if err != nil {
log.Fatal(err)
}
defer c.Close()
ack, err := c.Connect(ctx)
if err != nil {
log.Fatal(err)
}
fmt.Printf("session: %s, state: %s\n", ack.SessionID, ack.State)
ch := c.Events()
defer c.Unsubscribe(ch)
if err := c.SendInput(ctx, "用 Go 写一个 hello world"); err != nil {
log.Fatal(err)
}
for evt := range ch {
switch evt.Type {
case client.EventMessageDelta:
if d, ok := evt.AsMessageDeltaData(); ok {
fmt.Print(d.Content)
}
case client.EventDone:
return
case client.EventError:
if d, ok := evt.AsErrorData(); ok {
fmt.Fprintf(os.Stderr, "error: %s: %s\n", d.Code, d.Message)
}
return
}
}
}
核心 API
创建客户端
使用 Functional Options 模式。URL 和 WorkerType 为必填项:
c, err := client.New(ctx,
client.URL("ws://localhost:8888"), // 必填:Gateway WebSocket 地址
client.WorkerType("claude_code"), // 必填:Worker 类型
client.BotID("bot-123"), // Bot ID for multi-bot setups
client.APIKey("ak-xxx"), // X-API-Key header(可选)
client.AutoReconnect(true), // 启用指数退避自动重连
client.PingInterval(54*time.Second), // 心跳间隔(默认 54s)
client.ClientSessionID("my-stable-id"), // 确定性 session ID
client.Metadata(map[string]any{"env": "test"}), // Init 附加元数据
client.Logger(slog.Default()), // 自定义 slog 日志
)
连接与会话
// 新建连接 — 发送 init 握手,返回 InitAckData
ack, err := c.Connect(ctx)
// ack.SessionID — 服务端分配的 session ID(sess_xxx 格式)
// ack.State — 初始状态(created / running)
// ack.ServerCaps — 服务端能力集
// 恢复已有 session — 跳过 worker 冷启动
ack, err := c.Resume(ctx, "sess_xxxx")
// 查询运行时状态
c.SessionID() // 当前 session ID
c.State() // 当前 SessionState
发送方法
| 方法 | 事件类型 | Priority | 说明 |
|---|---|---|---|
SendInput(ctx, content, metadata?) | input | data | 发送用户输入 |
SendInputAsync(ctx, content, metadata?) | input → 阻塞等待 | — | 发送并等待 done/error,返回 *DoneData |
SendPermissionResponse(ctx, id, approved, reason) | permission_response | control | 响应工具权限请求 |
SendQuestionResponse(ctx, id, answers) | question_response | control | 响应问题请求 |
SendElicitationResponse(ctx, id, action, content) | elicitation_response | control | 响应 MCP 触发请求 |
SendControl(ctx, action) | control | control | 发送控制指令(terminate / delete) |
SendReset(ctx, reason) | control | control | 清空上下文,Worker 自行决定 in-place 或重启 |
SendGC(ctx, reason) | control | control | 归档会话,Worker 终止但保留历史 |
事件订阅
Events() 返回 <-chan Event,支持多 listener。使用 Unsubscribe(ch) 停止接收并释放资源:
ch := c.Events()
defer c.Unsubscribe(ch)
for evt := range ch {
switch evt.Type {
case client.EventMessageDelta:
if d, ok := evt.AsMessageDeltaData(); ok {
fmt.Print(d.Content)
}
case client.EventPermissionRequest:
if d, ok := evt.AsPermissionRequestData(); ok {
// 自动批准
c.SendPermissionResponse(ctx, d.ID, true, "")
}
case client.EventQuestionRequest:
if d, ok := evt.AsQuestionRequestData(); ok {
answers := map[string]string{
d.Questions[0].Question: d.Questions[0].Options[0].Label,
}
c.SendQuestionResponse(ctx, d.ID, answers)
}
case client.EventState:
// recvPump 自动同步 c.State(),此处可选记录日志
}
}
背压策略:done/error/state 阻塞投递(永不丢弃),message.delta/raw 在通道满时静默丢弃。done 事件通过 Dropped 字段指示是否有 delta 被丢弃。
连接生命周期
Connect 成功后启动 3 个后台 goroutine:
| Goroutine | 职责 |
|---|---|
recvPump | 循环 NextReader() → aep.DecodeLine → deliver 到所有 listener |
sendPump | 循环 range sendCh → WriteMessage(带背压) |
pingPump | 定时发送 WebSocket Ping(默认 54s 间隔) |
关闭顺序:cancel ctx → close ws → close sendCh → wg.Wait → close listeners。
事件类型
AEP v1 事件类型一览
注意:上表列出的是 Go SDK 中导出了
Kind常量的事件类型。部分 AEP 协议事件(如context_usage、mcp_status、skills_list、worker_command、question_request、question_response、elicitation_request、elicitation_response)在 Go SDK 中未导出Kind常量。处理这些事件时需使用字符串形式匹配evt.Type == "context_usage"等。
| Kind | 方向 | Go Data 类型 | 说明 |
|---|---|---|---|
init | C→S | map[string]any | 握手初始化 |
init_ack | S→C | InitAckData | 握手响应 |
error | S→C | ErrorData | 错误通知 |
state | S→C | StateData | Session 状态变更 |
input | C→S | — | 用户输入 |
message.start | S→C | MessageStartData | 流式消息开始 |
message.delta | S→C | MessageDeltaData | 流式内容片段 |
message.end | S→C | MessageEndData | 流式消息结束 |
message | S→C | MessageData | 完整消息(非流式) |
tool_call | S→C | ToolCallData | Worker 调用工具 |
tool_result | S→C | ToolResultData | 工具执行结果 |
reasoning | S→C | ReasoningData | Agent 推理/思考过程 |
step | S→C | StepData | 执行步骤标记 |
done | S→C | DoneData | 任务完成(Turn 终止符) |
permission_request | S→C | PermissionRequestData | 请求用户授权 |
permission_response | C→S | PermissionResponseData | 用户授权/拒绝 |
question_request | S→C | QuestionRequestData | 请求用户回答问题 |
question_response | C→S | QuestionResponseData | 用户回答 |
elicitation_request | S→C | ElicitationRequestData | MCP 请求用户输入 |
elicitation_response | C→S | ElicitationResponseData | MCP 用户响应 |
control | 双向 | ControlData | 控制指令 |
ping / pong | 双向 | — | 心跳 |
Session 状态机
created → running ⇄ idle → terminated → deleted
↘ terminated ↗ running (resume)
状态通过 client.StateXxx 常量访问(StateCreated / StateRunning / StateIdle / StateTerminated / StateDeleted)。
类型安全的 Event 解析
每个 Event 提供对应的 AsXxxData() 方法,返回 (T, bool):
d, ok := evt.AsMessageDeltaData() // MessageDeltaData
d, ok := evt.AsDoneData() // DoneData
d, ok := evt.AsErrorData() // ErrorData
d, ok := evt.AsToolCallData() // ToolCallData
d, ok := evt.AsPermissionRequestData() // PermissionRequestData
d, ok := evt.AsQuestionRequestData() // QuestionRequestData
d, ok := evt.AsElicitationRequestData() // ElicitationRequestData
d, ok := evt.AsStateData() // StateData
d, ok := evt.AsReasoningData() // ReasoningData
d, ok := evt.AsStepData() // StepData
d, ok := evt.AsMessageStartData() // MessageStartData
d, ok := evt.AsMessageEndData() // MessageEndData
d, ok := evt.AsInitAckData() // InitAckData
错误处理
错误码常量
SDK 导出 client.ErrCodeXxx 常量,与网关错误码一一对应:
| 常量 | 值 | 含义 |
|---|---|---|
ErrCodeSessionBusy | SESSION_BUSY | Session 正忙,稍后重试 |
ErrCodeSessionAlreadyConnected | SESSION_ALREADY_CONNECTED | 已有 WebChat 连接;关闭原连接后由用户显式重试,不能自动重连 |
ErrCodeInternalError | INTERNAL_ERROR | 网关内部错误 |
ErrCodeUnauthorized | UNAUTHORIZED | 认证失败 |
ErrCodeSessionNotFound | SESSION_NOT_FOUND | Session 不存在 |
完整错误码列表参见 pkg/events/events.go 中的 ErrorCode 定义。
错误处理模式
// 1. 连接错误
ack, err := c.Connect(ctx)
if err != nil {
// err 可能包含 "client: init rejected: CODE: message"
log.Fatal(err)
}
// 2. 发送错误
if err := c.SendInput(ctx, "hello"); err != nil {
if errors.Is(err, client.ErrNotConnected) {
// 未连接
}
}
// 3. 网关错误事件
for evt := range ch {
if evt.Type == client.EventError {
if d, ok := evt.AsErrorData(); ok {
switch d.Code {
case client.ErrCodeSessionBusy:
// Session 忙,可延迟重试
case client.ErrCodeUnauthorized:
// Token 过期,需重新认证
default:
log.Error("gateway error", "code", d.Code, "msg", d.Message)
}
}
}
}
Bot ID(多 Bot 设置)
在多 Bot 环境中,使用 BotID 选项指定目标 Bot:
c, err := client.New(ctx,
client.URL("ws://localhost:8888"),
client.WorkerType("claude_code"),
client.APIKey("ak-xxx"),
client.BotID("bot-123"), // 指定 Bot ID
)
完整示例
权限处理
c, _ := client.New(ctx,
client.URL("ws://localhost:8888"),
client.WorkerType("claude_code"),
client.APIKey("test-api-key"),
client.AutoReconnect(true),
)
defer c.Close()
ack, _ := c.Connect(ctx)
fmt.Printf("Session: %s\n", ack.SessionID)
done := make(chan struct{})
go func() {
defer close(done)
for evt := range c.Events() {
switch evt.Type {
case client.EventMessageDelta:
if d, ok := evt.AsMessageDeltaData(); ok {
fmt.Print(d.Content)
}
case client.EventPermissionRequest:
if d, ok := evt.AsPermissionRequestData(); ok {
// 按工具名策略审批
approved := d.ToolName == "Read" || d.ToolName == "Glob"
c.SendPermissionResponse(ctx, d.ID, approved, "")
}
case client.EventDone:
return
case client.EventError:
if d, ok := evt.AsErrorData(); ok {
fmt.Fprintf(os.Stderr, "Error: %s\n", d.Message)
}
return
}
}
}()
c.SendInput(ctx, "Read the file go.mod and tell me the Go version")
<-done
多轮对话
c, _ := client.New(ctx,
client.URL("ws://localhost:8888"),
client.WorkerType("claude_code"),
client.APIKey("test-api-key"),
)
defer c.Close()
ack, _ := c.Connect(ctx)
ready := make(chan struct{}, 1)
ready <- struct{}{}
go func() {
for evt := range c.Events() {
switch evt.Type {
case client.EventMessageDelta:
if d, ok := evt.AsMessageDeltaData(); ok {
fmt.Print(d.Content)
}
case client.EventState:
if d, ok := evt.AsStateData(); ok && d.State == client.StateIdle {
select { case ready <- struct{}{}: default: }
}
case client.EventDone, client.EventError:
return
}
}
}()
scanner := bufio.NewScanner(os.Stdin)
for {
<-ready
fmt.Print("> ")
if !scanner.Scan() { break }
c.SendInput(ctx, scanner.Text())
}