openclaw-xiaoyou 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +163 -0
- package/config.example.json +35 -0
- package/demo-enterprise-server.ts +138 -0
- package/docs/Xiaoyou-SKILL.md +111 -0
- package/docs/communication-flow.md +639 -0
- package/docs/install-xiaoyou.sh +427 -0
- package/docs/publish-and-deploy.md +296 -0
- package/index.ts +70 -0
- package/openclaw.plugin.json +74 -0
- package/package.json +34 -0
- package/setup-entry.ts +18 -0
- package/src/channel.test.ts +140 -0
- package/src/channel.ts +218 -0
- package/src/enterprise-client.ts +190 -0
- package/src/onboarding.ts +76 -0
- package/src/types.ts +86 -0
- package/tsconfig.json +17 -0
|
@@ -0,0 +1,639 @@
|
|
|
1
|
+
# OpenClaw xiaoyou 通信架构与流程文档
|
|
2
|
+
|
|
3
|
+
## 1. 系统总览
|
|
4
|
+
|
|
5
|
+
xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 WebSocket 出站连接,
|
|
6
|
+
企业侧无需暴露公网端口,天然适合内网部署。
|
|
7
|
+
|
|
8
|
+
三个参与方:
|
|
9
|
+
|
|
10
|
+
| 角色 | 说明 |
|
|
11
|
+
|------|------|
|
|
12
|
+
| **企业服务** | 企业内部 IM / 业务系统,提供 WebSocket Server |
|
|
13
|
+
| **xiaoyou 插件** | 运行在 OpenClaw 进程内的 channel 插件,桥接两端 |
|
|
14
|
+
| **OpenClaw Gateway** | AI Agent 运行时,负责 LLM 调用、工具执行、会话管理 |
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## 2. 整体架构图
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
21
|
+
│ 企业内网 │
|
|
22
|
+
│ │
|
|
23
|
+
│ ┌──────────────┐ ┌────────────────────────┐ │
|
|
24
|
+
│ │ │ WebSocket (持久连接) │ │ │
|
|
25
|
+
│ │ 企业 IM │◀═══════════════════════════▶│ OpenClaw Gateway │ │
|
|
26
|
+
│ │ 服务端 │ wss://im.corp.com/ws │ (port 18789) │ │
|
|
27
|
+
│ │ │ │ │ │
|
|
28
|
+
│ │ ┌────────┐ │ ┌──────────────────┐ │ ┌──────────────────┐ │ │
|
|
29
|
+
│ │ │ 用户A │ │ │ │ │ │ │ │ │
|
|
30
|
+
│ │ │ 用户B │──┼───▶│ xiaoyou 插件 │───▶│ │ Agent / LLM │ │ │
|
|
31
|
+
│ │ │ 用户C │ │ │ (channel plugin)│◀───│ │ Tools / Memory │ │ │
|
|
32
|
+
│ │ └────────┘ │ │ │ │ │ │ │ │
|
|
33
|
+
│ │ │ └──────────────────┘ │ └──────────────────┘ │ │
|
|
34
|
+
│ └──────────────┘ └────────────────────────┘ │
|
|
35
|
+
│ │
|
|
36
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
37
|
+
|
|
38
|
+
连接方向:xiaoyou 插件 ──主动连接──▶ 企业服务(出站 WebSocket)
|
|
39
|
+
数据方向:双向全双工
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 3. 连接生命周期
|
|
43
|
+
|
|
44
|
+
### 3.1 启动与认证
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
xiaoyou 插件 企业服务端
|
|
48
|
+
│ │
|
|
49
|
+
│ ① WebSocket 握手 (HTTP Upgrade) │
|
|
50
|
+
│─────────────────────────────────────▶│
|
|
51
|
+
│ Header: Authorization: Bearer <token>│
|
|
52
|
+
│ │
|
|
53
|
+
│ ② 101 Switching Protocols │
|
|
54
|
+
│◀─────────────────────────────────────│
|
|
55
|
+
│ │
|
|
56
|
+
│ ③ 发送 auth 帧 │
|
|
57
|
+
│─────────────────────────────────────▶│
|
|
58
|
+
│ { │
|
|
59
|
+
│ "type": "auth", │
|
|
60
|
+
│ "token": "enterprise-token", │
|
|
61
|
+
│ "clientId": "openclaw-xiaoyou-default",
|
|
62
|
+
│ "clientVersion": "1.0.0" │
|
|
63
|
+
│ } │
|
|
64
|
+
│ │
|
|
65
|
+
│ ④ 返回 auth_result │
|
|
66
|
+
│◀─────────────────────────────────────│
|
|
67
|
+
│ { │
|
|
68
|
+
│ "type": "auth_result", │
|
|
69
|
+
│ "ok": true, │
|
|
70
|
+
│ "sessionId": "sess-abc123" │
|
|
71
|
+
│ } │
|
|
72
|
+
│ │
|
|
73
|
+
│ ═══ 认证完成,开始心跳 + 业务通信 ═══│
|
|
74
|
+
│ │
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3.2 心跳保活
|
|
78
|
+
|
|
79
|
+
认证成功后,插件按配置间隔(默认 30s)发送心跳:
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
xiaoyou 插件 企业服务端
|
|
83
|
+
│ │
|
|
84
|
+
│ ping {"type":"ping","seq":1,"ts":T} │
|
|
85
|
+
│─────────────────────────────────────▶│
|
|
86
|
+
│ │
|
|
87
|
+
│ pong {"type":"pong","seq":1,"ts":T} │
|
|
88
|
+
│◀─────────────────────────────────────│
|
|
89
|
+
│ │
|
|
90
|
+
│ ... 每 30 秒重复 ... │
|
|
91
|
+
│ │
|
|
92
|
+
│ ping {"type":"ping","seq":N,"ts":T} │
|
|
93
|
+
│─────────────────────────────────────▶│
|
|
94
|
+
│ │
|
|
95
|
+
│ ✗ 超过 10s 未收到 pong │
|
|
96
|
+
│ → 关闭连接 (code=4002) │
|
|
97
|
+
│ → 触发自动重连 │
|
|
98
|
+
│ │
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### 3.3 断线重连
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
xiaoyou 插件 企业服务端
|
|
105
|
+
│ │
|
|
106
|
+
│ 连接断开 (网络异常/心跳超时) │
|
|
107
|
+
│ ╳ │
|
|
108
|
+
│ │
|
|
109
|
+
│ 等待 3s (第1次) │
|
|
110
|
+
│ ...... │
|
|
111
|
+
│ 重新连接 ──────────────────────────▶│
|
|
112
|
+
│ 重新认证 ──────────────────────────▶│
|
|
113
|
+
│ │
|
|
114
|
+
│ 若再次失败: │
|
|
115
|
+
│ 等待 6s (第2次, 指数退避) │
|
|
116
|
+
│ 等待 12s (第3次) │
|
|
117
|
+
│ 等待 24s (第4次) │
|
|
118
|
+
│ ... │
|
|
119
|
+
│ 上限 60s │
|
|
120
|
+
│ │
|
|
121
|
+
│ 认证成功 → 重置重连计数 → 恢复通信 │
|
|
122
|
+
│ │
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
## 4. 业务消息流转
|
|
127
|
+
|
|
128
|
+
### 4.1 入站流程:企业用户 → Agent 回复(完整链路)
|
|
129
|
+
|
|
130
|
+
```
|
|
131
|
+
企业用户 企业服务端 xiaoyou 插件 OpenClaw Gateway LLM / Tools
|
|
132
|
+
│ │ │ │ │
|
|
133
|
+
│ ① 发送消息 │ │ │ │
|
|
134
|
+
│──────────────▶│ │ │ │
|
|
135
|
+
│ "帮我查项目进度"│ │ │ │
|
|
136
|
+
│ │ │ │ │
|
|
137
|
+
│ │ ② WebSocket 帧 │ │ │
|
|
138
|
+
│ │──────────────────▶│ │ │
|
|
139
|
+
│ │ { │ │ │
|
|
140
|
+
│ │ "type":"message"│ │ │
|
|
141
|
+
│ │ "conversationId"│ │ │
|
|
142
|
+
│ │ "senderId" │ │ │
|
|
143
|
+
│ │ "text":"帮我查.."│ │ │
|
|
144
|
+
│ │ } │ │ │
|
|
145
|
+
│ │ │ │ │
|
|
146
|
+
│ │ │ ③ 安全检查 │ │
|
|
147
|
+
│ │ │ - 是否已认证? │ │
|
|
148
|
+
│ │ │ - senderId 在白名单? │ │
|
|
149
|
+
│ │ │ │ │
|
|
150
|
+
│ │ │ ④ 归一化 + 分发 │ │
|
|
151
|
+
│ │ │─────────────────────▶│ │
|
|
152
|
+
│ │ │ runtime.inbound │ │
|
|
153
|
+
│ │ │ .dispatch({ │ │
|
|
154
|
+
│ │ │ channelId, │ │
|
|
155
|
+
│ │ │ senderId, │ │
|
|
156
|
+
│ │ │ conversationId, │ │
|
|
157
|
+
│ │ │ text, ... │ │
|
|
158
|
+
│ │ │ }) │ │
|
|
159
|
+
│ │ │ │ │
|
|
160
|
+
│ │ │ │ ⑤ 路由 + 会话 │
|
|
161
|
+
│ │ │ │ resolveAgentRoute() │
|
|
162
|
+
│ │ │ │ → sessionKey │
|
|
163
|
+
│ │ │ │ │
|
|
164
|
+
│ │ │ │ ⑥ 调用 LLM │
|
|
165
|
+
│ │ │ │───────────────────▶│
|
|
166
|
+
│ │ │ │ │
|
|
167
|
+
│ │ │ │ │ ⑦ 推理 + 工具调用
|
|
168
|
+
│ │ │ │ │ (可能多轮)
|
|
169
|
+
│ │ │ │ │
|
|
170
|
+
│ │ │ │ ⑧ 生成回复 │
|
|
171
|
+
│ │ │ │◀───────────────────│
|
|
172
|
+
│ │ │ │ "项目Alpha进度85%" │
|
|
173
|
+
│ │ │ │ │
|
|
174
|
+
│ │ │ ⑨ outbound.send() │ │
|
|
175
|
+
│ │ │◀─────────────────────│ │
|
|
176
|
+
│ │ │ payload={kind:"text",│ │
|
|
177
|
+
│ │ │ text:"项目Alpha.."} │ │
|
|
178
|
+
│ │ │ │ │
|
|
179
|
+
│ │ ⑩ WebSocket 帧 │ │ │
|
|
180
|
+
│ │◀──────────────────│ │ │
|
|
181
|
+
│ │ { │ │ │
|
|
182
|
+
│ │ "type":"reply" │ │ │
|
|
183
|
+
│ │ "conversationId"│ │ │
|
|
184
|
+
│ │ "text":"项目..."│ │ │
|
|
185
|
+
│ │ "timestamp":... │ │ │
|
|
186
|
+
│ │ } │ │ │
|
|
187
|
+
│ │ │ │ │
|
|
188
|
+
│ ⑪ 展示回复 │ │ │ │
|
|
189
|
+
│◀──────────────│ │ │ │
|
|
190
|
+
│ "项目Alpha │ │ │ │
|
|
191
|
+
│ 进度85%..." │ │ │ │
|
|
192
|
+
│ │ │ │ │
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### 4.2 入站流程步骤说明
|
|
196
|
+
|
|
197
|
+
| 步骤 | 位置 | 动作 | 代码位置 |
|
|
198
|
+
|------|------|------|----------|
|
|
199
|
+
| ① | 企业用户 → 企业服务 | 用户在企业 IM 中发送消息 | 企业侧实现 |
|
|
200
|
+
| ② | 企业服务 → xiaoyou | 企业服务将消息封装为 `type:"message"` 帧,通过 WebSocket 发送 | 企业侧实现 |
|
|
201
|
+
| ③ | xiaoyou 内部 | 检查是否已认证;检查 senderId 是否在 allowFrom 白名单 | `enterprise-client.ts` handleFrame() |
|
|
202
|
+
| ④ | xiaoyou → Gateway | 调用 `runtime.inbound.dispatch()` 将消息归一化后分发给 Gateway | `channel.ts` gateway.start.onMessage |
|
|
203
|
+
| ⑤ | Gateway 内部 | `resolveAgentRoute()` 根据 channelId + conversationId 生成 sessionKey | OpenClaw Core |
|
|
204
|
+
| ⑥ | Gateway → LLM | 将用户消息 + 上下文 + system prompt 发送给 LLM | OpenClaw Core |
|
|
205
|
+
| ⑦ | LLM | 推理、可能调用 tools(查数据库、执行命令等) | LLM Provider |
|
|
206
|
+
| ⑧ | LLM → Gateway | 返回生成的回复文本 | LLM Provider |
|
|
207
|
+
| ⑨ | Gateway → xiaoyou | 调用 `outbound.send()` 将回复交给插件 | `channel.ts` outbound.send |
|
|
208
|
+
| ⑩ | xiaoyou → 企业服务 | 将回复封装为 `type:"reply"` 帧,通过 WebSocket 发送 | `enterprise-client.ts` sendReply() |
|
|
209
|
+
| ⑪ | 企业服务 → 用户 | 企业服务将回复展示给用户 | 企业侧实现 |
|
|
210
|
+
|
|
211
|
+
### 4.3 带附件的消息流转
|
|
212
|
+
|
|
213
|
+
```
|
|
214
|
+
企业服务端 xiaoyou 插件 OpenClaw Gateway
|
|
215
|
+
│ │ │
|
|
216
|
+
│ message + attachments │ │
|
|
217
|
+
│─────────────────────────▶│ │
|
|
218
|
+
│ { │ │
|
|
219
|
+
│ "type": "message", │ │
|
|
220
|
+
│ "conversationId": "c1",│ │
|
|
221
|
+
│ "senderId": "u1", │ │
|
|
222
|
+
│ "text": "这张图是什么?",│ │
|
|
223
|
+
│ "attachments": [{ │ │
|
|
224
|
+
│ "kind": "image", │ │
|
|
225
|
+
│ "url": "https://...",│ │
|
|
226
|
+
│ "mimeType": "image/png" │
|
|
227
|
+
│ }] │ │
|
|
228
|
+
│ } │ │
|
|
229
|
+
│ │ dispatch (含附件信息) │
|
|
230
|
+
│ │─────────────────────────▶│
|
|
231
|
+
│ │ │
|
|
232
|
+
│ │ │ Agent 处理图片
|
|
233
|
+
│ │ │ (vision / tools)
|
|
234
|
+
│ │ │
|
|
235
|
+
│ │ reply (含媒体URL) │
|
|
236
|
+
│ │◀─────────────────────────│
|
|
237
|
+
│ reply + mediaUrls │ │
|
|
238
|
+
│◀─────────────────────────│ │
|
|
239
|
+
│ { │ │
|
|
240
|
+
│ "type": "reply", │ │
|
|
241
|
+
│ "text": "这是一张...",│ │
|
|
242
|
+
│ "mediaUrls": [ │ │
|
|
243
|
+
│ "https://..." │ │
|
|
244
|
+
│ ] │ │
|
|
245
|
+
│ } │ │
|
|
246
|
+
│ │ │
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
## 5. 协议帧参考
|
|
251
|
+
|
|
252
|
+
所有通信使用 JSON 文本帧,通过 `type` 字段区分:
|
|
253
|
+
|
|
254
|
+
### 5.1 帧类型总览
|
|
255
|
+
|
|
256
|
+
```
|
|
257
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
258
|
+
│ WebSocket 帧类型 │
|
|
259
|
+
├──────────────┬──────────────┬───────────────────────────────┤
|
|
260
|
+
│ 类别 │ type 值 │ 方向 │
|
|
261
|
+
├──────────────┼──────────────┼───────────────────────────────┤
|
|
262
|
+
│ 认证 │ auth │ 插件 → 企业服务 │
|
|
263
|
+
│ │ auth_result │ 企业服务 → 插件 │
|
|
264
|
+
├──────────────┼──────────────┼───────────────────────────────┤
|
|
265
|
+
│ 心跳 │ ping │ 插件 → 企业服务 │
|
|
266
|
+
│ │ pong │ 企业服务 → 插件 │
|
|
267
|
+
├──────────────┼──────────────┼───────────────────────────────┤
|
|
268
|
+
│ 业务消息 │ message │ 企业服务 → 插件 (入站) │
|
|
269
|
+
│ │ reply │ 插件 → 企业服务 (出站) │
|
|
270
|
+
└──────────────┴──────────────┴───────────────────────────────┘
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### 5.2 各帧详细格式
|
|
274
|
+
|
|
275
|
+
#### auth(插件 → 企业服务)
|
|
276
|
+
|
|
277
|
+
```json
|
|
278
|
+
{
|
|
279
|
+
"type": "auth",
|
|
280
|
+
"token": "enterprise-auth-token",
|
|
281
|
+
"clientId": "openclaw-xiaoyou-default",
|
|
282
|
+
"clientVersion": "1.0.0"
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### auth_result(企业服务 → 插件)
|
|
287
|
+
|
|
288
|
+
```json
|
|
289
|
+
{
|
|
290
|
+
"type": "auth_result",
|
|
291
|
+
"ok": true,
|
|
292
|
+
"sessionId": "sess-abc123",
|
|
293
|
+
"error": null
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
| 字段 | 类型 | 说明 |
|
|
298
|
+
|------|------|------|
|
|
299
|
+
| ok | boolean | 认证是否成功 |
|
|
300
|
+
| sessionId | string? | 成功时返回的会话标识 |
|
|
301
|
+
| error | string? | 失败时的错误信息 |
|
|
302
|
+
|
|
303
|
+
#### message(企业服务 → 插件)
|
|
304
|
+
|
|
305
|
+
```json
|
|
306
|
+
{
|
|
307
|
+
"type": "message",
|
|
308
|
+
"conversationId": "conv-123",
|
|
309
|
+
"senderId": "user-001",
|
|
310
|
+
"senderName": "张三",
|
|
311
|
+
"text": "帮我查一下项目进度",
|
|
312
|
+
"attachments": [
|
|
313
|
+
{
|
|
314
|
+
"kind": "image",
|
|
315
|
+
"url": "https://files.corp.com/img/screenshot.png",
|
|
316
|
+
"mimeType": "image/png",
|
|
317
|
+
"fileName": "screenshot.png"
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
"metadata": {
|
|
321
|
+
"source": "enterprise-im",
|
|
322
|
+
"priority": "high",
|
|
323
|
+
"department": "engineering"
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
| 字段 | 类型 | 必须 | 说明 |
|
|
329
|
+
|------|------|------|------|
|
|
330
|
+
| conversationId | string | ✅ | 会话 ID,用于路由和回复定位 |
|
|
331
|
+
| senderId | string | ✅ | 发送者唯一标识 |
|
|
332
|
+
| senderName | string | 否 | 发送者显示名 |
|
|
333
|
+
| text | string | 否 | 文本内容 |
|
|
334
|
+
| attachments | array | 否 | 附件列表 |
|
|
335
|
+
| metadata | object | 否 | 业务元数据,透传给 Agent |
|
|
336
|
+
|
|
337
|
+
#### reply(插件 → 企业服务)
|
|
338
|
+
|
|
339
|
+
```json
|
|
340
|
+
{
|
|
341
|
+
"type": "reply",
|
|
342
|
+
"conversationId": "conv-123",
|
|
343
|
+
"messageId": "xiaoyou-1730000000000",
|
|
344
|
+
"text": "项目 Alpha 当前进度 85%,预计下周三完成。",
|
|
345
|
+
"mediaUrls": ["https://openclaw-local/media/chart.png"],
|
|
346
|
+
"agentId": "main",
|
|
347
|
+
"timestamp": 1730000000000
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
| 字段 | 类型 | 说明 |
|
|
352
|
+
|------|------|------|
|
|
353
|
+
| conversationId | string | 对应入站消息的会话 ID |
|
|
354
|
+
| messageId | string | 回复消息的唯一 ID |
|
|
355
|
+
| text | string | 回复文本 |
|
|
356
|
+
| mediaUrls | string[]? | 附带的媒体文件 URL |
|
|
357
|
+
| agentId | string? | 处理该消息的 Agent ID |
|
|
358
|
+
| timestamp | number | 毫秒时间戳 |
|
|
359
|
+
|
|
360
|
+
#### ping / pong
|
|
361
|
+
|
|
362
|
+
```json
|
|
363
|
+
// 插件 → 企业服务
|
|
364
|
+
{ "type": "ping", "seq": 42, "ts": 1730000000000 }
|
|
365
|
+
|
|
366
|
+
// 企业服务 → 插件
|
|
367
|
+
{ "type": "pong", "seq": 42, "ts": 1730000000001 }
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
## 6. 插件内部模块交互
|
|
372
|
+
|
|
373
|
+
```
|
|
374
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
375
|
+
│ xiaoyou 插件内部 │
|
|
376
|
+
│ │
|
|
377
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
378
|
+
│ │ index.ts (入口) │ │
|
|
379
|
+
│ │ │ │
|
|
380
|
+
│ │ register(api) { │ │
|
|
381
|
+
│ │ setRuntime(api.runtime) ──▶ 注入 runtime 引用 │ │
|
|
382
|
+
│ │ api.registerChannel() ──▶ 注册到 PluginRegistry │ │
|
|
383
|
+
│ │ api.registerCli() ──▶ 注册 CLI 子命令 │ │
|
|
384
|
+
│ │ } │ │
|
|
385
|
+
│ └──────────────┬───────────────────────────────────────────┘ │
|
|
386
|
+
│ │ │
|
|
387
|
+
│ ▼ │
|
|
388
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
389
|
+
│ │ channel.ts (ChannelPlugin) │ │
|
|
390
|
+
│ │ │ │
|
|
391
|
+
│ │ config ──▶ 解析 wsUrl / authToken / allowFrom │ │
|
|
392
|
+
│ │ security ──▶ DM 白名单策略 │ │
|
|
393
|
+
│ │ gateway ──▶ 创建 EnterpriseClient,管理连接生命周期 │ │
|
|
394
|
+
│ │ outbound ──▶ 将 Agent 回复通过 EnterpriseClient 发出 │ │
|
|
395
|
+
│ │ status ──▶ 报告连接健康状态 │ │
|
|
396
|
+
│ └──────────────┬───────────────────────────────────────────┘ │
|
|
397
|
+
│ │ │
|
|
398
|
+
│ ▼ │
|
|
399
|
+
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
400
|
+
│ │ enterprise-client.ts (WebSocket 连接管理) │ │
|
|
401
|
+
│ │ │ │
|
|
402
|
+
│ │ connect() ──▶ 建立 WebSocket → 发送 auth │ │
|
|
403
|
+
│ │ handleFrame() ──▶ 解析 JSON 帧 → 分发到对应处理器 │ │
|
|
404
|
+
│ │ startHeartbeat() ──▶ 定时 ping,超时触发重连 │ │
|
|
405
|
+
│ │ scheduleReconnect() ──▶ 指数退避重连 │ │
|
|
406
|
+
│ │ sendReply() ──▶ 发送出站 reply 帧 │ │
|
|
407
|
+
│ │ disconnect() ──▶ 优雅关闭 │ │
|
|
408
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
409
|
+
│ │
|
|
410
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
## 7. 多账号场景
|
|
414
|
+
|
|
415
|
+
xiaoyou 支持同时连接多个企业服务(如销售系统 + 客服系统):
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
┌──────────────────────┐
|
|
419
|
+
│ OpenClaw Gateway │
|
|
420
|
+
│ │
|
|
421
|
+
│ ┌────────────────┐ │
|
|
422
|
+
│ │ xiaoyou 插件 │ │
|
|
423
|
+
│ │ │ │
|
|
424
|
+
┌──────────┐ │ │ ┌──────────┐ │ │
|
|
425
|
+
│ 销售 IM │◀════╪══╪══│ client │ │ │
|
|
426
|
+
│ 服务 │ │ │ │ "sales" │ │ │
|
|
427
|
+
└──────────┘ │ │ └──────────┘ │ │
|
|
428
|
+
│ │ │ │
|
|
429
|
+
┌──────────┐ │ │ ┌──────────┐ │ │
|
|
430
|
+
│ 客服 IM │◀════╪══╪══│ client │ │ │
|
|
431
|
+
│ 服务 │ │ │ │"support" │ │ │
|
|
432
|
+
└──────────┘ │ │ └──────────┘ │ │
|
|
433
|
+
│ │ │ │
|
|
434
|
+
│ └────────────────┘ │
|
|
435
|
+
└──────────────────────┘
|
|
436
|
+
|
|
437
|
+
配置:
|
|
438
|
+
{
|
|
439
|
+
"channels": {
|
|
440
|
+
"xiaoyou": {
|
|
441
|
+
"accounts": {
|
|
442
|
+
"sales": { "wsUrl": "wss://sales.corp.com/ws", "authToken": "..." },
|
|
443
|
+
"support": { "wsUrl": "wss://support.corp.com/ws", "authToken": "..." }
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
每个 account 独立维护一个 EnterpriseClient 实例,独立的连接、认证、心跳和重连。
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
## 8. 异常处理与容错
|
|
454
|
+
|
|
455
|
+
### 8.1 异常场景处理矩阵
|
|
456
|
+
|
|
457
|
+
```
|
|
458
|
+
┌──────────────────────┬─────────────────────────┬──────────────────────────┐
|
|
459
|
+
│ 异常场景 │ 插件行为 │ 企业侧感知 │
|
|
460
|
+
├──────────────────────┼─────────────────────────┼──────────────────────────┤
|
|
461
|
+
│ 网络断开 │ 自动重连(指数退避) │ 短暂无响应,恢复后正常 │
|
|
462
|
+
│ 企业服务重启 │ 连接断开 → 自动重连 │ 重启完成后自动恢复 │
|
|
463
|
+
│ 认证 token 过期 │ auth_result.ok=false │ 需更新配置中的 token │
|
|
464
|
+
│ │ 关闭连接(code=4001) │ │
|
|
465
|
+
│ 心跳超时 │ 关闭连接(code=4002) │ 插件会自动重连 │
|
|
466
|
+
│ │ 触发重连 │ │
|
|
467
|
+
│ 消息格式错误 │ 日志警告,跳过该帧 │ 无影响 │
|
|
468
|
+
│ senderId 不在白名单 │ 拒绝分发,日志记录 │ 用户收不到回复 │
|
|
469
|
+
│ LLM 调用失败 │ Gateway 返回错误信息 │ 收到错误提示 │
|
|
470
|
+
│ OpenClaw 重启 │ gateway.start() 重新 │ 短暂断开后自动恢复 │
|
|
471
|
+
│ │ 创建连接 │ │
|
|
472
|
+
│ 达到最大重连次数 │ 停止重连,日志报错 │ 需人工介入 │
|
|
473
|
+
└──────────────────────┴─────────────────────────┴──────────────────────────┘
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### 8.2 重连时序
|
|
477
|
+
|
|
478
|
+
```
|
|
479
|
+
时间轴 ──────────────────────────────────────────────────────▶
|
|
480
|
+
|
|
481
|
+
连接正常 断开! 3s 6s 12s 24s
|
|
482
|
+
═══════════╳·····│·····│·········│·············│
|
|
483
|
+
↑ ↑ ↑ ↑
|
|
484
|
+
重连1 重连2 重连3 重连4
|
|
485
|
+
(失败) (失败) (失败) (成功!)
|
|
486
|
+
│
|
|
487
|
+
▼
|
|
488
|
+
认证 → 重置计数
|
|
489
|
+
恢复心跳 + 通信
|
|
490
|
+
═══════════════▶
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## 9. 企业服务端实现要点
|
|
494
|
+
|
|
495
|
+
企业侧需要实现一个 WebSocket Server,处理以下职责:
|
|
496
|
+
|
|
497
|
+
### 9.1 必须实现
|
|
498
|
+
|
|
499
|
+
```
|
|
500
|
+
┌─────────────────────────────────────────────────────────┐
|
|
501
|
+
│ 企业 WebSocket Server 必须实现 │
|
|
502
|
+
├─────────────────────────────────────────────────────────┤
|
|
503
|
+
│ │
|
|
504
|
+
│ 1. 接受 WebSocket 连接 │
|
|
505
|
+
│ - 监听指定端口 (如 wss://im.corp.com/ws) │
|
|
506
|
+
│ - 支持 TLS (生产环境) │
|
|
507
|
+
│ │
|
|
508
|
+
│ 2. 处理 auth 帧 │
|
|
509
|
+
│ - 验证 token │
|
|
510
|
+
│ - 返回 auth_result │
|
|
511
|
+
│ │
|
|
512
|
+
│ 3. 响应 ping 帧 │
|
|
513
|
+
│ - 收到 ping 后回复相同 seq 的 pong │
|
|
514
|
+
│ │
|
|
515
|
+
│ 4. 发送 message 帧 │
|
|
516
|
+
│ - 将用户消息封装为 type:"message" 格式 │
|
|
517
|
+
│ - 包含 conversationId + senderId + text │
|
|
518
|
+
│ │
|
|
519
|
+
│ 5. 接收 reply 帧 │
|
|
520
|
+
│ - 解析 Agent 回复 │
|
|
521
|
+
│ - 根据 conversationId 路由到对应用户 │
|
|
522
|
+
│ │
|
|
523
|
+
└─────────────────────────────────────────────────────────┘
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### 9.2 企业服务端伪代码示例
|
|
527
|
+
|
|
528
|
+
```python
|
|
529
|
+
# Python 示例 (FastAPI + websockets)
|
|
530
|
+
|
|
531
|
+
from fastapi import FastAPI, WebSocket
|
|
532
|
+
import json
|
|
533
|
+
|
|
534
|
+
app = FastAPI()
|
|
535
|
+
AUTH_TOKEN = "enterprise-auth-token"
|
|
536
|
+
|
|
537
|
+
@app.websocket("/ws")
|
|
538
|
+
async def ws_endpoint(ws: WebSocket):
|
|
539
|
+
await ws.accept()
|
|
540
|
+
authenticated = False
|
|
541
|
+
|
|
542
|
+
while True:
|
|
543
|
+
raw = await ws.receive_text()
|
|
544
|
+
frame = json.loads(raw)
|
|
545
|
+
|
|
546
|
+
if frame["type"] == "auth":
|
|
547
|
+
# ① 验证 token
|
|
548
|
+
if frame["token"] == AUTH_TOKEN:
|
|
549
|
+
authenticated = True
|
|
550
|
+
await ws.send_json({
|
|
551
|
+
"type": "auth_result",
|
|
552
|
+
"ok": True,
|
|
553
|
+
"sessionId": "sess-" + str(id(ws))
|
|
554
|
+
})
|
|
555
|
+
else:
|
|
556
|
+
await ws.send_json({
|
|
557
|
+
"type": "auth_result",
|
|
558
|
+
"ok": False,
|
|
559
|
+
"error": "invalid token"
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
elif frame["type"] == "ping":
|
|
563
|
+
# ② 回复心跳
|
|
564
|
+
await ws.send_json({
|
|
565
|
+
"type": "pong",
|
|
566
|
+
"seq": frame["seq"],
|
|
567
|
+
"ts": int(time.time() * 1000)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
elif frame["type"] == "reply":
|
|
571
|
+
# ③ 收到 Agent 回复,转发给用户
|
|
572
|
+
deliver_to_user(
|
|
573
|
+
conversation_id=frame["conversationId"],
|
|
574
|
+
text=frame["text"],
|
|
575
|
+
media_urls=frame.get("mediaUrls", [])
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
# 当用户发消息时,推送到 WebSocket:
|
|
579
|
+
async def on_user_message(ws, user_msg):
|
|
580
|
+
await ws.send_json({
|
|
581
|
+
"type": "message",
|
|
582
|
+
"conversationId": user_msg.conversation_id,
|
|
583
|
+
"senderId": user_msg.user_id,
|
|
584
|
+
"senderName": user_msg.user_name,
|
|
585
|
+
"text": user_msg.text
|
|
586
|
+
})
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## 10. 部署拓扑
|
|
590
|
+
|
|
591
|
+
### 10.1 最简部署(单机)
|
|
592
|
+
|
|
593
|
+
```
|
|
594
|
+
┌─────────────────────────────────────────────┐
|
|
595
|
+
│ 单台服务器 │
|
|
596
|
+
│ │
|
|
597
|
+
│ ┌──────────────────┐ ┌─────────────────┐ │
|
|
598
|
+
│ │ OpenClaw Gateway │ │ 企业 IM 服务 │ │
|
|
599
|
+
│ │ + xiaoyou 插件 │══│ WebSocket Server│ │
|
|
600
|
+
│ │ (port 18789) │ │ (port 9090) │ │
|
|
601
|
+
│ └──────────────────┘ └─────────────────┘ │
|
|
602
|
+
│ │
|
|
603
|
+
│ 连接: ws://127.0.0.1:9090 │
|
|
604
|
+
└─────────────────────────────────────────────┘
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
### 10.2 生产部署(分离)
|
|
608
|
+
|
|
609
|
+
```
|
|
610
|
+
┌──────────────────┐ ┌──────────────────┐
|
|
611
|
+
│ OpenClaw 节点 │ wss:// │ 企业服务集群 │
|
|
612
|
+
│ │ (TLS) │ │
|
|
613
|
+
│ Gateway │════════▶│ WebSocket LB │
|
|
614
|
+
│ + xiaoyou │ │ ├─ IM Server 1 │
|
|
615
|
+
│ │ │ ├─ IM Server 2 │
|
|
616
|
+
│ (内网/DMZ) │ │ └─ IM Server 3 │
|
|
617
|
+
└──────────────────┘ └──────────────────┘
|
|
618
|
+
|
|
619
|
+
注意:
|
|
620
|
+
- 生产环境务必使用 wss:// (TLS)
|
|
621
|
+
- 企业侧可用负载均衡,但需保证 WebSocket 会话粘性
|
|
622
|
+
- xiaoyou 的重连机制可以应对单节点故障
|
|
623
|
+
```
|
|
624
|
+
|
|
625
|
+
## 11. 快速对照表
|
|
626
|
+
|
|
627
|
+
| 你想知道... | 看这里 |
|
|
628
|
+
|------------|--------|
|
|
629
|
+
| 整体架构是什么样的? | §2 整体架构图 |
|
|
630
|
+
| 连接是怎么建立的? | §3.1 启动与认证 |
|
|
631
|
+
| 断线了怎么办? | §3.3 断线重连 |
|
|
632
|
+
| 用户消息怎么到达 Agent? | §4.1 入站流程 |
|
|
633
|
+
| Agent 回复怎么送回用户? | §4.1 步骤 ⑨⑩⑪ |
|
|
634
|
+
| WebSocket 帧格式是什么? | §5 协议帧参考 |
|
|
635
|
+
| 插件内部怎么组织的? | §6 插件内部模块交互 |
|
|
636
|
+
| 怎么连多个企业服务? | §7 多账号场景 |
|
|
637
|
+
| 出了问题怎么处理? | §8 异常处理与容错 |
|
|
638
|
+
| 企业侧要实现什么? | §9 企业服务端实现要点 |
|
|
639
|
+
| 怎么部署? | §10 部署拓扑 |
|