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,296 @@
|
|
|
1
|
+
# openclaw-xiaoyou 发布与部署指南
|
|
2
|
+
|
|
3
|
+
## 1. 整体流程
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
开发者侧 用户侧
|
|
7
|
+
───────── ──────
|
|
8
|
+
|
|
9
|
+
编写代码 在 OpenClaw 中输入口令
|
|
10
|
+
│ │
|
|
11
|
+
▼ ▼
|
|
12
|
+
npm run build Agent 读取 Xiaoyou-SKILL.md
|
|
13
|
+
│ │
|
|
14
|
+
▼ ▼
|
|
15
|
+
npm publish ──▶ npm 仓库 ◀── openclaw plugins install
|
|
16
|
+
│ │ │
|
|
17
|
+
▼ │ ▼
|
|
18
|
+
上传脚本到服务器 │ curl install-xiayou.sh
|
|
19
|
+
│ │
|
|
20
|
+
└──────────────────────────────────┘
|
|
21
|
+
脚本从 npm 拉取插件包
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## 2. 开发者侧:构建与发布
|
|
25
|
+
|
|
26
|
+
### 2.1 前置条件
|
|
27
|
+
|
|
28
|
+
- Node.js >= 18
|
|
29
|
+
- npm 账号(公共 npm)或私有仓库访问权限
|
|
30
|
+
- OpenClaw 开发环境(用于本地测试)
|
|
31
|
+
|
|
32
|
+
### 2.2 构建
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
cd openclaw-ws-bridge
|
|
36
|
+
|
|
37
|
+
# 安装依赖
|
|
38
|
+
npm install
|
|
39
|
+
|
|
40
|
+
# 编译 TypeScript → dist/
|
|
41
|
+
npm run build
|
|
42
|
+
|
|
43
|
+
# 运行测试确认
|
|
44
|
+
npm test
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
构建产物在 `dist/` 目录,包含编译后的 JS 文件和类型声明。
|
|
48
|
+
|
|
49
|
+
### 2.3 发布前检查
|
|
50
|
+
|
|
51
|
+
确认 `package.json` 中的关键字段:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"name": "openclaw-xiaoyou", ← npm 包名,用户安装时用
|
|
56
|
+
"version": "1.0.0", ← 每次发布前递增
|
|
57
|
+
"type": "module",
|
|
58
|
+
"openclaw": {
|
|
59
|
+
"extensions": ["./index.ts"],
|
|
60
|
+
"setupEntry": "./setup-entry.ts",
|
|
61
|
+
"channel": {
|
|
62
|
+
"id": "xiaoyou" ← OpenClaw 内部 channel 标识
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
确认 `.gitignore` 或 `.npmignore` 不会排除必要文件。npm 发布时需要包含:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
index.ts / dist/index.js
|
|
72
|
+
setup-entry.ts / dist/setup-entry.js
|
|
73
|
+
src/ 或 dist/src/
|
|
74
|
+
package.json
|
|
75
|
+
openclaw.plugin.json
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 2.4 发布到公共 npm
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# 首次使用需要登录
|
|
82
|
+
npm login
|
|
83
|
+
|
|
84
|
+
# 检查将要发布的文件
|
|
85
|
+
npm pack --dry-run
|
|
86
|
+
|
|
87
|
+
# 发布
|
|
88
|
+
npm publish
|
|
89
|
+
|
|
90
|
+
# 发布指定 tag(如 beta 版本)
|
|
91
|
+
npm publish --tag beta
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
发布成功后,任何人都可以通过以下方式安装:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
openclaw plugins install openclaw-xiaoyou
|
|
98
|
+
# 或
|
|
99
|
+
npm install openclaw-xiaoyou
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2.5 发布到私有 npm 仓库
|
|
103
|
+
|
|
104
|
+
如果使用企业私有仓库(Verdaccio / Nexus / GitHub Packages / 阿里云 npm 等):
|
|
105
|
+
|
|
106
|
+
#### 方式一:使用 scope(推荐)
|
|
107
|
+
|
|
108
|
+
修改 `package.json`:
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"name": "@yourorg/openclaw-xiaoyou",
|
|
113
|
+
"publishConfig": {
|
|
114
|
+
"registry": "https://npm.yourcompany.com/"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
同时修改 `install-xiayou.sh` 中的 `PLUGIN_NAME`:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
PLUGIN_NAME="@yourorg/openclaw-xiaoyou"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
发布:
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm publish --registry https://npm.yourcompany.com/
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
#### 方式二:不使用 scope
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# 临时指定 registry 发布
|
|
135
|
+
npm publish --registry https://npm.yourcompany.com/
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 2.6 版本管理
|
|
139
|
+
|
|
140
|
+
每次发布新版本前递增版本号:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# 补丁版本 1.0.0 → 1.0.1(bug 修复)
|
|
144
|
+
npm version patch
|
|
145
|
+
|
|
146
|
+
# 次版本 1.0.0 → 1.1.0(新功能)
|
|
147
|
+
npm version minor
|
|
148
|
+
|
|
149
|
+
# 主版本 1.0.0 → 2.0.0(破坏性变更)
|
|
150
|
+
npm version major
|
|
151
|
+
|
|
152
|
+
# 然后发布
|
|
153
|
+
npm publish
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## 3. 开发者侧:部署脚本和文档
|
|
157
|
+
|
|
158
|
+
### 3.1 需要上传到服务器的文件
|
|
159
|
+
|
|
160
|
+
将以下两个文件上传到 `https://aitest.haiersmarthomes.com/aiservice/claw/`:
|
|
161
|
+
|
|
162
|
+
| 文件 | 服务器路径 | 用途 |
|
|
163
|
+
|------|-----------|------|
|
|
164
|
+
| `install-xiayou.sh` | `/aiservice/claw/install-xiayou.sh` | 一键安装脚本 |
|
|
165
|
+
| `Xiaoyou-SKILL.md` | `/aiservice/claw/Xiaoyou-SKILL.md` | Agent 技能文档 |
|
|
166
|
+
|
|
167
|
+
上传后确认可访问:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
# 验证脚本可下载
|
|
171
|
+
curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiayou.sh | head -5
|
|
172
|
+
|
|
173
|
+
# 验证 SKILL 文档可下载
|
|
174
|
+
curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/Xiaoyou-SKILL.md | head -5
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 3.2 服务器目录结构
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
/aiservice/claw/
|
|
181
|
+
├── install-xiayou.sh # 安装脚本
|
|
182
|
+
└── Xiaoyou-SKILL.md # 技能文档
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
npm 仓库(公共或私有):
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
openclaw-xiaoyou@1.0.0 # 插件包(由 npm publish 发布)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
## 4. 用户侧:安装使用
|
|
192
|
+
|
|
193
|
+
### 4.1 通过 OpenClaw Agent 口令安装(推荐)
|
|
194
|
+
|
|
195
|
+
用户在 OpenClaw 对话中输入:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
按照 https://aitest.haiersmarthomes.com/aiservice/claw/Xiaoyou-SKILL.md 文档完成小优 channel 配置,
|
|
199
|
+
wsUrl 为 wss://im.corp.example.com/ws,token 为 my-enterprise-token
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Agent 会自动:
|
|
203
|
+
1. 读取 SKILL.md
|
|
204
|
+
2. 执行 `curl | bash` 安装脚本
|
|
205
|
+
3. 脚本从 npm 拉取 `openclaw-xiaoyou` 插件
|
|
206
|
+
4. 写入 channel 配置
|
|
207
|
+
|
|
208
|
+
### 4.2 通过命令行手动安装
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiayou.sh | bash -s -- \
|
|
212
|
+
--ws-url wss://im.corp.example.com/ws \
|
|
213
|
+
--token my-enterprise-token
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### 4.3 私有 npm 仓库的用户侧配置
|
|
217
|
+
|
|
218
|
+
如果插件发布在私有 npm,用户机器上需要先配置 registry:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# 方式一:全局配置(影响所有 npm 操作)
|
|
222
|
+
npm config set registry https://npm.yourcompany.com/
|
|
223
|
+
|
|
224
|
+
# 方式二:仅对 scope 配置(推荐,不影响其他包)
|
|
225
|
+
npm config set @yourorg:registry https://npm.yourcompany.com/
|
|
226
|
+
|
|
227
|
+
# 方式三:通过 .npmrc 文件(放在用户 home 目录)
|
|
228
|
+
echo "@yourorg:registry=https://npm.yourcompany.com/" >> ~/.npmrc
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
如果私有仓库需要认证:
|
|
232
|
+
|
|
233
|
+
```bash
|
|
234
|
+
# 登录私有仓库
|
|
235
|
+
npm login --registry https://npm.yourcompany.com/
|
|
236
|
+
|
|
237
|
+
# 或手动配置 token
|
|
238
|
+
echo "//npm.yourcompany.com/:_authToken=YOUR_TOKEN" >> ~/.npmrc
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
配置完成后,安装脚本中的 `openclaw plugins install` 就能自动从私有仓库拉包。
|
|
242
|
+
|
|
243
|
+
## 5. 更新发布流程
|
|
244
|
+
|
|
245
|
+
当插件代码有更新时:
|
|
246
|
+
|
|
247
|
+
```bash
|
|
248
|
+
# 1. 修改代码
|
|
249
|
+
# 2. 运行测试
|
|
250
|
+
npm test
|
|
251
|
+
|
|
252
|
+
# 3. 递增版本
|
|
253
|
+
npm version patch # 或 minor / major
|
|
254
|
+
|
|
255
|
+
# 4. 构建
|
|
256
|
+
npm run build
|
|
257
|
+
|
|
258
|
+
# 5. 发布
|
|
259
|
+
npm publish
|
|
260
|
+
|
|
261
|
+
# 6. 如果脚本或文档也有变更,重新上传到服务器
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
用户侧升级:
|
|
265
|
+
|
|
266
|
+
```bash
|
|
267
|
+
# 通过脚本升级(自动检测新版本)
|
|
268
|
+
curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiayou.sh | bash -s --
|
|
269
|
+
|
|
270
|
+
# 或指定版本
|
|
271
|
+
curl -fsSL https://aitest.haiersmarthomes.com/aiservice/claw/install-xiayou.sh | bash -s -- --version 1.1.0
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## 6. 完整操作清单
|
|
275
|
+
|
|
276
|
+
### 首次发布
|
|
277
|
+
|
|
278
|
+
```
|
|
279
|
+
□ npm install && npm run build && npm test
|
|
280
|
+
□ npm login(首次)
|
|
281
|
+
□ npm publish
|
|
282
|
+
□ 上传 install-xiayou.sh 到服务器
|
|
283
|
+
□ 上传 Xiaoyou-SKILL.md 到服务器
|
|
284
|
+
□ 验证 curl 可下载脚本和文档
|
|
285
|
+
□ 在测试环境执行安装脚本验证
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### 后续更新
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
□ 修改代码 → npm test
|
|
292
|
+
□ npm version patch/minor/major
|
|
293
|
+
□ npm run build
|
|
294
|
+
□ npm publish
|
|
295
|
+
□ 如有脚本/文档变更,重新上传到服务器
|
|
296
|
+
```
|
package/index.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openclaw-xiaoyou 插件入口
|
|
3
|
+
*
|
|
4
|
+
* 安装方式:
|
|
5
|
+
* openclaw plugins install openclaw-xiaoyou
|
|
6
|
+
* openclaw plugins enable openclaw-xiaoyou
|
|
7
|
+
*
|
|
8
|
+
* 配置方式:
|
|
9
|
+
* openclaw channels add --channel xiaoyou
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { xiayouPlugin, setRuntime } from "./src/channel.js";
|
|
13
|
+
|
|
14
|
+
const plugin = {
|
|
15
|
+
id: "xiaoyou",
|
|
16
|
+
name: "Xiaoyou",
|
|
17
|
+
description: "Bridge OpenClaw to enterprise services via persistent WebSocket connection",
|
|
18
|
+
|
|
19
|
+
register(api: any) {
|
|
20
|
+
// 注入 runtime,供 gateway/outbound 内部使用
|
|
21
|
+
setRuntime(api.runtime);
|
|
22
|
+
|
|
23
|
+
// 注册 channel
|
|
24
|
+
api.registerChannel({ plugin: xiayouPlugin });
|
|
25
|
+
|
|
26
|
+
// 注册 CLI 子命令
|
|
27
|
+
api.registerCli?.(
|
|
28
|
+
({ program }: any) => {
|
|
29
|
+
const cmd = program
|
|
30
|
+
.command("xiaoyou")
|
|
31
|
+
.description("Xiaoyou channel management");
|
|
32
|
+
|
|
33
|
+
cmd
|
|
34
|
+
.command("status")
|
|
35
|
+
.description("Show enterprise WebSocket connection status")
|
|
36
|
+
.action(async () => {
|
|
37
|
+
const status = await xiayouPlugin.status.describe({
|
|
38
|
+
account: { accountId: "default" },
|
|
39
|
+
});
|
|
40
|
+
console.log(JSON.stringify(status, null, 2));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
cmd
|
|
44
|
+
.command("test")
|
|
45
|
+
.description("Send a test reply through the bridge")
|
|
46
|
+
.option("--to <conversationId>", "Target conversation ID")
|
|
47
|
+
.option("--text <text>", "Message text", "Hello from OpenClaw!")
|
|
48
|
+
.action(async (opts: any) => {
|
|
49
|
+
if (!opts.to) {
|
|
50
|
+
console.error("--to <conversationId> is required");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const result = await xiayouPlugin.outbound.send({
|
|
54
|
+
account: { accountId: "default" },
|
|
55
|
+
to: opts.to,
|
|
56
|
+
payload: { kind: "text", text: opts.text },
|
|
57
|
+
});
|
|
58
|
+
console.log(JSON.stringify(result, null, 2));
|
|
59
|
+
});
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
descriptors: [
|
|
63
|
+
{ name: "xiaoyou", description: "Xiaoyou channel management", hasSubcommands: true },
|
|
64
|
+
],
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export default plugin;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "xiaoyou",
|
|
3
|
+
"name": "Xiaoyou",
|
|
4
|
+
"description": "Bridge OpenClaw to enterprise services via persistent WebSocket connection",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"channel": {
|
|
7
|
+
"id": "xiaoyou",
|
|
8
|
+
"configSchema": {
|
|
9
|
+
"type": "object",
|
|
10
|
+
"required": ["wsUrl", "authToken"],
|
|
11
|
+
"properties": {
|
|
12
|
+
"enabled": {
|
|
13
|
+
"type": "boolean",
|
|
14
|
+
"default": false,
|
|
15
|
+
"description": "启用/禁用此 channel"
|
|
16
|
+
},
|
|
17
|
+
"wsUrl": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "企业 WebSocket 服务地址",
|
|
20
|
+
"examples": ["wss://im.corp.example.com/ws", "ws://192.168.1.100:9090"]
|
|
21
|
+
},
|
|
22
|
+
"authToken": {
|
|
23
|
+
"type": "string",
|
|
24
|
+
"sensitive": true,
|
|
25
|
+
"description": "连接企业服务的认证 Token"
|
|
26
|
+
},
|
|
27
|
+
"dmSecurity": {
|
|
28
|
+
"type": "string",
|
|
29
|
+
"enum": ["open", "allowlist"],
|
|
30
|
+
"default": "open",
|
|
31
|
+
"description": "DM 安全策略。open=允许所有用户,allowlist=仅白名单用户"
|
|
32
|
+
},
|
|
33
|
+
"allowFrom": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": { "type": "string" },
|
|
36
|
+
"default": [],
|
|
37
|
+
"description": "白名单用户 ID 列表(dmSecurity=allowlist 时生效)"
|
|
38
|
+
},
|
|
39
|
+
"reconnectIntervalMs": {
|
|
40
|
+
"type": "number",
|
|
41
|
+
"default": 3000,
|
|
42
|
+
"description": "重连基础间隔(毫秒),实际按指数退避递增"
|
|
43
|
+
},
|
|
44
|
+
"maxReconnectAttempts": {
|
|
45
|
+
"type": "number",
|
|
46
|
+
"default": 0,
|
|
47
|
+
"description": "最大重连次数,0=无限重试"
|
|
48
|
+
},
|
|
49
|
+
"heartbeatIntervalMs": {
|
|
50
|
+
"type": "number",
|
|
51
|
+
"default": 30000,
|
|
52
|
+
"description": "心跳发送间隔(毫秒)"
|
|
53
|
+
},
|
|
54
|
+
"heartbeatTimeoutMs": {
|
|
55
|
+
"type": "number",
|
|
56
|
+
"default": 10000,
|
|
57
|
+
"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
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "openclaw-xiaoyou",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Xiaoyou channel plugin for OpenClaw — connects enterprise services via persistent outbound WebSocket",
|
|
6
|
+
"openclaw": {
|
|
7
|
+
"extensions": ["./index.ts"],
|
|
8
|
+
"setupEntry": "./setup-entry.ts",
|
|
9
|
+
"channel": {
|
|
10
|
+
"id": "xiaoyou",
|
|
11
|
+
"label": "Xiaoyou",
|
|
12
|
+
"selectionLabel": "Xiaoyou (enterprise WebSocket)",
|
|
13
|
+
"blurb": "Bridge OpenClaw to enterprise services via persistent WebSocket.",
|
|
14
|
+
"docsPath": "/channels/xiaoyou",
|
|
15
|
+
"aliases": ["xiaoyou"],
|
|
16
|
+
"order": 50
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"ws": "^8.18.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"@types/ws": "^8.5.0",
|
|
25
|
+
"tsx": "^4.19.0",
|
|
26
|
+
"typescript": "^5.5.0",
|
|
27
|
+
"vitest": "^2.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"demo:server": "tsx demo-enterprise-server.ts"
|
|
33
|
+
}
|
|
34
|
+
}
|
package/setup-entry.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 轻量 setup 入口
|
|
3
|
+
*
|
|
4
|
+
* 当 channel 未启用时,OpenClaw 只加载此文件,
|
|
5
|
+
* 不会引入 ws 等重型依赖。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { xiayouPlugin } from "./src/channel.js";
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
id: "xiaoyou",
|
|
12
|
+
name: "Xiaoyou",
|
|
13
|
+
description: "Bridge OpenClaw to enterprise services via WebSocket",
|
|
14
|
+
setup: {
|
|
15
|
+
resolveAccount: xiayouPlugin.config.resolveAccount,
|
|
16
|
+
inspectAccount: xiayouPlugin.config.inspectAccount,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { xiayouPlugin } from "./channel.js";
|
|
3
|
+
|
|
4
|
+
const fullCfg = {
|
|
5
|
+
channels: {
|
|
6
|
+
xiaoyou: {
|
|
7
|
+
wsUrl: "wss://im.corp.example.com/ws",
|
|
8
|
+
authToken: "test-token-123",
|
|
9
|
+
allowFrom: ["user-1", "user-2"],
|
|
10
|
+
dmSecurity: "allowlist",
|
|
11
|
+
reconnectIntervalMs: 5000,
|
|
12
|
+
heartbeatIntervalMs: 15000,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe("config adapter", () => {
|
|
18
|
+
it("resolves account with all fields", () => {
|
|
19
|
+
const acct = xiayouPlugin.config.resolveAccount(fullCfg, undefined);
|
|
20
|
+
expect(acct.wsUrl).toBe("wss://im.corp.example.com/ws");
|
|
21
|
+
expect(acct.authToken).toBe("test-token-123");
|
|
22
|
+
expect(acct.allowFrom).toEqual(["user-1", "user-2"]);
|
|
23
|
+
expect(acct.reconnectIntervalMs).toBe(5000);
|
|
24
|
+
expect(acct.heartbeatIntervalMs).toBe(15000);
|
|
25
|
+
expect(acct.heartbeatTimeoutMs).toBe(10000);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("applies defaults for optional fields", () => {
|
|
29
|
+
const cfg = { channels: { xiaoyou: { wsUrl: "ws://localhost:9090", authToken: "t" } } };
|
|
30
|
+
const acct = xiayouPlugin.config.resolveAccount(cfg, undefined);
|
|
31
|
+
expect(acct.reconnectIntervalMs).toBe(3000);
|
|
32
|
+
expect(acct.maxReconnectAttempts).toBe(0);
|
|
33
|
+
expect(acct.heartbeatIntervalMs).toBe(30000);
|
|
34
|
+
expect(acct.heartbeatTimeoutMs).toBe(10000);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("throws when wsUrl missing", () => {
|
|
38
|
+
expect(() => xiayouPlugin.config.resolveAccount({ channels: { xiaoyou: {} } }, undefined))
|
|
39
|
+
.toThrow("wsUrl is required");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("throws when section missing", () => {
|
|
43
|
+
expect(() => xiayouPlugin.config.resolveAccount({ channels: {} }, undefined))
|
|
44
|
+
.toThrow("config section not found");
|
|
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
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("inspectAccount", () => {
|
|
80
|
+
it("reports configured", () => {
|
|
81
|
+
const r = xiayouPlugin.config.inspectAccount(fullCfg, undefined);
|
|
82
|
+
expect(r.enabled).toBe(true);
|
|
83
|
+
expect(r.configured).toBe(true);
|
|
84
|
+
expect(r.tokenStatus).toBe("available");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("reports not configured", () => {
|
|
88
|
+
const r = xiayouPlugin.config.inspectAccount({ channels: {} }, undefined);
|
|
89
|
+
expect(r.enabled).toBe(false);
|
|
90
|
+
expect(r.configured).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
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
|
+
});
|