openclaw-xiaoyou 1.1.0 → 1.2.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.
@@ -323,24 +323,57 @@ install_or_upgrade() {
323
323
  patch_config() {
324
324
  [[ -n "$WS_URL" || -n "$TOKEN" ]] || return 0
325
325
 
326
- log "写入 xiaoyou channel 配置"
327
- run openclaw config set "channels.${CHANNEL_KEY}.enabled" true --strict-json
326
+ log "写入 xiaoyou channel 配置(直接修改配置文件)"
328
327
 
329
- if [[ -n "$WS_URL" ]]; then
330
- run openclaw config set "channels.${CHANNEL_KEY}.wsUrl" "$WS_URL"
331
- fi
332
- if [[ -n "$TOKEN" ]]; then
333
- run openclaw config set "channels.${CHANNEL_KEY}.authToken" "$TOKEN"
334
- fi
328
+ local config_file
329
+ config_file="$(openclaw_config_file)"
335
330
 
336
- run openclaw config set "channels.${CHANNEL_KEY}.dmSecurity" open
337
- run openclaw config set "channels.${CHANNEL_KEY}.reconnectIntervalMs" 3000 --strict-json
338
- run openclaw config set "channels.${CHANNEL_KEY}.heartbeatIntervalMs" 30000 --strict-json
339
- run openclaw config set "channels.${CHANNEL_KEY}.heartbeatTimeoutMs" 10000 --strict-json
331
+ if [[ ! -f "$config_file" ]]; then
332
+ die "配置文件不存在: $config_file"
333
+ fi
340
334
 
341
- local config_file
342
- config_file="$(capture_or_empty openclaw config file)"
343
- log "已写入配置: ${config_file:-$(openclaw_config_file)}"
335
+ # 使用 python3 直接写入配置,绕过 openclaw config set 的 channel id 校验
336
+ python3 -c "
337
+ import json, sys
338
+
339
+ config_file = '$config_file'
340
+ ws_url = '$WS_URL'
341
+ token = '$TOKEN'
342
+
343
+ try:
344
+ with open(config_file, 'r') as f:
345
+ cfg = json.load(f)
346
+ except Exception as e:
347
+ print(f'[xiaoyou-install][error] 无法读取配置文件: {e}', file=sys.stderr)
348
+ sys.exit(1)
349
+
350
+ # 确保 channels 对象存在
351
+ cfg.setdefault('channels', {})
352
+
353
+ # 写入 xiaoyou channel 配置
354
+ xiaoyou = cfg['channels'].get('xiaoyou', {})
355
+ xiaoyou['enabled'] = True
356
+ if ws_url:
357
+ xiaoyou['wsUrl'] = ws_url
358
+ if token:
359
+ xiaoyou['authToken'] = token
360
+ xiaoyou.setdefault('dmSecurity', 'open')
361
+ xiaoyou.setdefault('allowFrom', ['*'])
362
+ xiaoyou.setdefault('reconnectIntervalMs', 3000)
363
+ xiaoyou.setdefault('heartbeatIntervalMs', 30000)
364
+ xiaoyou.setdefault('heartbeatTimeoutMs', 10000)
365
+ cfg['channels']['xiaoyou'] = xiaoyou
366
+
367
+ try:
368
+ with open(config_file, 'w') as f:
369
+ json.dump(cfg, f, indent=2, ensure_ascii=False)
370
+ print('[xiaoyou-install] 配置写入成功')
371
+ except Exception as e:
372
+ print(f'[xiaoyou-install][error] 写入配置失败: {e}', file=sys.stderr)
373
+ sys.exit(1)
374
+ " || die "写入配置失败"
375
+
376
+ log "已写入配置: $config_file"
344
377
  }
345
378
 
346
379
  # -----------------------------------------------------------------------------
@@ -375,11 +408,31 @@ xiayou_status() {
375
408
 
376
409
  xiayou_uninstall_plugin_only() {
377
410
  # 仅删除插件记录和文件,不删除配置(升级时用)
378
- if command_exists openclaw; then
379
- run_or_dry_run openclaw config unset "plugins.entries.${PLUGIN_NAME}" || true
380
- run_or_dry_run openclaw config unset "plugins.installs.${PLUGIN_NAME}" || true
411
+ local config_file
412
+ config_file="$(openclaw_config_file)"
413
+
414
+ if [[ -f "$config_file" ]] && command_exists python3; then
415
+ python3 -c "
416
+ import json
417
+ config_file = '$config_file'
418
+ with open(config_file, 'r') as f:
419
+ cfg = json.load(f)
420
+ changed = False
421
+ for key in ['xiaoyou', 'openclaw-xiaoyou']:
422
+ if cfg.get('plugins', {}).get('entries', {}).pop(key, None): changed = True
423
+ if cfg.get('plugins', {}).get('installs', {}).pop(key, None): changed = True
424
+ allow = cfg.get('plugins', {}).get('allow', [])
425
+ if key in allow:
426
+ allow.remove(key)
427
+ changed = True
428
+ if changed:
429
+ with open(config_file, 'w') as f:
430
+ json.dump(cfg, f, indent=2, ensure_ascii=False)
431
+ print('[xiaoyou-install] 已清理插件注册记录')
432
+ " || true
381
433
  fi
382
- run_or_dry_run rm -rf "$HOME/.openclaw/extensions/${PLUGIN_NAME}" "$HOME"/.openclaw/extensions/.openclaw-* || true
434
+
435
+ run_or_dry_run rm -rf "$HOME/.openclaw/extensions/xiaoyou" "$HOME/.openclaw/extensions/openclaw-xiaoyou" "$HOME"/.openclaw/extensions/.openclaw-* || true
383
436
  }
384
437
 
385
438
  xiayou_uninstall() {
@@ -392,13 +445,29 @@ xiayou_uninstall() {
392
445
  fi
393
446
 
394
447
  log "开始卸载 xiaoyou channel"
395
- if command_exists openclaw; then
396
- run_or_dry_run openclaw config unset "channels.${CHANNEL_KEY}" || warn "删除 channels.${CHANNEL_KEY} 失败"
397
- run_or_dry_run openclaw config unset "plugins.entries.${PLUGIN_NAME}" || warn "删除 plugins.entries 失败"
398
- run_or_dry_run openclaw config unset "plugins.installs.${PLUGIN_NAME}" || warn "删除 plugins.installs 失败"
448
+
449
+ local config_file
450
+ config_file="$(openclaw_config_file)"
451
+
452
+ if [[ -f "$config_file" ]] && command_exists python3; then
453
+ run_or_dry_run python3 -c "
454
+ import json
455
+ config_file = '$config_file'
456
+ with open(config_file, 'r') as f:
457
+ cfg = json.load(f)
458
+ cfg.get('channels', {}).pop('xiaoyou', None)
459
+ for key in ['xiaoyou', 'openclaw-xiaoyou']:
460
+ cfg.get('plugins', {}).get('entries', {}).pop(key, None)
461
+ cfg.get('plugins', {}).get('installs', {}).pop(key, None)
462
+ allow = cfg.get('plugins', {}).get('allow', [])
463
+ if key in allow: allow.remove(key)
464
+ with open(config_file, 'w') as f:
465
+ json.dump(cfg, f, indent=2, ensure_ascii=False)
466
+ print('[xiaoyou-install] 已清理所有 xiaoyou 配置')
467
+ " || warn "清理配置失败"
399
468
  fi
400
469
 
401
- run_or_dry_run rm -rf "$HOME/.openclaw/extensions/${PLUGIN_NAME}" "$HOME"/.openclaw/extensions/.openclaw-* || warn "删除插件文件失败"
470
+ run_or_dry_run rm -rf "$HOME/.openclaw/extensions/xiaoyou" "$HOME/.openclaw/extensions/openclaw-xiaoyou" "$HOME"/.openclaw/extensions/.openclaw-* || warn "删除插件文件失败"
402
471
 
403
472
  if [[ "$CHECK_ONLY" -eq 1 ]]; then
404
473
  phase_end "xiaoyou.uninstall" "planned" "check-only:已输出计划,未执行"
@@ -424,6 +493,13 @@ main() {
424
493
 
425
494
  install_or_upgrade
426
495
 
496
+ # 重启 gateway 让插件和配置生效
497
+ if [[ "$CHECK_ONLY" -eq 0 ]] && openclaw_gateway_running; then
498
+ log "重启 gateway 以加载插件和配置..."
499
+ run openclaw gateway restart || warn "gateway 重启失败,请手动执行: openclaw gateway restart"
500
+ sleep 5
501
+ fi
502
+
427
503
  log "完成。企业服务端应能收到来自 xiaoyou 插件的 WebSocket 连接。"
428
504
  phase_end "main.entry" "ok" "安装流程完成"
429
505
  }
package/index.ts CHANGED
@@ -3,10 +3,6 @@
3
3
  *
4
4
  * 安装方式:
5
5
  * openclaw plugins install openclaw-xiaoyou
6
- * openclaw plugins enable openclaw-xiaoyou
7
- *
8
- * 配置方式:
9
- * openclaw channels add --channel xiaoyou
10
6
  */
11
7
 
12
8
  import { xiayouPlugin, setRuntime } from "./src/channel.js";
@@ -19,7 +15,7 @@ const plugin = {
19
15
 
20
16
  register(api: any) {
21
17
  // 注入 runtime,供 gateway/outbound 内部使用
22
- setRuntime(api.runtime);
18
+ if (api.runtime) setRuntime(api.runtime);
23
19
 
24
20
  // 注册 channel
25
21
  api.registerChannel({ plugin: xiayouPlugin });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-xiaoyou",
3
- "version": "1.1.0",
3
+ "version": "1.2.1",
4
4
  "type": "module",
5
5
  "description": "Xiaoyou channel plugin for OpenClaw — connects enterprise services via persistent outbound WebSocket",
6
6
  "openclaw": {
package/setup-entry.ts CHANGED
@@ -13,6 +13,6 @@ export default {
13
13
  description: "Bridge OpenClaw to enterprise services via WebSocket",
14
14
  setup: {
15
15
  resolveAccount: xiayouPlugin.config.resolveAccount,
16
- inspectAccount: xiayouPlugin.config.inspectAccount,
16
+ isConfigured: xiayouPlugin.config.isConfigured,
17
17
  },
18
18
  };
package/src/channel.ts CHANGED
@@ -3,8 +3,6 @@
3
3
  *
4
4
  * 运行在 OpenClaw Gateway 进程内,通过 gateway adapter 管理
5
5
  * 与企业服务的 WebSocket 连接生命周期。
6
- *
7
- * 用户通过 OpenClaw 标准 channel 配置来设置企业服务地址等参数。
8
6
  */
9
7
 
10
8
  import type { ResolvedAccount } from "./types.js";
@@ -14,7 +12,6 @@ import { createEnterpriseClient, type EnterpriseClient } from "./enterprise-clie
14
12
 
15
13
  let _client: EnterpriseClient | null = null;
16
14
 
17
- /** OpenClaw runtime API 引用,由 index.ts register() 注入 */
18
15
  let _runtime: any = null;
19
16
  export function setRuntime(rt: any) { _runtime = rt; }
20
17
  export function getRuntime() { return _runtime; }
@@ -25,6 +22,12 @@ function getChannelConfig(cfg: any): any {
25
22
  return cfg?.channels?.xiaoyou ?? {};
26
23
  }
27
24
 
25
+ function listAccountIds(cfg: any): string[] {
26
+ const section = getChannelConfig(cfg);
27
+ if (section?.wsUrl) return ["default"];
28
+ return [];
29
+ }
30
+
28
31
  function resolveAccount(cfg: any, accountId?: string | null): any {
29
32
  const section = getChannelConfig(cfg);
30
33
  const id = accountId || "default";
@@ -35,32 +38,10 @@ function resolveAccount(cfg: any, accountId?: string | null): any {
35
38
  config: section,
36
39
  enabled: section.enabled !== false,
37
40
  configured,
38
- wsUrl: section.wsUrl ?? "",
39
- authToken: section.authToken ?? "",
40
- allowFrom: section.allowFrom ?? [],
41
- dmPolicy: section.dmSecurity,
42
- reconnectIntervalMs: section.reconnectIntervalMs ?? 3000,
43
- maxReconnectAttempts: section.maxReconnectAttempts ?? 0,
44
- heartbeatIntervalMs: section.heartbeatIntervalMs ?? 30000,
45
- heartbeatTimeoutMs: section.heartbeatTimeoutMs ?? 10000,
46
- };
47
- }
48
-
49
- function inspectAccount(cfg: any) {
50
- const section = getChannelConfig(cfg);
51
- return {
52
- enabled: Boolean(section?.wsUrl),
53
- configured: Boolean(section?.wsUrl),
54
- tokenStatus: section?.authToken ? "available" : "missing",
41
+ name: "Xiaoyou",
55
42
  };
56
43
  }
57
44
 
58
- function listAccountIds(cfg: any): string[] {
59
- const section = getChannelConfig(cfg);
60
- if (section?.wsUrl) return ["default"];
61
- return [];
62
- }
63
-
64
45
  // ─── Channel Plugin ──────────────────────────────────
65
46
 
66
47
  export const xiayouPlugin = {
@@ -89,11 +70,11 @@ export const xiayouPlugin = {
89
70
  blockStreaming: false,
90
71
  },
91
72
 
92
- // ── Config(必须)──────────────────────────────────
73
+ reload: { configPrefixes: ["channels.xiaoyou"] },
74
+
93
75
  config: {
94
- resolveAccount,
95
- inspectAccount,
96
76
  listAccountIds,
77
+ resolveAccount,
97
78
  isConfigured: (account: any): boolean => Boolean(account?.config?.wsUrl),
98
79
  describeAccount: (account: any) => ({
99
80
  accountId: account?.accountId || "default",
@@ -103,37 +84,41 @@ export const xiayouPlugin = {
103
84
  defaultAccountId: (): string => "default",
104
85
  },
105
86
 
106
- // ── DM 安全策略 ────────────────────────────────────
107
87
  security: {
108
88
  dm: {
109
89
  channelKey: "xiaoyou",
110
- resolvePolicy: (account: ResolvedAccount) => account.dmPolicy,
111
- resolveAllowFrom: (account: ResolvedAccount) => account.allowFrom,
112
- defaultPolicy: "allowlist",
90
+ resolvePolicy: (account: any) => account?.config?.dmSecurity ?? "open",
91
+ resolveAllowFrom: (account: any) => account?.config?.allowFrom ?? ["*"],
92
+ defaultPolicy: "open",
113
93
  },
114
94
  },
115
95
 
116
- // ── 回复线程模式 ───────────────────────────────────
117
96
  threading: { topLevelReplyToMode: "reply" as const },
118
97
 
119
98
  // ── Gateway 生命周期 ───────────────────────────────
120
- reload: { configPrefixes: ["channels.xiaoyou"] },
121
-
122
99
  gateway: {
123
100
  startAccount: async (ctx: any) => {
124
101
  const { account, cfg, log } = ctx;
102
+
103
+ // 注入 runtime
104
+ if (ctx.runtime) setRuntime(ctx.runtime);
105
+
125
106
  const section = account?.config || getChannelConfig(cfg);
126
107
  const logger = log || console;
127
108
 
109
+ if (!section?.wsUrl) {
110
+ throw new Error("xiaoyou: wsUrl is required");
111
+ }
112
+
128
113
  const resolved: ResolvedAccount = {
129
- wsUrl: section?.wsUrl ?? "",
130
- authToken: section?.authToken ?? "",
131
- allowFrom: section?.allowFrom ?? [],
132
- dmPolicy: section?.dmSecurity,
133
- reconnectIntervalMs: section?.reconnectIntervalMs ?? 3000,
134
- maxReconnectAttempts: section?.maxReconnectAttempts ?? 0,
135
- heartbeatIntervalMs: section?.heartbeatIntervalMs ?? 30000,
136
- heartbeatTimeoutMs: section?.heartbeatTimeoutMs ?? 10000,
114
+ wsUrl: section.wsUrl,
115
+ authToken: section.authToken ?? "",
116
+ allowFrom: section.allowFrom ?? [],
117
+ dmPolicy: section.dmSecurity ?? "open",
118
+ reconnectIntervalMs: section.reconnectIntervalMs ?? 3000,
119
+ maxReconnectAttempts: section.maxReconnectAttempts ?? 0,
120
+ heartbeatIntervalMs: section.heartbeatIntervalMs ?? 30000,
121
+ heartbeatTimeoutMs: section.heartbeatTimeoutMs ?? 10000,
137
122
  };
138
123
 
139
124
  // 断开已有连接
@@ -145,7 +130,7 @@ export const xiayouPlugin = {
145
130
  onMessage: async (msg) => {
146
131
  const runtime = getRuntime();
147
132
  if (!runtime) {
148
- logger.error("[xiaoyou] runtime not available");
133
+ logger.error("[xiaoyou] runtime not available, cannot dispatch");
149
134
  return;
150
135
  }
151
136
  await runtime.inbound.dispatch({
@@ -168,7 +153,18 @@ export const xiayouPlugin = {
168
153
  client.connect();
169
154
  _client = client;
170
155
  logger.info("[xiaoyou] gateway started");
171
- return client;
156
+
157
+ // 保持 startAccount 挂起,直到 abortSignal 触发
158
+ return new Promise<void>((resolve) => {
159
+ if (ctx.abortSignal) {
160
+ ctx.abortSignal.addEventListener("abort", () => {
161
+ logger.info("[xiaoyou] abortSignal received, disconnecting");
162
+ client.disconnect();
163
+ if (_client === client) _client = null;
164
+ resolve();
165
+ });
166
+ }
167
+ });
172
168
  },
173
169
 
174
170
  stopAccount: async () => {
@@ -211,8 +207,8 @@ export const xiayouPlugin = {
211
207
  status: {
212
208
  describe: async ({ account }: any) => {
213
209
  const issues: Array<{ severity: string; message: string }> = [];
214
- const wsUrl = account?.wsUrl ?? account?.config?.wsUrl;
215
- const authToken = account?.authToken ?? account?.config?.authToken;
210
+ const wsUrl = account?.config?.wsUrl;
211
+ const authToken = account?.config?.authToken;
216
212
 
217
213
  if (!wsUrl) issues.push({ severity: "error", message: "wsUrl not configured" });
218
214
  if (!authToken) issues.push({ severity: "warning", message: "authToken not set" });