openclaw-xiaoyou 1.0.0 → 1.0.2
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 +0 -26
- package/docs/{Xiaoyou-SKILL.md → SKILL.md} +9 -10
- package/docs/communication-flow.md +12 -53
- package/docs/install-xiaoyou.sh +9 -5
- package/docs/user-comand.md +16 -0
- package/index.ts +1 -2
- package/openclaw.plugin.json +53 -12
- package/package.json +16 -1
- package/src/channel.test.ts +7 -86
- package/src/channel.ts +24 -50
- package/src/enterprise-client.ts +1 -1
- package/src/types.ts +0 -1
package/README.md
CHANGED
|
@@ -85,32 +85,6 @@ openclaw channels add --channel xiaoyou
|
|
|
85
85
|
| `heartbeatIntervalMs` | number | 否 | 30000 | 心跳间隔(ms) |
|
|
86
86
|
| `heartbeatTimeoutMs` | number | 否 | 10000 | 心跳超时(ms) |
|
|
87
87
|
|
|
88
|
-
### 多账号
|
|
89
|
-
|
|
90
|
-
同时连接多个企业服务:
|
|
91
|
-
|
|
92
|
-
```json
|
|
93
|
-
{
|
|
94
|
-
"channels": {
|
|
95
|
-
"xiaoyou": {
|
|
96
|
-
"enabled": true,
|
|
97
|
-
"wsUrl": "wss://default.corp.com/ws",
|
|
98
|
-
"authToken": "default-token",
|
|
99
|
-
"accounts": {
|
|
100
|
-
"sales": {
|
|
101
|
-
"wsUrl": "wss://sales.corp.com/ws",
|
|
102
|
-
"authToken": "sales-token"
|
|
103
|
-
},
|
|
104
|
-
"support": {
|
|
105
|
-
"wsUrl": "wss://support.corp.com/ws",
|
|
106
|
-
"authToken": "support-token"
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
88
|
## CLI 命令
|
|
115
89
|
|
|
116
90
|
```bash
|
|
@@ -8,7 +8,7 @@ description: Use when users want to install, upgrade, uninstall, configure, or t
|
|
|
8
8
|
小优 channel 安装、升级、卸载统一使用远程脚本:
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
|
-
https://
|
|
11
|
+
https://d10.tfdl.net/public/2026-05-06/e466d49d-4c5c-4fca-88dd-d69a38aec3ba/install-xiaoyou.sh
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
不要把 OpenClaw 的底层安装命令拆散执行。除非远程脚本失败且需要排障,否则直接调用 `install-xiaoyou.sh`。
|
|
@@ -28,14 +28,14 @@ https://aitest.haiersmarthomes.com/aiservice/claw/install-xiaoyou.sh
|
|
|
28
28
|
优先直接通过 `curl | bash` 执行远程脚本:
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
curl -fsSL https://
|
|
31
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/e466d49d-4c5c-4fca-88dd-d69a38aec3ba/install-xiaoyou.sh | bash -s -- --ws-url ws://aiws-sim.haiersmarthomes.com:11055/xiaoyou/claw --token <AccessToken>
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
### 参数说明
|
|
35
35
|
|
|
36
36
|
| 参数 | 必填 | 说明 |
|
|
37
37
|
|------|------|------|
|
|
38
|
-
| `--ws-url <url>` | ✅ 首次安装 | 企业 WebSocket
|
|
38
|
+
| `--ws-url <url>` | ✅ 首次安装 | 企业 WebSocket 服务地址,默认 `ws://aiws-sim.haiersmarthomes.com:11055/xiaoyou/claw` |
|
|
39
39
|
| `--token <token>` | ✅ 首次安装 | 企业服务认证 Token |
|
|
40
40
|
| `--version <ver>` | 否 | 指定 openclaw-xiaoyou 版本,默认 latest |
|
|
41
41
|
| `--check-only` | 否 | 只输出计划,不执行真实改动 |
|
|
@@ -46,7 +46,7 @@ curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiaoyou.sh
|
|
|
46
46
|
用户必须提供 `--ws-url` 和 `--token`:
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
curl -fsSL https://
|
|
49
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/e466d49d-4c5c-4fca-88dd-d69a38aec3ba/install-xiaoyou.sh | bash -s -- --ws-url ws://aiws-sim.haiersmarthomes.com:11055/xiaoyou/claw --token my-enterprise-token
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
### 升级
|
|
@@ -54,7 +54,7 @@ curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiaoyou.sh
|
|
|
54
54
|
已安装后的升级可以不传参数,脚本会复用现有配置:
|
|
55
55
|
|
|
56
56
|
```bash
|
|
57
|
-
curl -fsSL https://
|
|
57
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/e466d49d-4c5c-4fca-88dd-d69a38aec3ba/install-xiaoyou.sh | bash -s --
|
|
58
58
|
```
|
|
59
59
|
|
|
60
60
|
如果升级时传入 `--ws-url` 或 `--token`,会覆盖现有配置。
|
|
@@ -62,13 +62,13 @@ curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiaoyou.sh
|
|
|
62
62
|
### 指定版本
|
|
63
63
|
|
|
64
64
|
```bash
|
|
65
|
-
curl -fsSL https://
|
|
65
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/e466d49d-4c5c-4fca-88dd-d69a38aec3ba/install-xiaoyou.sh | bash -s -- --version 1.0.2 --ws-url ws://aiws-sim.haiersmarthomes.com:11055/xiaoyou/claw --token my-token
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
## 卸载
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
curl -fsSL https://
|
|
71
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/e466d49d-4c5c-4fca-88dd-d69a38aec3ba/install-xiaoyou.sh | bash -s -- uninstall
|
|
72
72
|
```
|
|
73
73
|
|
|
74
74
|
卸载会删除 xiaoyou channel 配置、插件记录和扩展目录。
|
|
@@ -76,8 +76,8 @@ curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiaoyou.sh
|
|
|
76
76
|
## 预检
|
|
77
77
|
|
|
78
78
|
```bash
|
|
79
|
-
curl -fsSL https://
|
|
80
|
-
curl -fsSL https://
|
|
79
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/e466d49d-4c5c-4fca-88dd-d69a38aec3ba/install-xiaoyou.sh | bash -s -- --check-only
|
|
80
|
+
curl -fsSL https://d10.tfdl.net/public/2026-05-06/e466d49d-4c5c-4fca-88dd-d69a38aec3ba/install-xiaoyou.sh | bash -s -- uninstall --check-only
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
## 失败处理
|
|
@@ -86,7 +86,6 @@ curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiaoyou.sh
|
|
|
86
86
|
|
|
87
87
|
| 日志现象 | 处理 |
|
|
88
88
|
|----------|------|
|
|
89
|
-
| 缺少 `--ws-url` | 要求用户提供企业 WebSocket 服务地址 |
|
|
90
89
|
| 缺少 `--token` | 要求用户提供企业服务认证 Token |
|
|
91
90
|
| 未检测到 OpenClaw | 提示用户先安装并启动 OpenClaw |
|
|
92
91
|
| npm latest 查询失败 | 可让脚本继续幂等安装,或指定 `--version` |
|
|
@@ -410,49 +410,9 @@ xiaoyou 插件采用 **Bridge 模式**:由插件主动向企业服务发起 We
|
|
|
410
410
|
└─────────────────────────────────────────────────────────────────┘
|
|
411
411
|
```
|
|
412
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. 异常处理与容错
|
|
413
|
+
## 7. 异常处理与容错
|
|
454
414
|
|
|
455
|
-
###
|
|
415
|
+
### 7.1 异常场景处理矩阵
|
|
456
416
|
|
|
457
417
|
```
|
|
458
418
|
┌──────────────────────┬─────────────────────────┬──────────────────────────┐
|
|
@@ -473,7 +433,7 @@ xiaoyou 支持同时连接多个企业服务(如销售系统 + 客服系统)
|
|
|
473
433
|
└──────────────────────┴─────────────────────────┴──────────────────────────┘
|
|
474
434
|
```
|
|
475
435
|
|
|
476
|
-
###
|
|
436
|
+
### 7.2 重连时序
|
|
477
437
|
|
|
478
438
|
```
|
|
479
439
|
时间轴 ──────────────────────────────────────────────────────▶
|
|
@@ -490,7 +450,7 @@ xiaoyou 支持同时连接多个企业服务(如销售系统 + 客服系统)
|
|
|
490
450
|
═══════════════▶
|
|
491
451
|
```
|
|
492
452
|
|
|
493
|
-
##
|
|
453
|
+
## 8. 企业服务端实现要点
|
|
494
454
|
|
|
495
455
|
企业侧需要实现一个 WebSocket Server,处理以下职责:
|
|
496
456
|
|
|
@@ -523,7 +483,7 @@ xiaoyou 支持同时连接多个企业服务(如销售系统 + 客服系统)
|
|
|
523
483
|
└─────────────────────────────────────────────────────────┘
|
|
524
484
|
```
|
|
525
485
|
|
|
526
|
-
###
|
|
486
|
+
### 8.2 企业服务端伪代码示例
|
|
527
487
|
|
|
528
488
|
```python
|
|
529
489
|
# Python 示例 (FastAPI + websockets)
|
|
@@ -586,9 +546,9 @@ async def on_user_message(ws, user_msg):
|
|
|
586
546
|
})
|
|
587
547
|
```
|
|
588
548
|
|
|
589
|
-
##
|
|
549
|
+
## 9. 部署拓扑
|
|
590
550
|
|
|
591
|
-
###
|
|
551
|
+
### 9.1 最简部署(单机)
|
|
592
552
|
|
|
593
553
|
```
|
|
594
554
|
┌─────────────────────────────────────────────┐
|
|
@@ -604,7 +564,7 @@ async def on_user_message(ws, user_msg):
|
|
|
604
564
|
└─────────────────────────────────────────────┘
|
|
605
565
|
```
|
|
606
566
|
|
|
607
|
-
###
|
|
567
|
+
### 9.2 生产部署(分离)
|
|
608
568
|
|
|
609
569
|
```
|
|
610
570
|
┌──────────────────┐ ┌──────────────────┐
|
|
@@ -622,7 +582,7 @@ async def on_user_message(ws, user_msg):
|
|
|
622
582
|
- xiaoyou 的重连机制可以应对单节点故障
|
|
623
583
|
```
|
|
624
584
|
|
|
625
|
-
##
|
|
585
|
+
## 10. 快速对照表
|
|
626
586
|
|
|
627
587
|
| 你想知道... | 看这里 |
|
|
628
588
|
|------------|--------|
|
|
@@ -633,7 +593,6 @@ async def on_user_message(ws, user_msg):
|
|
|
633
593
|
| Agent 回复怎么送回用户? | §4.1 步骤 ⑨⑩⑪ |
|
|
634
594
|
| WebSocket 帧格式是什么? | §5 协议帧参考 |
|
|
635
595
|
| 插件内部怎么组织的? | §6 插件内部模块交互 |
|
|
636
|
-
|
|
|
637
|
-
|
|
|
638
|
-
|
|
|
639
|
-
| 怎么部署? | §10 部署拓扑 |
|
|
596
|
+
| 出了问题怎么处理? | §7 异常处理与容错 |
|
|
597
|
+
| 企业侧要实现什么? | §8 企业服务端实现要点 |
|
|
598
|
+
| 怎么部署? | §9 部署拓扑 |
|
package/docs/install-xiaoyou.sh
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
# install-xiayou.sh — OpenClaw xiaoyou channel 一键安装/升级/卸载脚本
|
|
3
3
|
#
|
|
4
4
|
# 用法:
|
|
5
|
+
# install-xiayou.sh --token <token>
|
|
5
6
|
# install-xiayou.sh --ws-url <url> --token <token>
|
|
6
|
-
# install-xiayou.sh --version <ver> --
|
|
7
|
+
# install-xiayou.sh --version <ver> --token <token>
|
|
7
8
|
# install-xiayou.sh uninstall
|
|
8
9
|
# install-xiayou.sh --check-only
|
|
9
10
|
|
|
@@ -18,6 +19,7 @@ fi
|
|
|
18
19
|
set -Eeuo pipefail
|
|
19
20
|
|
|
20
21
|
SCRIPT_NAME="$(basename "$0")"
|
|
22
|
+
DEFAULT_WS_URL="ws://aiws-sim.haiersmarthomes.com:11055/xiaoyou/claw"
|
|
21
23
|
WS_URL=""
|
|
22
24
|
TOKEN=""
|
|
23
25
|
CHECK_ONLY=0
|
|
@@ -89,13 +91,14 @@ capture_timeout_or_empty() {
|
|
|
89
91
|
usage() {
|
|
90
92
|
cat <<'USAGE'
|
|
91
93
|
Usage:
|
|
94
|
+
install-xiayou.sh --token <token>
|
|
92
95
|
install-xiayou.sh --ws-url <url> --token <token>
|
|
93
|
-
install-xiayou.sh --version <ver> --
|
|
96
|
+
install-xiayou.sh --version <ver> --token <token>
|
|
94
97
|
install-xiayou.sh uninstall
|
|
95
98
|
install-xiayou.sh --check-only
|
|
96
99
|
|
|
97
100
|
Options:
|
|
98
|
-
--ws-url <url> 企业 WebSocket 服务地址 (
|
|
101
|
+
--ws-url <url> 企业 WebSocket 服务地址 (默认 ws://aiws-sim.haiersmarthomes.com:11055/xiaoyou/claw)
|
|
99
102
|
--token <token> 企业服务认证 Token
|
|
100
103
|
--version <ver> 指定 openclaw-xiaoyou 版本 (默认 latest)
|
|
101
104
|
--check-only 只输出计划,不执行
|
|
@@ -263,10 +266,11 @@ install_or_upgrade() {
|
|
|
263
266
|
fi
|
|
264
267
|
fi
|
|
265
268
|
|
|
266
|
-
#
|
|
269
|
+
# 首次安装:ws-url 未提供时使用默认值
|
|
267
270
|
if [[ -z "$current" && "$CHECK_ONLY" -eq 0 ]]; then
|
|
268
271
|
if [[ -z "$WS_URL" ]]; then
|
|
269
|
-
|
|
272
|
+
WS_URL="$DEFAULT_WS_URL"
|
|
273
|
+
log "未指定 --ws-url,使用默认地址: $WS_URL"
|
|
270
274
|
fi
|
|
271
275
|
if [[ -z "$TOKEN" ]]; then
|
|
272
276
|
die "首次安装必须提供 --token。请运行: $SCRIPT_NAME --ws-url <url> --token <token>"
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
/aiysnfs/nginx/ssl/aiservice/html/
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
https://aitest.haiersmarthomes.com:11001/skills/upload/emotion/robot_emotion_v1.0.tar.gz
|
|
6
|
+
|
|
7
|
+
验收环境位置:
|
|
8
|
+
/aiysnfs/nginx/share/skills/upload/claw
|
|
9
|
+
仿真环境位置:
|
|
10
|
+
/ai-sim/export/nginx/share/skills/upload/claw
|
|
11
|
+
生产环境位置:
|
|
12
|
+
/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
|
package/index.ts
CHANGED
|
@@ -35,7 +35,7 @@ const plugin = {
|
|
|
35
35
|
.description("Show enterprise WebSocket connection status")
|
|
36
36
|
.action(async () => {
|
|
37
37
|
const status = await xiayouPlugin.status.describe({
|
|
38
|
-
account: {
|
|
38
|
+
account: {},
|
|
39
39
|
});
|
|
40
40
|
console.log(JSON.stringify(status, null, 2));
|
|
41
41
|
});
|
|
@@ -51,7 +51,6 @@ const plugin = {
|
|
|
51
51
|
return;
|
|
52
52
|
}
|
|
53
53
|
const result = await xiayouPlugin.outbound.send({
|
|
54
|
-
account: { accountId: "default" },
|
|
55
54
|
to: opts.to,
|
|
56
55
|
payload: { kind: "text", text: opts.text },
|
|
57
56
|
});
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,6 +3,59 @@
|
|
|
3
3
|
"name": "Xiaoyou",
|
|
4
4
|
"description": "Bridge OpenClaw to enterprise services via persistent WebSocket connection",
|
|
5
5
|
"version": "1.0.0",
|
|
6
|
+
"configSchema": {
|
|
7
|
+
"type": "object",
|
|
8
|
+
"required": ["wsUrl", "authToken"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"enabled": {
|
|
11
|
+
"type": "boolean",
|
|
12
|
+
"default": false,
|
|
13
|
+
"description": "启用/禁用此 channel"
|
|
14
|
+
},
|
|
15
|
+
"wsUrl": {
|
|
16
|
+
"type": "string",
|
|
17
|
+
"description": "企业 WebSocket 服务地址",
|
|
18
|
+
"examples": ["wss://im.corp.example.com/ws", "ws://192.168.1.100:9090"]
|
|
19
|
+
},
|
|
20
|
+
"authToken": {
|
|
21
|
+
"type": "string",
|
|
22
|
+
"sensitive": true,
|
|
23
|
+
"description": "连接企业服务的认证 Token"
|
|
24
|
+
},
|
|
25
|
+
"dmSecurity": {
|
|
26
|
+
"type": "string",
|
|
27
|
+
"enum": ["open", "allowlist"],
|
|
28
|
+
"default": "open",
|
|
29
|
+
"description": "DM 安全策略。open=允许所有用户,allowlist=仅白名单用户"
|
|
30
|
+
},
|
|
31
|
+
"allowFrom": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": { "type": "string" },
|
|
34
|
+
"default": [],
|
|
35
|
+
"description": "白名单用户 ID 列表(dmSecurity=allowlist 时生效)"
|
|
36
|
+
},
|
|
37
|
+
"reconnectIntervalMs": {
|
|
38
|
+
"type": "number",
|
|
39
|
+
"default": 3000,
|
|
40
|
+
"description": "重连基础间隔(毫秒),实际按指数退避递增"
|
|
41
|
+
},
|
|
42
|
+
"maxReconnectAttempts": {
|
|
43
|
+
"type": "number",
|
|
44
|
+
"default": 0,
|
|
45
|
+
"description": "最大重连次数,0=无限重试"
|
|
46
|
+
},
|
|
47
|
+
"heartbeatIntervalMs": {
|
|
48
|
+
"type": "number",
|
|
49
|
+
"default": 30000,
|
|
50
|
+
"description": "心跳发送间隔(毫秒)"
|
|
51
|
+
},
|
|
52
|
+
"heartbeatTimeoutMs": {
|
|
53
|
+
"type": "number",
|
|
54
|
+
"default": 10000,
|
|
55
|
+
"description": "心跳超时时间(毫秒),超时则断开重连"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
},
|
|
6
59
|
"channel": {
|
|
7
60
|
"id": "xiaoyou",
|
|
8
61
|
"configSchema": {
|
|
@@ -55,18 +108,6 @@
|
|
|
55
108
|
"type": "number",
|
|
56
109
|
"default": 10000,
|
|
57
110
|
"description": "心跳超时时间(毫秒),超时则断开重连"
|
|
58
|
-
},
|
|
59
|
-
"accounts": {
|
|
60
|
-
"type": "object",
|
|
61
|
-
"description": "多账号配置,key 为 accountId,值为上述字段的覆盖",
|
|
62
|
-
"additionalProperties": {
|
|
63
|
-
"type": "object",
|
|
64
|
-
"properties": {
|
|
65
|
-
"wsUrl": { "type": "string" },
|
|
66
|
-
"authToken": { "type": "string", "sensitive": true },
|
|
67
|
-
"allowFrom": { "type": "array", "items": { "type": "string" } }
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
111
|
}
|
|
71
112
|
}
|
|
72
113
|
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-xiaoyou",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Xiaoyou channel plugin for OpenClaw — connects enterprise services via persistent outbound WebSocket",
|
|
6
6
|
"openclaw": {
|
|
7
7
|
"extensions": ["./index.ts"],
|
|
8
8
|
"setupEntry": "./setup-entry.ts",
|
|
9
|
+
"configSchema": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["wsUrl", "authToken"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"enabled": { "type": "boolean", "default": false },
|
|
14
|
+
"wsUrl": { "type": "string", "description": "企业 WebSocket 服务地址" },
|
|
15
|
+
"authToken": { "type": "string", "sensitive": true, "description": "连接企业服务的认证 Token" },
|
|
16
|
+
"dmSecurity": { "type": "string", "enum": ["open", "allowlist"], "default": "open" },
|
|
17
|
+
"allowFrom": { "type": "array", "items": { "type": "string" }, "default": [] },
|
|
18
|
+
"reconnectIntervalMs": { "type": "number", "default": 3000 },
|
|
19
|
+
"maxReconnectAttempts": { "type": "number", "default": 0 },
|
|
20
|
+
"heartbeatIntervalMs": { "type": "number", "default": 30000 },
|
|
21
|
+
"heartbeatTimeoutMs": { "type": "number", "default": 10000 }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
9
24
|
"channel": {
|
|
10
25
|
"id": "xiaoyou",
|
|
11
26
|
"label": "Xiaoyou",
|
package/src/channel.test.ts
CHANGED
|
@@ -15,8 +15,8 @@ const fullCfg = {
|
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
describe("config adapter", () => {
|
|
18
|
-
it("resolves
|
|
19
|
-
const acct = xiayouPlugin.config.resolveAccount(fullCfg
|
|
18
|
+
it("resolves config with all fields", () => {
|
|
19
|
+
const acct = xiayouPlugin.config.resolveAccount(fullCfg);
|
|
20
20
|
expect(acct.wsUrl).toBe("wss://im.corp.example.com/ws");
|
|
21
21
|
expect(acct.authToken).toBe("test-token-123");
|
|
22
22
|
expect(acct.allowFrom).toEqual(["user-1", "user-2"]);
|
|
@@ -27,7 +27,7 @@ describe("config adapter", () => {
|
|
|
27
27
|
|
|
28
28
|
it("applies defaults for optional fields", () => {
|
|
29
29
|
const cfg = { channels: { xiaoyou: { wsUrl: "ws://localhost:9090", authToken: "t" } } };
|
|
30
|
-
const acct = xiayouPlugin.config.resolveAccount(cfg
|
|
30
|
+
const acct = xiayouPlugin.config.resolveAccount(cfg);
|
|
31
31
|
expect(acct.reconnectIntervalMs).toBe(3000);
|
|
32
32
|
expect(acct.maxReconnectAttempts).toBe(0);
|
|
33
33
|
expect(acct.heartbeatIntervalMs).toBe(30000);
|
|
@@ -35,106 +35,27 @@ describe("config adapter", () => {
|
|
|
35
35
|
});
|
|
36
36
|
|
|
37
37
|
it("throws when wsUrl missing", () => {
|
|
38
|
-
expect(() => xiayouPlugin.config.resolveAccount({ channels: { xiaoyou: {} } }
|
|
38
|
+
expect(() => xiayouPlugin.config.resolveAccount({ channels: { xiaoyou: {} } }))
|
|
39
39
|
.toThrow("wsUrl is required");
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
it("throws when section missing", () => {
|
|
43
|
-
expect(() => xiayouPlugin.config.resolveAccount({ channels: {} }
|
|
43
|
+
expect(() => xiayouPlugin.config.resolveAccount({ channels: {} }))
|
|
44
44
|
.toThrow("config section not found");
|
|
45
45
|
});
|
|
46
|
-
|
|
47
|
-
it("resolves multi-account override", () => {
|
|
48
|
-
const cfg = {
|
|
49
|
-
channels: {
|
|
50
|
-
xiaoyou: {
|
|
51
|
-
wsUrl: "ws://default/ws",
|
|
52
|
-
authToken: "default-token",
|
|
53
|
-
accounts: {
|
|
54
|
-
sales: { wsUrl: "ws://sales/ws", authToken: "sales-token" },
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
const acct = xiayouPlugin.config.resolveAccount(cfg, "sales");
|
|
60
|
-
expect(acct.wsUrl).toBe("ws://sales/ws");
|
|
61
|
-
expect(acct.authToken).toBe("sales-token");
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("falls back to default when accountId not in accounts", () => {
|
|
65
|
-
const cfg = {
|
|
66
|
-
channels: {
|
|
67
|
-
xiaoyou: {
|
|
68
|
-
wsUrl: "ws://default/ws",
|
|
69
|
-
authToken: "default-token",
|
|
70
|
-
accounts: { sales: { wsUrl: "ws://sales/ws" } },
|
|
71
|
-
},
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
const acct = xiayouPlugin.config.resolveAccount(cfg, "unknown");
|
|
75
|
-
expect(acct.wsUrl).toBe("ws://default/ws");
|
|
76
|
-
});
|
|
77
46
|
});
|
|
78
47
|
|
|
79
48
|
describe("inspectAccount", () => {
|
|
80
49
|
it("reports configured", () => {
|
|
81
|
-
const r = xiayouPlugin.config.inspectAccount(fullCfg
|
|
50
|
+
const r = xiayouPlugin.config.inspectAccount(fullCfg);
|
|
82
51
|
expect(r.enabled).toBe(true);
|
|
83
52
|
expect(r.configured).toBe(true);
|
|
84
53
|
expect(r.tokenStatus).toBe("available");
|
|
85
54
|
});
|
|
86
55
|
|
|
87
56
|
it("reports not configured", () => {
|
|
88
|
-
const r = xiayouPlugin.config.inspectAccount({ channels: {} }
|
|
57
|
+
const r = xiayouPlugin.config.inspectAccount({ channels: {} });
|
|
89
58
|
expect(r.enabled).toBe(false);
|
|
90
59
|
expect(r.configured).toBe(false);
|
|
91
60
|
});
|
|
92
61
|
});
|
|
93
|
-
|
|
94
|
-
describe("listAccounts", () => {
|
|
95
|
-
it("returns default when no accounts section", () => {
|
|
96
|
-
expect(xiayouPlugin.config.listAccounts(fullCfg)).toEqual(["default"]);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it("returns account keys", () => {
|
|
100
|
-
const cfg = {
|
|
101
|
-
channels: {
|
|
102
|
-
xiaoyou: {
|
|
103
|
-
wsUrl: "ws://x",
|
|
104
|
-
accounts: { a: {}, b: {} },
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
};
|
|
108
|
-
expect(xiayouPlugin.config.listAccounts(cfg)).toEqual(["a", "b"]);
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it("returns empty when not configured", () => {
|
|
112
|
-
expect(xiayouPlugin.config.listAccounts({ channels: {} })).toEqual([]);
|
|
113
|
-
});
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
describe("capabilities", () => {
|
|
117
|
-
it("declares correct feature set", () => {
|
|
118
|
-
const c = xiayouPlugin.capabilities;
|
|
119
|
-
expect(c.media).toBe(true);
|
|
120
|
-
expect(c.reply).toBe(true);
|
|
121
|
-
expect(c.edit).toBe(false);
|
|
122
|
-
expect(c.polls).toBe(false);
|
|
123
|
-
expect(c.reactions).toBe(false);
|
|
124
|
-
});
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
describe("status adapter", () => {
|
|
128
|
-
it("reports issues when not connected", async () => {
|
|
129
|
-
const s = await xiayouPlugin.status.describe({
|
|
130
|
-
account: { accountId: "default", wsUrl: "ws://x", authToken: "t" },
|
|
131
|
-
});
|
|
132
|
-
expect(s.summary).toBe("error");
|
|
133
|
-
expect(s.issues.some((i: any) => i.message.includes("not connected"))).toBe(true);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("reports wsUrl missing", async () => {
|
|
137
|
-
const s = await xiayouPlugin.status.describe({ account: { accountId: "default" } });
|
|
138
|
-
expect(s.issues.some((i: any) => i.message.includes("wsUrl"))).toBe(true);
|
|
139
|
-
});
|
|
140
|
-
});
|
package/src/channel.ts
CHANGED
|
@@ -12,8 +12,7 @@ import { createEnterpriseClient, type EnterpriseClient } from "./enterprise-clie
|
|
|
12
12
|
|
|
13
13
|
// ─── 运行时状态 ──────────────────────────────────────
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
const clients = new Map<string, EnterpriseClient>();
|
|
15
|
+
let _client: EnterpriseClient | null = null;
|
|
17
16
|
|
|
18
17
|
/** OpenClaw runtime API 引用,由 index.ts register() 注入 */
|
|
19
18
|
let _runtime: any = null;
|
|
@@ -22,31 +21,24 @@ export function getRuntime() { return _runtime; }
|
|
|
22
21
|
|
|
23
22
|
// ─── Config Adapter ──────────────────────────────────
|
|
24
23
|
|
|
25
|
-
function resolveAccount(cfg: any
|
|
24
|
+
function resolveAccount(cfg: any): ResolvedAccount {
|
|
26
25
|
const section = cfg.channels?.["xiaoyou"];
|
|
27
26
|
if (!section) throw new Error("xiaoyou: channel config section not found");
|
|
28
|
-
|
|
29
|
-
// 多账号支持:accounts.<id> 覆盖顶层默认值
|
|
30
|
-
const acct = accountId && section.accounts?.[accountId]
|
|
31
|
-
? { ...section, ...section.accounts[accountId] }
|
|
32
|
-
: section;
|
|
33
|
-
|
|
34
|
-
if (!acct.wsUrl) throw new Error("xiaoyou: wsUrl is required");
|
|
27
|
+
if (!section.wsUrl) throw new Error("xiaoyou: wsUrl is required");
|
|
35
28
|
|
|
36
29
|
return {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
heartbeatTimeoutMs: acct.heartbeatTimeoutMs ?? 10000,
|
|
30
|
+
wsUrl: section.wsUrl,
|
|
31
|
+
authToken: section.authToken ?? "",
|
|
32
|
+
allowFrom: section.allowFrom ?? [],
|
|
33
|
+
dmPolicy: section.dmSecurity,
|
|
34
|
+
reconnectIntervalMs: section.reconnectIntervalMs ?? 3000,
|
|
35
|
+
maxReconnectAttempts: section.maxReconnectAttempts ?? 0,
|
|
36
|
+
heartbeatIntervalMs: section.heartbeatIntervalMs ?? 30000,
|
|
37
|
+
heartbeatTimeoutMs: section.heartbeatTimeoutMs ?? 10000,
|
|
46
38
|
};
|
|
47
39
|
}
|
|
48
40
|
|
|
49
|
-
function inspectAccount(cfg: any
|
|
41
|
+
function inspectAccount(cfg: any) {
|
|
50
42
|
const section = cfg.channels?.["xiaoyou"];
|
|
51
43
|
return {
|
|
52
44
|
enabled: Boolean(section?.wsUrl),
|
|
@@ -55,13 +47,6 @@ function inspectAccount(cfg: any, _accountId?: string | null) {
|
|
|
55
47
|
};
|
|
56
48
|
}
|
|
57
49
|
|
|
58
|
-
function listAccounts(cfg: any): string[] {
|
|
59
|
-
const section = cfg.channels?.["xiaoyou"];
|
|
60
|
-
if (!section?.wsUrl) return [];
|
|
61
|
-
if (section.accounts) return Object.keys(section.accounts);
|
|
62
|
-
return ["default"];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
50
|
// ─── Channel Plugin ──────────────────────────────────
|
|
66
51
|
|
|
67
52
|
export const xiayouPlugin = {
|
|
@@ -94,7 +79,6 @@ export const xiayouPlugin = {
|
|
|
94
79
|
config: {
|
|
95
80
|
resolveAccount,
|
|
96
81
|
inspectAccount,
|
|
97
|
-
listAccounts,
|
|
98
82
|
},
|
|
99
83
|
|
|
100
84
|
// ── DM 安全策略 ────────────────────────────────────
|
|
@@ -116,13 +100,10 @@ export const xiayouPlugin = {
|
|
|
116
100
|
const resolved: ResolvedAccount =
|
|
117
101
|
typeof account.wsUrl === "string"
|
|
118
102
|
? account
|
|
119
|
-
: resolveAccount(config
|
|
120
|
-
|
|
121
|
-
const clientKey = resolved.accountId ?? "default";
|
|
103
|
+
: resolveAccount(config);
|
|
122
104
|
|
|
123
105
|
// 断开已有连接
|
|
124
|
-
|
|
125
|
-
if (existing) existing.disconnect();
|
|
106
|
+
if (_client) _client.disconnect();
|
|
126
107
|
|
|
127
108
|
const client = createEnterpriseClient({
|
|
128
109
|
account: resolved,
|
|
@@ -135,7 +116,6 @@ export const xiayouPlugin = {
|
|
|
135
116
|
}
|
|
136
117
|
await runtime.inbound.dispatch({
|
|
137
118
|
channelId: "xiaoyou",
|
|
138
|
-
accountId: resolved.accountId ?? "default",
|
|
139
119
|
senderId: msg.senderId,
|
|
140
120
|
senderName: msg.senderName ?? msg.senderId,
|
|
141
121
|
conversationId: msg.conversationId,
|
|
@@ -152,25 +132,21 @@ export const xiayouPlugin = {
|
|
|
152
132
|
});
|
|
153
133
|
|
|
154
134
|
client.connect();
|
|
155
|
-
|
|
156
|
-
logger.info(
|
|
135
|
+
_client = client;
|
|
136
|
+
logger.info("[xiaoyou] gateway started");
|
|
157
137
|
return client;
|
|
158
138
|
},
|
|
159
139
|
|
|
160
140
|
stop: async (client: EnterpriseClient) => {
|
|
161
141
|
client.disconnect();
|
|
162
|
-
|
|
163
|
-
if (c === client) { clients.delete(key); break; }
|
|
164
|
-
}
|
|
142
|
+
if (_client === client) _client = null;
|
|
165
143
|
},
|
|
166
144
|
},
|
|
167
145
|
|
|
168
146
|
// ── Outbound 出站 ──────────────────────────────────
|
|
169
147
|
outbound: {
|
|
170
|
-
send: async ({
|
|
171
|
-
|
|
172
|
-
const client = clients.get(clientKey);
|
|
173
|
-
if (!client || !client.isConnected()) {
|
|
148
|
+
send: async ({ to, payload }: any) => {
|
|
149
|
+
if (!_client || !_client.isConnected()) {
|
|
174
150
|
return { ok: false, error: "xiaoyou: not connected" };
|
|
175
151
|
}
|
|
176
152
|
|
|
@@ -183,11 +159,11 @@ export const xiayouPlugin = {
|
|
|
183
159
|
};
|
|
184
160
|
|
|
185
161
|
if (payload.kind === "text") {
|
|
186
|
-
|
|
162
|
+
_client.sendReply({ ...baseReply, text: payload.text });
|
|
187
163
|
return { ok: true };
|
|
188
164
|
}
|
|
189
165
|
if (payload.kind === "image" || payload.kind === "file") {
|
|
190
|
-
|
|
166
|
+
_client.sendReply({
|
|
191
167
|
...baseReply,
|
|
192
168
|
text: payload.caption ?? "",
|
|
193
169
|
mediaUrls: [payload.url ?? payload.filePath],
|
|
@@ -201,13 +177,11 @@ export const xiayouPlugin = {
|
|
|
201
177
|
// ── Status 状态检查 ────────────────────────────────
|
|
202
178
|
status: {
|
|
203
179
|
describe: async ({ account }: any) => {
|
|
204
|
-
const clientKey = account.accountId ?? "default";
|
|
205
|
-
const client = clients.get(clientKey);
|
|
206
180
|
const issues: Array<{ severity: string; message: string }> = [];
|
|
207
181
|
|
|
208
|
-
if (!account
|
|
209
|
-
if (!account
|
|
210
|
-
if (!
|
|
182
|
+
if (!account?.wsUrl) issues.push({ severity: "error", message: "wsUrl not configured" });
|
|
183
|
+
if (!account?.authToken) issues.push({ severity: "warning", message: "authToken not set" });
|
|
184
|
+
if (!_client || !_client.isConnected()) issues.push({ severity: "error", message: "WebSocket not connected" });
|
|
211
185
|
|
|
212
186
|
return {
|
|
213
187
|
summary: issues.length === 0 ? "connected" : "error",
|
package/src/enterprise-client.ts
CHANGED
|
@@ -78,7 +78,7 @@ export function createEnterpriseClient(opts: EnterpriseClientOptions): Enterpris
|
|
|
78
78
|
ws.send(JSON.stringify({
|
|
79
79
|
type: "auth",
|
|
80
80
|
token: account.authToken,
|
|
81
|
-
clientId:
|
|
81
|
+
clientId: "openclaw-xiaoyou",
|
|
82
82
|
clientVersion: "1.0.0",
|
|
83
83
|
}));
|
|
84
84
|
logger.info("[xiaoyou] auth sent, waiting for auth_result...");
|