openclaw-xiaoyou 1.2.5 → 1.3.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/docs/communication-flow.md +35 -21
- package/docs/user-comand.md +46 -6
- package/docs/xiaoyou-plugin-architecture.svg +93 -0
- package/package.json +1 -1
- package/src/channel.ts +54 -7
- package/src/enterprise-client.ts +23 -1
- package/src/types.ts +29 -0
|
@@ -138,24 +138,24 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
138
138
|
│ │──────────────────▶│ │ │
|
|
139
139
|
│ │ { │ │ │
|
|
140
140
|
│ │ "type":"message"│ │ │
|
|
141
|
+
│ │ "messageId" │ │ │
|
|
141
142
|
│ │ "conversationId"│ │ │
|
|
142
143
|
│ │ "senderId" │ │ │
|
|
143
144
|
│ │ "text":"帮我查.."│ │ │
|
|
144
145
|
│ │ } │ │ │
|
|
145
146
|
│ │ │ │ │
|
|
146
|
-
│ │ │ ③
|
|
147
|
-
│ │ │ - 是否已认证?
|
|
148
|
-
│ │ │ - senderId 在白名单? │ │
|
|
147
|
+
│ │ │ ③ 认证检查 │ │
|
|
148
|
+
│ │ │ - WebSocket 是否已认证? │ │
|
|
149
149
|
│ │ │ │ │
|
|
150
|
-
│ │ │ ④
|
|
150
|
+
│ │ │ ④ 路由 + 格式化 + 分发│ │
|
|
151
151
|
│ │ │─────────────────────▶│ │
|
|
152
|
-
│ │ │
|
|
153
|
-
│ │ │
|
|
154
|
-
│ │ │
|
|
155
|
-
│ │ │
|
|
156
|
-
│ │ │
|
|
157
|
-
│ │ │
|
|
158
|
-
│ │ │
|
|
152
|
+
│ │ │ rt.channel.routing │ │
|
|
153
|
+
│ │ │ .resolveAgentRoute() │
|
|
154
|
+
│ │ │ rt.channel.reply │ │
|
|
155
|
+
│ │ │ .formatInboundEnvelope() │
|
|
156
|
+
│ │ │ .finalizeInboundContext() │
|
|
157
|
+
│ │ │ .dispatchReplyWith │ │
|
|
158
|
+
│ │ │ BufferedBlock...() │ │
|
|
159
159
|
│ │ │ │ │
|
|
160
160
|
│ │ │ │ ⑤ 路由 + 会话 │
|
|
161
161
|
│ │ │ │ resolveAgentRoute() │
|
|
@@ -171,16 +171,16 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
171
171
|
│ │ │ │◀───────────────────│
|
|
172
172
|
│ │ │ │ "项目Alpha进度85%" │
|
|
173
173
|
│ │ │ │ │
|
|
174
|
-
│ │ │ ⑨
|
|
174
|
+
│ │ │ ⑨ deliver() 回调 │ │
|
|
175
175
|
│ │ │◀─────────────────────│ │
|
|
176
|
-
│ │ │ payload={
|
|
177
|
-
│ │ │ text:"项目Alpha.."} │ │
|
|
176
|
+
│ │ │ payload={text:"..."} │ │
|
|
178
177
|
│ │ │ │ │
|
|
179
178
|
│ │ ⑩ WebSocket 帧 │ │ │
|
|
180
179
|
│ │◀──────────────────│ │ │
|
|
181
180
|
│ │ { │ │ │
|
|
182
181
|
│ │ "type":"reply" │ │ │
|
|
183
182
|
│ │ "conversationId"│ │ │
|
|
183
|
+
│ │ "replyToMessageId" │ │
|
|
184
184
|
│ │ "text":"项目..."│ │ │
|
|
185
185
|
│ │ "timestamp":... │ │ │
|
|
186
186
|
│ │ } │ │ │
|
|
@@ -199,12 +199,12 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
199
199
|
| ① | 企业用户 → 企业服务 | 用户在企业 IM 中发送消息 | 企业侧实现 |
|
|
200
200
|
| ② | 企业服务 → xiaoyou | 企业服务将消息封装为 `type:"message"` 帧,通过 WebSocket 发送 | 企业侧实现 |
|
|
201
201
|
| ③ | xiaoyou 内部 | 检查是否已认证;检查 senderId 是否在 allowFrom 白名单 | `enterprise-client.ts` handleFrame() |
|
|
202
|
-
| ④ | xiaoyou → Gateway | 调用 `
|
|
202
|
+
| ④ | xiaoyou → Gateway | 调用 `rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher()` 将消息分发给 Gateway | `channel.ts` gateway.startAccount.onMessage |
|
|
203
203
|
| ⑤ | Gateway 内部 | `resolveAgentRoute()` 根据 channelId + conversationId 生成 sessionKey | OpenClaw Core |
|
|
204
204
|
| ⑥ | Gateway → LLM | 将用户消息 + 上下文 + system prompt 发送给 LLM | OpenClaw Core |
|
|
205
205
|
| ⑦ | LLM | 推理、可能调用 tools(查数据库、执行命令等) | LLM Provider |
|
|
206
206
|
| ⑧ | LLM → Gateway | 返回生成的回复文本 | LLM Provider |
|
|
207
|
-
| ⑨ | Gateway → xiaoyou |
|
|
207
|
+
| ⑨ | Gateway → xiaoyou | 通过 dispatcher 的 deliver 回调将回复交给插件 | `channel.ts` deliver() |
|
|
208
208
|
| ⑩ | xiaoyou → 企业服务 | 将回复封装为 `type:"reply"` 帧,通过 WebSocket 发送 | `enterprise-client.ts` sendReply() |
|
|
209
209
|
| ⑪ | 企业服务 → 用户 | 企业服务将回复展示给用户 | 企业侧实现 |
|
|
210
210
|
|
|
@@ -306,6 +306,7 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
306
306
|
{
|
|
307
307
|
"type": "message",
|
|
308
308
|
"conversationId": "conv-123",
|
|
309
|
+
"messageId": "msg-001",
|
|
309
310
|
"senderId": "user-001",
|
|
310
311
|
"senderName": "张三",
|
|
311
312
|
"text": "帮我查一下项目进度",
|
|
@@ -328,6 +329,7 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
328
329
|
| 字段 | 类型 | 必须 | 说明 |
|
|
329
330
|
|------|------|------|------|
|
|
330
331
|
| conversationId | string | ✅ | 会话 ID,用于路由和回复定位 |
|
|
332
|
+
| messageId | string | 否 | 消息唯一 ID,用于请求-回复一一绑定。回复帧会通过 replyToMessageId 回传 |
|
|
331
333
|
| senderId | string | ✅ | 发送者唯一标识 |
|
|
332
334
|
| senderName | string | 否 | 发送者显示名 |
|
|
333
335
|
| text | string | 否 | 文本内容 |
|
|
@@ -341,6 +343,7 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
341
343
|
"type": "reply",
|
|
342
344
|
"conversationId": "conv-123",
|
|
343
345
|
"messageId": "xiaoyou-1730000000000",
|
|
346
|
+
"replyToMessageId": "msg-001",
|
|
344
347
|
"text": "项目 Alpha 当前进度 85%,预计下周三完成。",
|
|
345
348
|
"mediaUrls": ["https://openclaw-local/media/chart.png"],
|
|
346
349
|
"agentId": "main",
|
|
@@ -351,7 +354,8 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
351
354
|
| 字段 | 类型 | 说明 |
|
|
352
355
|
|------|------|------|
|
|
353
356
|
| conversationId | string | 对应入站消息的会话 ID |
|
|
354
|
-
| messageId | string | 回复消息的唯一 ID |
|
|
357
|
+
| messageId | string | 回复消息的唯一 ID(由插件生成) |
|
|
358
|
+
| replyToMessageId | string? | 对应入站 message 的 messageId,用于请求-回复一一绑定 |
|
|
355
359
|
| text | string | 回复文本 |
|
|
356
360
|
| mediaUrls | string[]? | 附带的媒体文件 URL |
|
|
357
361
|
| agentId | string? | 处理该消息的 Agent ID |
|
|
@@ -388,10 +392,20 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
388
392
|
│ ┌──────────────────────────────────────────────────────────┐ │
|
|
389
393
|
│ │ channel.ts (ChannelPlugin) │ │
|
|
390
394
|
│ │ │ │
|
|
391
|
-
│ │ config ──▶
|
|
392
|
-
│ │
|
|
393
|
-
│ │
|
|
394
|
-
│ │
|
|
395
|
+
│ │ config ──▶ listAccountIds / resolveAccount / │ │
|
|
396
|
+
│ │ isConfigured / describeAccount │ │
|
|
397
|
+
│ │ security ──▶ DM 策略 (默认 open,允许所有用户) │ │
|
|
398
|
+
│ │ gateway.startAccount(ctx) ──▶ │ │
|
|
399
|
+
│ │ 1. 从 ctx.account.config 读取 wsUrl/authToken │ │
|
|
400
|
+
│ │ 2. 创建 EnterpriseClient 并连接 │ │
|
|
401
|
+
│ │ 3. onMessage 收到消息时: │ │
|
|
402
|
+
│ │ rt.channel.routing.resolveAgentRoute() │ │
|
|
403
|
+
│ │ rt.channel.reply.formatInboundEnvelope() │ │
|
|
404
|
+
│ │ rt.channel.reply.finalizeInboundContext() │ │
|
|
405
|
+
│ │ rt.channel.reply.dispatchReplyWith...() │ │
|
|
406
|
+
│ │ 4. deliver() 回调将 LLM 回复通过 WebSocket 发出 │ │
|
|
407
|
+
│ │ 5. 通过 abortSignal 保持挂起,直到 gateway 停止 │ │
|
|
408
|
+
│ │ outbound ──▶ 将主动推送通过 EnterpriseClient 发出 │ │
|
|
395
409
|
│ │ status ──▶ 报告连接健康状态 │ │
|
|
396
410
|
│ └──────────────┬───────────────────────────────────────────┘ │
|
|
397
411
|
│ │ │
|
package/docs/user-comand.md
CHANGED
|
@@ -1,16 +1,56 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+
# 小优 Channel 安装话术与部署路径
|
|
3
3
|
|
|
4
|
+
## 安装话术
|
|
4
5
|
|
|
5
|
-
https://
|
|
6
|
+
按照 https://d10.tfdl.net/public/2026-05-06/14dbf63d-d0aa-4b5d-bdd6-47eddb455f36/install-xiaoyou.sh 文档完成小优channel配置,AccessToken为xiaoyou-025b07e43ae84cd6
|
|
7
|
+
|
|
8
|
+
## 脚本位置
|
|
9
|
+
|
|
10
|
+
https://d10.tfdl.net/public/2026-05-06/14dbf63d-d0aa-4b5d-bdd6-47eddb455f36/install-xiaoyou.sh
|
|
11
|
+
|
|
12
|
+
## 卸载+重装命令
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/14dbf63d-d0aa-4b5d-bdd6-47eddb455f36/install-xiaoyou.sh | bash -s -- uninstall && \
|
|
16
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/14dbf63d-d0aa-4b5d-bdd6-47eddb455f36/install-xiaoyou.sh | bash -s -- \
|
|
17
|
+
--version 1.2.5 \
|
|
18
|
+
--ws-url ws://aiws-sim.haiersmarthomes.com:11055/xiaoyou/claw \
|
|
19
|
+
--token xiaoyou-025b07e43ae84cd6
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 验证发消息
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
curl -X POST http://172.16.4.35:11480/ai-connection/xiaoyou/message/send \
|
|
26
|
+
--header 'appId: VOMD-AICUSTOM-0000' \
|
|
27
|
+
--header 'clientId: 793C339F7C' \
|
|
28
|
+
--header 'content-type: application/json' \
|
|
29
|
+
--header 'deviceId: 793C339F7C' \
|
|
30
|
+
--header 'sign: 63ce6135d8580230c2729ed39cd50cb0' \
|
|
31
|
+
--header 'timestamp: 2248687025' \
|
|
32
|
+
-d '{
|
|
33
|
+
"userToken": "xiaoyou-025b07e43ae84cd6",
|
|
34
|
+
"conversationId": "conv-123",
|
|
35
|
+
"messageId": "msg-001",
|
|
36
|
+
"senderId": "user-001",
|
|
37
|
+
"text": "你好"
|
|
38
|
+
}'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 查看日志
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
journalctl --user -u openclaw-gateway.service --since "2 min ago" --no-pager | grep -E "xiaoyou.*reply|xiaoyou.*error|xiaoyou.*sent"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 部署路径
|
|
6
48
|
|
|
7
49
|
验收环境位置:
|
|
8
50
|
/aiysnfs/nginx/share/skills/upload/claw
|
|
51
|
+
|
|
9
52
|
仿真环境位置:
|
|
10
53
|
/ai-sim/export/nginx/share/skills/upload/claw
|
|
54
|
+
|
|
11
55
|
生产环境位置:
|
|
12
56
|
/ai-prod/export/nginx/share/skills/upload/claw
|
|
13
|
-
|
|
14
|
-
/aiysnfs/nginx/ssl/aiservice/html/
|
|
15
|
-
|
|
16
|
-
按照 https://aitest.haiersmarthomes.com/aiservice/html/claw/SKILL.md 文档完成小优channel配置,AccessToken为xiaoyou-025b07e43ae84cd6
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 920" font-family="'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif">
|
|
3
|
+
<defs>
|
|
4
|
+
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
|
5
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="#555"/>
|
|
6
|
+
</marker>
|
|
7
|
+
<filter id="shadow" x="-2%" y="-2%" width="104%" height="104%">
|
|
8
|
+
<feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity="0.1"/>
|
|
9
|
+
</filter>
|
|
10
|
+
</defs>
|
|
11
|
+
|
|
12
|
+
<!-- 外框 -->
|
|
13
|
+
<rect x="30" y="20" width="840" height="880" rx="16" fill="#f8fafc" stroke="#94a3b8" stroke-width="2" filter="url(#shadow)"/>
|
|
14
|
+
<text x="450" y="55" text-anchor="middle" font-size="20" font-weight="bold" fill="#1e293b">xiaoyou 插件内部架构</text>
|
|
15
|
+
|
|
16
|
+
<!-- index.ts 模块 -->
|
|
17
|
+
<rect x="60" y="80" width="780" height="180" rx="12" fill="#ffffff" stroke="#3b82f6" stroke-width="2" filter="url(#shadow)"/>
|
|
18
|
+
<rect x="60" y="80" width="780" height="36" rx="12" fill="#3b82f6"/>
|
|
19
|
+
<rect x="60" y="104" width="780" height="12" fill="#3b82f6"/>
|
|
20
|
+
<text x="80" y="104" font-size="14" font-weight="bold" fill="#ffffff">index.ts (入口)</text>
|
|
21
|
+
|
|
22
|
+
<text x="90" y="145" font-size="13" fill="#334155" font-family="'Consolas', 'Courier New', monospace">register(api) {</text>
|
|
23
|
+
<text x="110" y="170" font-size="13" fill="#334155" font-family="'Consolas', 'Courier New', monospace">setRuntime(api.runtime)</text>
|
|
24
|
+
<text x="430" y="170" font-size="12" fill="#6366f1">──▶ 注入 runtime 引用</text>
|
|
25
|
+
<text x="110" y="195" font-size="13" fill="#334155" font-family="'Consolas', 'Courier New', monospace">api.registerChannel()</text>
|
|
26
|
+
<text x="430" y="195" font-size="12" fill="#6366f1">──▶ 注册到 PluginRegistry</text>
|
|
27
|
+
<text x="110" y="220" font-size="13" fill="#334155" font-family="'Consolas', 'Courier New', monospace">api.registerCli()</text>
|
|
28
|
+
<text x="430" y="220" font-size="12" fill="#6366f1">──▶ 注册 CLI 子命令</text>
|
|
29
|
+
<text x="90" y="245" font-size="13" fill="#334155" font-family="'Consolas', 'Courier New', monospace">}</text>
|
|
30
|
+
|
|
31
|
+
<!-- 箭头: index -> channel -->
|
|
32
|
+
<line x1="450" y1="260" x2="450" y2="295" stroke="#555" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
33
|
+
|
|
34
|
+
<!-- channel.ts 模块 -->
|
|
35
|
+
<rect x="60" y="300" width="780" height="320" rx="12" fill="#ffffff" stroke="#10b981" stroke-width="2" filter="url(#shadow)"/>
|
|
36
|
+
<rect x="60" y="300" width="780" height="36" rx="12" fill="#10b981"/>
|
|
37
|
+
<rect x="60" y="324" width="780" height="12" fill="#10b981"/>
|
|
38
|
+
<text x="80" y="324" font-size="14" font-weight="bold" fill="#ffffff">channel.ts (ChannelPlugin)</text>
|
|
39
|
+
|
|
40
|
+
<!-- config 区域 -->
|
|
41
|
+
<text x="90" y="365" font-size="13" font-weight="bold" fill="#1e293b">config</text>
|
|
42
|
+
<text x="170" y="365" font-size="12" fill="#6366f1">──▶ listAccountIds / resolveAccount / isConfigured / describeAccount</text>
|
|
43
|
+
|
|
44
|
+
<!-- security 区域 -->
|
|
45
|
+
<text x="90" y="395" font-size="13" font-weight="bold" fill="#1e293b">security</text>
|
|
46
|
+
<text x="170" y="395" font-size="12" fill="#6366f1">──▶ DM 策略 (默认 open,允许所有用户)</text>
|
|
47
|
+
|
|
48
|
+
<!-- gateway 区域 -->
|
|
49
|
+
<text x="90" y="430" font-size="13" font-weight="bold" fill="#1e293b">gateway.startAccount(ctx)</text>
|
|
50
|
+
<text x="110" y="455" font-size="12" fill="#475569">1. 从 ctx.account.config 读取 wsUrl / authToken</text>
|
|
51
|
+
<text x="110" y="478" font-size="12" fill="#475569">2. 创建 EnterpriseClient 并连接</text>
|
|
52
|
+
<text x="110" y="501" font-size="12" fill="#475569">3. onMessage 收到消息时:</text>
|
|
53
|
+
<text x="140" y="521" font-size="11" fill="#64748b" font-family="'Consolas', 'Courier New', monospace">rt.channel.routing.resolveAgentRoute()</text>
|
|
54
|
+
<text x="140" y="539" font-size="11" fill="#64748b" font-family="'Consolas', 'Courier New', monospace">rt.channel.reply.formatInboundEnvelope()</text>
|
|
55
|
+
<text x="140" y="557" font-size="11" fill="#64748b" font-family="'Consolas', 'Courier New', monospace">rt.channel.reply.finalizeInboundContext()</text>
|
|
56
|
+
<text x="140" y="575" font-size="11" fill="#64748b" font-family="'Consolas', 'Courier New', monospace">rt.channel.reply.dispatchReplyWith...()</text>
|
|
57
|
+
<text x="110" y="598" font-size="12" fill="#475569">4. deliver() 回调将 LLM 回复通过 WebSocket 发出</text>
|
|
58
|
+
|
|
59
|
+
<!-- outbound & status -->
|
|
60
|
+
<text x="90" y="555" font-size="13" font-weight="bold" fill="#1e293b" transform="translate(520, -195)">outbound</text>
|
|
61
|
+
<text x="170" y="555" font-size="12" fill="#6366f1" transform="translate(520, -195)">──▶ 将主动推送通过</text>
|
|
62
|
+
<text x="170" y="573" font-size="12" fill="#6366f1" transform="translate(520, -195)"> EnterpriseClient 发出</text>
|
|
63
|
+
|
|
64
|
+
<text x="90" y="598" font-size="13" font-weight="bold" fill="#1e293b" transform="translate(520, -195)">status</text>
|
|
65
|
+
<text x="170" y="598" font-size="12" fill="#6366f1" transform="translate(520, -195)">──▶ 报告连接健康状态</text>
|
|
66
|
+
|
|
67
|
+
<!-- 箭头: channel -> enterprise-client -->
|
|
68
|
+
<line x1="450" y1="620" x2="450" y2="655" stroke="#555" stroke-width="2" marker-end="url(#arrowhead)"/>
|
|
69
|
+
|
|
70
|
+
<!-- enterprise-client.ts 模块 -->
|
|
71
|
+
<rect x="60" y="660" width="780" height="220" rx="12" fill="#ffffff" stroke="#f59e0b" stroke-width="2" filter="url(#shadow)"/>
|
|
72
|
+
<rect x="60" y="660" width="780" height="36" rx="12" fill="#f59e0b"/>
|
|
73
|
+
<rect x="60" y="684" width="780" height="12" fill="#f59e0b"/>
|
|
74
|
+
<text x="80" y="684" font-size="14" font-weight="bold" fill="#ffffff">enterprise-client.ts (WebSocket 连接管理)</text>
|
|
75
|
+
|
|
76
|
+
<text x="90" y="725" font-size="13" font-weight="bold" fill="#1e293b">connect()</text>
|
|
77
|
+
<text x="260" y="725" font-size="12" fill="#6366f1">──▶ 建立 WebSocket → 发送 auth</text>
|
|
78
|
+
|
|
79
|
+
<text x="90" y="755" font-size="13" font-weight="bold" fill="#1e293b">handleFrame()</text>
|
|
80
|
+
<text x="260" y="755" font-size="12" fill="#6366f1">──▶ 解析 JSON 帧 → 分发到对应处理器</text>
|
|
81
|
+
|
|
82
|
+
<text x="90" y="785" font-size="13" font-weight="bold" fill="#1e293b">startHeartbeat()</text>
|
|
83
|
+
<text x="260" y="785" font-size="12" fill="#6366f1">──▶ 定时 ping,超时触发重连</text>
|
|
84
|
+
|
|
85
|
+
<text x="90" y="815" font-size="13" font-weight="bold" fill="#1e293b">scheduleReconnect()</text>
|
|
86
|
+
<text x="260" y="815" font-size="12" fill="#6366f1">──▶ 指数退避重连</text>
|
|
87
|
+
|
|
88
|
+
<text x="90" y="845" font-size="13" font-weight="bold" fill="#1e293b">sendReply()</text>
|
|
89
|
+
<text x="260" y="845" font-size="12" fill="#6366f1">──▶ 发送出站 reply 帧</text>
|
|
90
|
+
|
|
91
|
+
<text x="90" y="875" font-size="13" font-weight="bold" fill="#1e293b">disconnect()</text>
|
|
92
|
+
<text x="260" y="875" font-size="12" fill="#6366f1">──▶ 优雅关闭</text>
|
|
93
|
+
</svg>
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -67,7 +67,7 @@ export const xiayouPlugin = {
|
|
|
67
67
|
reactions: false,
|
|
68
68
|
threads: false,
|
|
69
69
|
nativeCommands: false,
|
|
70
|
-
blockStreaming:
|
|
70
|
+
blockStreaming: true,
|
|
71
71
|
},
|
|
72
72
|
|
|
73
73
|
reload: { configPrefixes: ["channels.xiaoyou"] },
|
|
@@ -180,7 +180,19 @@ export const xiayouPlugin = {
|
|
|
180
180
|
Timestamp: Date.now(),
|
|
181
181
|
});
|
|
182
182
|
|
|
183
|
-
// 4.
|
|
183
|
+
// 4. 分发并获取回复(流式 — block 级)
|
|
184
|
+
//
|
|
185
|
+
// OpenClaw 2026.3.x 的 block streaming 机制:
|
|
186
|
+
// capabilities.blockStreaming = true 告诉 Gateway 按 block 逐个调用 deliver,
|
|
187
|
+
// 而非等全部生成完毕。每个 block(段落/代码块等)生成完成后立即 deliver。
|
|
188
|
+
// 我们将每次 deliver 作为一个 reply_chunk 推送给企业服务,
|
|
189
|
+
// dispatch promise resolve 后发送 reply_end 标记。
|
|
190
|
+
//
|
|
191
|
+
const replyMessageId = `xiaoyou-${Date.now()}`;
|
|
192
|
+
let chunkSeq = 0;
|
|
193
|
+
let fullText = "";
|
|
194
|
+
let replyEndSent = false;
|
|
195
|
+
|
|
184
196
|
await rt.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
185
197
|
ctx: inboundCtx,
|
|
186
198
|
cfg,
|
|
@@ -189,20 +201,55 @@ export const xiayouPlugin = {
|
|
|
189
201
|
deliver: async (payload: any) => {
|
|
190
202
|
const textToSend = payload.markdown || payload.text;
|
|
191
203
|
if (!textToSend) return;
|
|
204
|
+
|
|
205
|
+
fullText += (fullText ? "\n" : "") + textToSend;
|
|
206
|
+
|
|
207
|
+
if (_client && _client.isConnected()) {
|
|
208
|
+
// 每个 block 作为一个 chunk 推送
|
|
209
|
+
_client.sendChunk({
|
|
210
|
+
type: "reply_chunk",
|
|
211
|
+
conversationId,
|
|
212
|
+
messageId: replyMessageId,
|
|
213
|
+
replyToMessageId: inboundMessageId,
|
|
214
|
+
delta: textToSend,
|
|
215
|
+
seq: chunkSeq++,
|
|
216
|
+
timestamp: Date.now(),
|
|
217
|
+
});
|
|
218
|
+
logger.info(`[xiaoyou] chunk #${chunkSeq} sent to ${conversationId}`);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
onComplete: async () => {
|
|
222
|
+
// Gateway 调用 onComplete 表示所有 block 已推送完毕
|
|
223
|
+
replyEndSent = true;
|
|
192
224
|
if (_client && _client.isConnected()) {
|
|
193
|
-
_client.
|
|
194
|
-
type: "
|
|
225
|
+
_client.sendReplyEnd({
|
|
226
|
+
type: "reply_end",
|
|
195
227
|
conversationId,
|
|
196
|
-
messageId:
|
|
228
|
+
messageId: replyMessageId,
|
|
197
229
|
replyToMessageId: inboundMessageId,
|
|
198
|
-
|
|
230
|
+
fullText,
|
|
199
231
|
timestamp: Date.now(),
|
|
200
232
|
});
|
|
201
|
-
logger.info(`[xiaoyou]
|
|
233
|
+
logger.info(`[xiaoyou] streaming completed to ${conversationId} (${chunkSeq} blocks)${inboundMessageId ? ` (replyTo=${inboundMessageId})` : ""}`);
|
|
202
234
|
}
|
|
203
235
|
},
|
|
204
236
|
},
|
|
205
237
|
});
|
|
238
|
+
|
|
239
|
+
// dispatch resolve 后,如果 onComplete 未被调用(旧版本兼容),手动发 reply_end
|
|
240
|
+
if (!replyEndSent && chunkSeq > 0 && _client && _client.isConnected()) {
|
|
241
|
+
_client.sendReplyEnd({
|
|
242
|
+
type: "reply_end",
|
|
243
|
+
conversationId,
|
|
244
|
+
messageId: replyMessageId,
|
|
245
|
+
replyToMessageId: inboundMessageId,
|
|
246
|
+
fullText,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
});
|
|
249
|
+
logger.info(`[xiaoyou] streaming completed (fallback) to ${conversationId} (${chunkSeq} blocks)${inboundMessageId ? ` (replyTo=${inboundMessageId})` : ""}`);
|
|
250
|
+
} else if (chunkSeq === 0) {
|
|
251
|
+
logger.warn(`[xiaoyou] no reply generated for ${conversationId}`);
|
|
252
|
+
}
|
|
206
253
|
},
|
|
207
254
|
});
|
|
208
255
|
|
package/src/enterprise-client.ts
CHANGED
|
@@ -11,6 +11,8 @@ import type {
|
|
|
11
11
|
Frame,
|
|
12
12
|
InboundMessage,
|
|
13
13
|
OutboundReply,
|
|
14
|
+
OutboundReplyChunk,
|
|
15
|
+
OutboundReplyEnd,
|
|
14
16
|
PongFrame,
|
|
15
17
|
} from "./types.js";
|
|
16
18
|
|
|
@@ -31,6 +33,8 @@ export type EnterpriseClient = {
|
|
|
31
33
|
connect: () => void;
|
|
32
34
|
disconnect: () => void;
|
|
33
35
|
sendReply: (reply: OutboundReply) => boolean;
|
|
36
|
+
sendChunk: (chunk: OutboundReplyChunk) => boolean;
|
|
37
|
+
sendReplyEnd: (end: OutboundReplyEnd) => boolean;
|
|
34
38
|
isConnected: () => boolean;
|
|
35
39
|
};
|
|
36
40
|
|
|
@@ -182,9 +186,27 @@ export function createEnterpriseClient(opts: EnterpriseClientOptions): Enterpris
|
|
|
182
186
|
return true;
|
|
183
187
|
}
|
|
184
188
|
|
|
189
|
+
function sendChunk(chunk: OutboundReplyChunk): boolean {
|
|
190
|
+
if (!ws || ws.readyState !== WebSocket.OPEN || !authenticated) {
|
|
191
|
+
logger.warn("[xiaoyou] cannot send chunk: not connected");
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
ws.send(JSON.stringify(chunk));
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function sendReplyEnd(end: OutboundReplyEnd): boolean {
|
|
199
|
+
if (!ws || ws.readyState !== WebSocket.OPEN || !authenticated) {
|
|
200
|
+
logger.warn("[xiaoyou] cannot send reply_end: not connected");
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
ws.send(JSON.stringify(end));
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
185
207
|
function isConnected(): boolean {
|
|
186
208
|
return ws !== null && ws.readyState === WebSocket.OPEN && authenticated;
|
|
187
209
|
}
|
|
188
210
|
|
|
189
|
-
return { connect, disconnect, sendReply, isConnected };
|
|
211
|
+
return { connect, disconnect, sendReply, sendChunk, sendReplyEnd, isConnected };
|
|
190
212
|
}
|
package/src/types.ts
CHANGED
|
@@ -58,6 +58,33 @@ export type OutboundReply = {
|
|
|
58
58
|
timestamp: number;
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
/** 流式回复片段(插件 → 企业服务) */
|
|
62
|
+
export type OutboundReplyChunk = {
|
|
63
|
+
type: "reply_chunk";
|
|
64
|
+
conversationId: string;
|
|
65
|
+
messageId: string;
|
|
66
|
+
replyToMessageId?: string;
|
|
67
|
+
/** 本次增量文本 */
|
|
68
|
+
delta: string;
|
|
69
|
+
/** 该 chunk 在整条回复中的序号(从 0 开始) */
|
|
70
|
+
seq: number;
|
|
71
|
+
agentId?: string;
|
|
72
|
+
timestamp: number;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** 流式回复结束标记(插件 → 企业服务) */
|
|
76
|
+
export type OutboundReplyEnd = {
|
|
77
|
+
type: "reply_end";
|
|
78
|
+
conversationId: string;
|
|
79
|
+
messageId: string;
|
|
80
|
+
replyToMessageId?: string;
|
|
81
|
+
/** 完整文本(可选,方便企业侧校验拼接结果) */
|
|
82
|
+
fullText?: string;
|
|
83
|
+
mediaUrls?: string[];
|
|
84
|
+
agentId?: string;
|
|
85
|
+
timestamp: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
61
88
|
// ─── 控制帧 ─────────────────────────────────────────────
|
|
62
89
|
|
|
63
90
|
export type AuthFrame = {
|
|
@@ -81,6 +108,8 @@ export type PongFrame = { type: "pong"; seq: number; ts: number };
|
|
|
81
108
|
export type Frame =
|
|
82
109
|
| InboundMessage
|
|
83
110
|
| OutboundReply
|
|
111
|
+
| OutboundReplyChunk
|
|
112
|
+
| OutboundReplyEnd
|
|
84
113
|
| AuthFrame
|
|
85
114
|
| AuthResultFrame
|
|
86
115
|
| PingFrame
|