openclaw-xiaoyou 1.0.0 → 1.0.1

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 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
@@ -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
- ### 8.1 异常场景处理矩阵
415
+ ### 7.1 异常场景处理矩阵
456
416
 
457
417
  ```
458
418
  ┌──────────────────────┬─────────────────────────┬──────────────────────────┐
@@ -473,7 +433,7 @@ xiaoyou 支持同时连接多个企业服务(如销售系统 + 客服系统)
473
433
  └──────────────────────┴─────────────────────────┴──────────────────────────┘
474
434
  ```
475
435
 
476
- ### 8.2 重连时序
436
+ ### 7.2 重连时序
477
437
 
478
438
  ```
479
439
  时间轴 ──────────────────────────────────────────────────────▶
@@ -490,7 +450,7 @@ xiaoyou 支持同时连接多个企业服务(如销售系统 + 客服系统)
490
450
  ═══════════════▶
491
451
  ```
492
452
 
493
- ## 9. 企业服务端实现要点
453
+ ## 8. 企业服务端实现要点
494
454
 
495
455
  企业侧需要实现一个 WebSocket Server,处理以下职责:
496
456
 
@@ -523,7 +483,7 @@ xiaoyou 支持同时连接多个企业服务(如销售系统 + 客服系统)
523
483
  └─────────────────────────────────────────────────────────┘
524
484
  ```
525
485
 
526
- ### 9.2 企业服务端伪代码示例
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
- ## 10. 部署拓扑
549
+ ## 9. 部署拓扑
590
550
 
591
- ### 10.1 最简部署(单机)
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
- ### 10.2 生产部署(分离)
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
- ## 11. 快速对照表
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
- | 怎么连多个企业服务? | §7 多账号场景 |
637
- | 出了问题怎么处理? | §8 异常处理与容错 |
638
- | 企业侧要实现什么? | §9 企业服务端实现要点 |
639
- | 怎么部署? | §10 部署拓扑 |
596
+ | 出了问题怎么处理? | §7 异常处理与容错 |
597
+ | 企业侧要实现什么? | §8 企业服务端实现要点 |
598
+ | 怎么部署? | §9 部署拓扑 |
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: { accountId: "default" },
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
  });
@@ -55,18 +55,6 @@
55
55
  "type": "number",
56
56
  "default": 10000,
57
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
58
  }
71
59
  }
72
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-xiaoyou",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "description": "Xiaoyou channel plugin for OpenClaw — connects enterprise services via persistent outbound WebSocket",
6
6
  "openclaw": {
@@ -15,8 +15,8 @@ const fullCfg = {
15
15
  };
16
16
 
17
17
  describe("config adapter", () => {
18
- it("resolves account with all fields", () => {
19
- const acct = xiayouPlugin.config.resolveAccount(fullCfg, undefined);
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, undefined);
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: {} } }, undefined))
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: {} }, undefined))
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, undefined);
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: {} }, undefined);
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
- /** 每个 account 一个 EnterpriseClient */
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, accountId?: string | null): ResolvedAccount {
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
- accountId: accountId ?? acct.defaultAccount ?? null,
38
- wsUrl: acct.wsUrl,
39
- authToken: acct.authToken ?? "",
40
- allowFrom: acct.allowFrom ?? [],
41
- dmPolicy: acct.dmSecurity,
42
- reconnectIntervalMs: acct.reconnectIntervalMs ?? 3000,
43
- maxReconnectAttempts: acct.maxReconnectAttempts ?? 0,
44
- heartbeatIntervalMs: acct.heartbeatIntervalMs ?? 30000,
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, _accountId?: string | null) {
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, account.accountId);
120
-
121
- const clientKey = resolved.accountId ?? "default";
103
+ : resolveAccount(config);
122
104
 
123
105
  // 断开已有连接
124
- const existing = clients.get(clientKey);
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
- clients.set(clientKey, client);
156
- logger.info(`[xiaoyou] gateway started (account=${clientKey})`);
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
- for (const [key, c] of clients.entries()) {
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 ({ account, to, payload }: any) => {
171
- const clientKey = account.accountId ?? "default";
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
- client.sendReply({ ...baseReply, text: payload.text });
162
+ _client.sendReply({ ...baseReply, text: payload.text });
187
163
  return { ok: true };
188
164
  }
189
165
  if (payload.kind === "image" || payload.kind === "file") {
190
- client.sendReply({
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.wsUrl) issues.push({ severity: "error", message: "wsUrl not configured" });
209
- if (!account.authToken) issues.push({ severity: "warning", message: "authToken not set" });
210
- if (!client || !client.isConnected()) issues.push({ severity: "error", message: "WebSocket not connected" });
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",
@@ -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: `openclaw-xiaoyou-${account.accountId ?? "default"}`,
81
+ clientId: "openclaw-xiaoyou",
82
82
  clientVersion: "1.0.0",
83
83
  }));
84
84
  logger.info("[xiaoyou] auth sent, waiting for auth_result...");
package/src/types.ts CHANGED
@@ -7,7 +7,6 @@
7
7
  // ─── 解析后的账号配置 ────────────────────────────────────
8
8
 
9
9
  export type ResolvedAccount = {
10
- accountId: string | null;
11
10
  /** 企业 WebSocket 服务地址,如 wss://im.corp.example.com/ws */
12
11
  wsUrl: string;
13
12
  /** 连接企业服务的认证 token */